mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
dnd-kit POC
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
"build"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@juggle/resize-observer": "^3.1.2",
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@reach/listbox": "^0.18.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// @ts-strict-ignore
|
||||
import React from 'react';
|
||||
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { css } from 'glamor';
|
||||
|
||||
import { type AccountEntity } from 'loot-core/src/types/models';
|
||||
@@ -9,13 +11,6 @@ import { styles, theme, type CSSProperties } from '../../style';
|
||||
import { AlignedText } from '../common/AlignedText';
|
||||
import { AnchorLink } from '../common/AnchorLink';
|
||||
import { View } from '../common/View';
|
||||
import {
|
||||
useDraggable,
|
||||
useDroppable,
|
||||
DropHighlight,
|
||||
type OnDragChangeCallback,
|
||||
type OnDropCallback,
|
||||
} from '../sort';
|
||||
import { type Binding } from '../spreadsheet';
|
||||
import { CellValue } from '../spreadsheet/CellValue';
|
||||
|
||||
@@ -42,8 +37,6 @@ type AccountProps = {
|
||||
updated?: boolean;
|
||||
style?: CSSProperties;
|
||||
outerStyle?: CSSProperties;
|
||||
onDragChange?: OnDragChangeCallback<{ id: string }>;
|
||||
onDrop?: OnDropCallback;
|
||||
};
|
||||
|
||||
export function Account({
|
||||
@@ -56,92 +49,85 @@ export function Account({
|
||||
query,
|
||||
style,
|
||||
outerStyle,
|
||||
onDragChange,
|
||||
onDrop,
|
||||
}: AccountProps) {
|
||||
const type = account
|
||||
? account.closed
|
||||
? 'account-closed'
|
||||
: account.offbudget
|
||||
? 'account-offbudget'
|
||||
: 'account-onbudget'
|
||||
: 'title';
|
||||
const {
|
||||
isDragging,
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({ id: account?.id || `sortable-account-${name}` });
|
||||
|
||||
const { dragRef } = useDraggable({
|
||||
type,
|
||||
onDragChange,
|
||||
item: { id: account && account.id },
|
||||
canDrag: account != null,
|
||||
});
|
||||
|
||||
const { dropRef, dropPos } = useDroppable({
|
||||
types: account ? [type] : [],
|
||||
id: account && account.id,
|
||||
onDrop,
|
||||
});
|
||||
const dndStyle = {
|
||||
opacity: isDragging ? 0.5 : undefined,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<View innerRef={dropRef} style={{ flexShrink: 0, ...outerStyle }}>
|
||||
<View>
|
||||
<DropHighlight pos={dropPos} />
|
||||
<View innerRef={dragRef}>
|
||||
<AnchorLink
|
||||
to={to}
|
||||
style={{
|
||||
...accountNameStyle,
|
||||
...style,
|
||||
position: 'relative',
|
||||
borderLeft: '4px solid transparent',
|
||||
...(updated && { fontWeight: 700 }),
|
||||
}}
|
||||
activeStyle={{
|
||||
borderColor: theme.sidebarItemAccentSelected,
|
||||
color: theme.sidebarItemTextSelected,
|
||||
// This is kind of a hack, but we don't ever want the account
|
||||
// that the user is looking at to be "bolded" which means it
|
||||
// has unread transactions. The system does mark is read and
|
||||
// unbolds it, but it still "flashes" bold so this just
|
||||
// ignores it if it's active
|
||||
fontWeight: (style && style.fontWeight) || 'normal',
|
||||
'& .dot': {
|
||||
backgroundColor: theme.sidebarItemAccentSelected,
|
||||
transform: 'translateX(-4.5px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`dot ${css({
|
||||
marginRight: 3,
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: 5,
|
||||
backgroundColor: failed
|
||||
? theme.sidebarItemBackgroundFailed
|
||||
: theme.sidebarItemBackgroundPositive,
|
||||
marginLeft: 2,
|
||||
transition: 'transform .3s',
|
||||
opacity: connected ? 1 : 0,
|
||||
})}`}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<AlignedText
|
||||
left={name}
|
||||
right={<CellValue binding={query} type="financial" />}
|
||||
/>
|
||||
</AnchorLink>
|
||||
<View
|
||||
innerRef={setNodeRef}
|
||||
style={{ flexShrink: 0, ...outerStyle, ...dndStyle }}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<AnchorLink
|
||||
to={to}
|
||||
style={{
|
||||
...accountNameStyle,
|
||||
...style,
|
||||
position: 'relative',
|
||||
borderLeft: '4px solid transparent',
|
||||
...(updated && { fontWeight: 700 }),
|
||||
...(isDragging && { pointerEvents: 'none' }),
|
||||
}}
|
||||
activeStyle={{
|
||||
borderColor: theme.sidebarItemAccentSelected,
|
||||
color: theme.sidebarItemTextSelected,
|
||||
// This is kind of a hack, but we don't ever want the account
|
||||
// that the user is looking at to be "bolded" which means it
|
||||
// has unread transactions. The system does mark is read and
|
||||
// unbolds it, but it still "flashes" bold so this just
|
||||
// ignores it if it's active
|
||||
fontWeight: (style && style.fontWeight) || 'normal',
|
||||
'& .dot': {
|
||||
backgroundColor: theme.sidebarItemAccentSelected,
|
||||
transform: 'translateX(-4.5px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`dot ${css({
|
||||
marginRight: 3,
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: 5,
|
||||
backgroundColor: failed
|
||||
? theme.sidebarItemBackgroundFailed
|
||||
: theme.sidebarItemBackgroundPositive,
|
||||
marginLeft: 2,
|
||||
transition: 'transform .3s',
|
||||
opacity: connected ? 1 : 0,
|
||||
})}`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AlignedText
|
||||
left={name}
|
||||
right={<CellValue binding={query} type="financial" />}
|
||||
/>
|
||||
</AnchorLink>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
|
||||
import { type AccountEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { SvgAdd } from '../../icons/v1';
|
||||
import { View } from '../common/View';
|
||||
import { type OnDropCallback } from '../sort';
|
||||
import { type Binding } from '../spreadsheet';
|
||||
|
||||
import { Account } from './Account';
|
||||
@@ -34,7 +48,7 @@ type AccountsProps = {
|
||||
showClosedAccounts: boolean;
|
||||
onAddAccount: () => void;
|
||||
onToggleClosedAccounts: () => void;
|
||||
onReorder: OnDropCallback;
|
||||
onReorder: (id: string, dropPos: 'top' | 'bottom', targetId: string) => void;
|
||||
};
|
||||
|
||||
export function Accounts({
|
||||
@@ -54,7 +68,6 @@ export function Accounts({
|
||||
onToggleClosedAccounts,
|
||||
onReorder,
|
||||
}: AccountsProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const offbudgetAccounts = useMemo(
|
||||
() =>
|
||||
accounts.filter(
|
||||
@@ -74,100 +87,120 @@ export function Accounts({
|
||||
[accounts],
|
||||
);
|
||||
|
||||
function onDragChange(drag) {
|
||||
setIsDragging(drag.state === 'start');
|
||||
}
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const makeDropPadding = i => {
|
||||
if (i === 0) {
|
||||
return {
|
||||
paddingTop: isDragging ? 15 : 0,
|
||||
marginTop: isDragging ? -15 : 0,
|
||||
};
|
||||
const onDragEnd = e => {
|
||||
const { active, over } = e;
|
||||
|
||||
if (active.id !== over.id) {
|
||||
const dropPos =
|
||||
active.data.current.sortable.index < over.data.current.sortable.index
|
||||
? 'bottom'
|
||||
: 'top';
|
||||
|
||||
onReorder(active.id, dropPos, over.id);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Account
|
||||
name="All accounts"
|
||||
to={allAccountsPath}
|
||||
query={getAllAccountBalance()}
|
||||
style={{ fontWeight, marginTop: 15 }}
|
||||
/>
|
||||
|
||||
{budgetedAccounts.length > 0 && (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<Account
|
||||
name="For budget"
|
||||
to={budgetedAccountPath}
|
||||
query={getOnBudgetBalance()}
|
||||
style={{ fontWeight, marginTop: 13 }}
|
||||
name="All accounts"
|
||||
to={allAccountsPath}
|
||||
query={getAllAccountBalance()}
|
||||
style={{ fontWeight, marginTop: 15 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{budgetedAccounts.map((account, i) => (
|
||||
<Account
|
||||
key={account.id}
|
||||
name={account.name}
|
||||
account={account}
|
||||
connected={!!account.bank}
|
||||
failed={failedAccounts && failedAccounts.has(account.id)}
|
||||
updated={updatedAccounts && updatedAccounts.includes(account.id)}
|
||||
to={getAccountPath(account)}
|
||||
query={getBalanceQuery(account)}
|
||||
onDragChange={onDragChange}
|
||||
onDrop={onReorder}
|
||||
outerStyle={makeDropPadding(i)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{offbudgetAccounts.length > 0 && (
|
||||
<Account
|
||||
name="Off budget"
|
||||
to={offBudgetAccountPath}
|
||||
query={getOffBudgetBalance()}
|
||||
style={{ fontWeight, marginTop: 13 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{offbudgetAccounts.map((account, i) => (
|
||||
<Account
|
||||
key={account.id}
|
||||
name={account.name}
|
||||
account={account}
|
||||
connected={!!account.bank}
|
||||
failed={failedAccounts && failedAccounts.has(account.id)}
|
||||
updated={updatedAccounts && updatedAccounts.includes(account.id)}
|
||||
to={getAccountPath(account)}
|
||||
query={getBalanceQuery(account)}
|
||||
onDragChange={onDragChange}
|
||||
onDrop={onReorder}
|
||||
outerStyle={makeDropPadding(i)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{closedAccounts.length > 0 && (
|
||||
<SecondaryItem
|
||||
style={{ marginTop: 15 }}
|
||||
title={'Closed accounts' + (showClosedAccounts ? '' : '...')}
|
||||
onClick={onToggleClosedAccounts}
|
||||
bold
|
||||
/>
|
||||
)}
|
||||
|
||||
{showClosedAccounts &&
|
||||
closedAccounts.map(account => (
|
||||
{budgetedAccounts.length > 0 && (
|
||||
<Account
|
||||
key={account.id}
|
||||
name={account.name}
|
||||
account={account}
|
||||
to={getAccountPath(account)}
|
||||
query={getBalanceQuery(account)}
|
||||
onDragChange={onDragChange}
|
||||
onDrop={onReorder}
|
||||
name="For budget"
|
||||
to={budgetedAccountPath}
|
||||
query={getOnBudgetBalance()}
|
||||
style={{ fontWeight, marginTop: 13 }}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
<SortableContext
|
||||
items={budgetedAccounts}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{budgetedAccounts.map((account, i) => (
|
||||
<Account
|
||||
key={account.id}
|
||||
name={account.name}
|
||||
account={account}
|
||||
connected={!!account.bank}
|
||||
failed={failedAccounts && failedAccounts.has(account.id)}
|
||||
updated={updatedAccounts && updatedAccounts.includes(account.id)}
|
||||
to={getAccountPath(account)}
|
||||
query={getBalanceQuery(account)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
{offbudgetAccounts.length > 0 && (
|
||||
<Account
|
||||
name="Off budget"
|
||||
to={offBudgetAccountPath}
|
||||
query={getOffBudgetBalance()}
|
||||
style={{ fontWeight, marginTop: 13 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SortableContext
|
||||
items={offbudgetAccounts}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{offbudgetAccounts.map((account, i) => (
|
||||
<Account
|
||||
key={account.id}
|
||||
name={account.name}
|
||||
account={account}
|
||||
connected={!!account.bank}
|
||||
failed={failedAccounts && failedAccounts.has(account.id)}
|
||||
updated={updatedAccounts && updatedAccounts.includes(account.id)}
|
||||
to={getAccountPath(account)}
|
||||
query={getBalanceQuery(account)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
{closedAccounts.length > 0 && (
|
||||
<SecondaryItem
|
||||
style={{ marginTop: 15 }}
|
||||
title={'Closed accounts' + (showClosedAccounts ? '' : '...')}
|
||||
onClick={onToggleClosedAccounts}
|
||||
bold
|
||||
/>
|
||||
)}
|
||||
|
||||
{showClosedAccounts && (
|
||||
<SortableContext
|
||||
items={offbudgetAccounts}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{closedAccounts.map((account, i) => (
|
||||
<Account
|
||||
key={account.id}
|
||||
name={account.name}
|
||||
account={account}
|
||||
to={getAccountPath(account)}
|
||||
query={getBalanceQuery(account)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
)}
|
||||
</DndContext>
|
||||
|
||||
<SecondaryItem
|
||||
style={{
|
||||
|
||||
66
yarn.lock
66
yarn.lock
@@ -58,6 +58,10 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/web@workspace:packages/desktop-client"
|
||||
dependencies:
|
||||
"@dnd-kit/core": "npm:^6.1.0"
|
||||
"@dnd-kit/modifiers": "npm:^7.0.0"
|
||||
"@dnd-kit/sortable": "npm:^8.0.0"
|
||||
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||
"@juggle/resize-observer": "npm:^3.1.2"
|
||||
"@playwright/test": "npm:^1.41.1"
|
||||
"@reach/listbox": "npm:^0.18.0"
|
||||
@@ -1744,6 +1748,68 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@dnd-kit/accessibility@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@dnd-kit/accessibility@npm:3.1.0"
|
||||
dependencies:
|
||||
tslib: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 750a0537877d5dde3753e9ef59d19628b553567e90fc3e3b14a79bded08f47f4a7161bc0d003d7cd6b3bd9e10aa233628dca07d2aa5a2120cac84555ba1653d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@dnd-kit/core@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "@dnd-kit/core@npm:6.1.0"
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility": "npm:^3.1.0"
|
||||
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||
tslib: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: cf9e99763fbd9220cb6fdde2950c19fdf6248391234f5ee835601814124445fd8a6e4b3f5bc35543c802d359db8cc47f07d87046577adc41952ae981a03fbda0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@dnd-kit/modifiers@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "@dnd-kit/modifiers@npm:7.0.0"
|
||||
dependencies:
|
||||
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||
tslib: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
"@dnd-kit/core": ^6.1.0
|
||||
react: ">=16.8.0"
|
||||
checksum: 9ee0b7b86c23c15f6820d76ec398724597abc9d9e31cf58836e7f0b9935e33f9136a60ee9600eb27818447623f07786d4fed3f1d685d9cc6d860d8f6c5354ae3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@dnd-kit/sortable@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "@dnd-kit/sortable@npm:8.0.0"
|
||||
dependencies:
|
||||
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||
tslib: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
"@dnd-kit/core": ^6.1.0
|
||||
react: ">=16.8.0"
|
||||
checksum: e2e0d37ace13db2e6aceb65a803195ef29e1a33a37e7722a988d7a9c1aacce77472a93b2adcd8e6780ac98b3d5640c5481892f530177c2eb966df235726942ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@dnd-kit/utilities@npm:^3.2.2":
|
||||
version: 3.2.2
|
||||
resolution: "@dnd-kit/utilities@npm:3.2.2"
|
||||
dependencies:
|
||||
tslib: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 6cfe46a5fcdaced943982e7ae66b08b89235493e106eb5bc833737c25905e13375c6ecc3aa0c357d136cb21dae3966213dba063f19b7a60b1235a29a7b05ff84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@electron/asar@npm:^3.2.1":
|
||||
version: 3.2.4
|
||||
resolution: "@electron/asar@npm:3.2.4"
|
||||
|
||||
Reference in New Issue
Block a user