From 2d4a2814d4685b1535d8435e9e0da771335995f7 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sun, 2 Mar 2025 21:47:14 +0000 Subject: [PATCH 01/12] Extract existing "Make Transfer" functionality --- .../src/components/accounts/Account.tsx | 50 +++---------------- .../src/hooks/useTransactionBatchActions.ts | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/packages/desktop-client/src/components/accounts/Account.tsx b/packages/desktop-client/src/components/accounts/Account.tsx index fb22b91d1d1..1675bf72484 100644 --- a/packages/desktop-client/src/components/accounts/Account.tsx +++ b/packages/desktop-client/src/components/accounts/Account.tsx @@ -308,6 +308,7 @@ type AccountInternalProps = { hideFraction: boolean; accountsSyncing: string[]; dispatch: AppDispatch; + onSetTransfer: ReturnType['onSetTransfer']; }; type AccountInternalState = { search: string; @@ -1336,49 +1337,11 @@ class AccountInternal extends PureComponent< }; onSetTransfer = async (ids: string[]) => { - const onConfirmTransfer = async (ids: string[]) => { - this.setState({ workingHard: true }); - - const payees = this.props.payees; - - const { data: transactions } = await runQuery( - q('transactions') - .filter({ id: { $oneof: ids } }) - .select('*'), - ); - const [fromTrans, toTrans] = transactions; - - if (transactions.length === 2 && validForTransfer(fromTrans, toTrans)) { - const fromPayee = payees.find( - p => p.transfer_acct === fromTrans.account, - ); - const toPayee = payees.find(p => p.transfer_acct === toTrans.account); - - const changes = { - updated: [ - { - ...fromTrans, - payee: toPayee?.id, - transfer_id: toTrans.id, - }, - { - ...toTrans, - payee: fromPayee?.id, - transfer_id: fromTrans.id, - }, - ], - }; - - await send('transactions-batch-update', changes); - } - - await this.refetchTransactions(); - }; - - await this.checkForReconciledTransactions( + this.setState({ workingHard: true }); + await this.props.onSetTransfer( ids, - 'batchEditWithReconciled', - onConfirmTransfer, + this.props.payees, + this.refetchTransactions, ); }; @@ -1923,6 +1886,7 @@ type AccountHackProps = Omit< | 'onBatchLinkSchedule' | 'onBatchUnlinkSchedule' | 'onBatchDelete' + | 'onSetTransfer' >; function AccountHack(props: AccountHackProps) { @@ -1934,6 +1898,7 @@ function AccountHack(props: AccountHackProps) { onBatchLinkSchedule, onBatchUnlinkSchedule, onBatchDelete, + onSetTransfer, } = useTransactionBatchActions(); return ( @@ -1945,6 +1910,7 @@ function AccountHack(props: AccountHackProps) { onBatchLinkSchedule={onBatchLinkSchedule} onBatchUnlinkSchedule={onBatchUnlinkSchedule} onBatchDelete={onBatchDelete} + onSetTransfer={onSetTransfer} {...props} /> ); diff --git a/packages/desktop-client/src/hooks/useTransactionBatchActions.ts b/packages/desktop-client/src/hooks/useTransactionBatchActions.ts index ff4f366751a..2f8d108ce94 100644 --- a/packages/desktop-client/src/hooks/useTransactionBatchActions.ts +++ b/packages/desktop-client/src/hooks/useTransactionBatchActions.ts @@ -12,12 +12,14 @@ import { } from 'loot-core/shared/transactions'; import { applyChanges, type Diff } from 'loot-core/shared/util'; import { + PayeeEntity, type AccountEntity, type ScheduleEntity, type TransactionEntity, } from 'loot-core/types/models'; import { useDispatch } from '../redux'; +import { validForTransfer } from 'loot-core/client/transfer'; type BatchEditProps = { name: keyof TransactionEntity; @@ -408,11 +410,59 @@ export function useTransactionBatchActions() { } }; + const onSetTransfer = async ( + ids: string[], + payees: PayeeEntity[], + onSuccess: (ids: string[]) => void, + ) => { + const onConfirmTransfer = async (ids: string[]) => { + const { data: transactions } = await runQuery( + q('transactions') + .filter({ id: { $oneof: ids } }) + .select('*'), + ); + const [fromTrans, toTrans] = transactions; + + if (transactions.length === 2 && validForTransfer(fromTrans, toTrans)) { + const fromPayee = payees.find( + p => p.transfer_acct === fromTrans.account, + ); + const toPayee = payees.find(p => p.transfer_acct === toTrans.account); + + const changes = { + updated: [ + { + ...fromTrans, + payee: toPayee?.id, + transfer_id: toTrans.id, + }, + { + ...toTrans, + payee: fromPayee?.id, + transfer_id: fromTrans.id, + }, + ], + }; + + await send('transactions-batch-update', changes); + } + + onSuccess?.(ids); + }; + + await checkForReconciledTransactions( + ids, + 'batchEditWithReconciled', + onConfirmTransfer, + ); + }; + return { onBatchEdit, onBatchDuplicate, onBatchDelete, onBatchLinkSchedule, onBatchUnlinkSchedule, + onSetTransfer, }; } From 373c02b699935c724a06c9ff1da3baef31671c2d Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sun, 2 Mar 2025 22:00:38 +0000 Subject: [PATCH 02/12] Add "Make Transfer" functionality to mobile menu --- .../src/components/accounts/Account.tsx | 1 - .../mobile/transactions/TransactionList.tsx | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/desktop-client/src/components/accounts/Account.tsx b/packages/desktop-client/src/components/accounts/Account.tsx index 1675bf72484..413c0df4f5f 100644 --- a/packages/desktop-client/src/components/accounts/Account.tsx +++ b/packages/desktop-client/src/components/accounts/Account.tsx @@ -45,7 +45,6 @@ import { type PagedQuery, } from 'loot-core/client/query-helpers'; import { type AppDispatch } from 'loot-core/client/store'; -import { validForTransfer } from 'loot-core/client/transfer'; import { send, listen } from 'loot-core/platform/client/fetch'; import * as undo from 'loot-core/platform/client/undo'; import { type UndoState } from 'loot-core/server/undo'; diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx index 2fe59e36af3..1ffe881f43a 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx @@ -268,6 +268,7 @@ function SelectedTransactionsFloatingActionBar({ onBatchDelete, onBatchLinkSchedule, onBatchUnlinkSchedule, + onSetTransfer, } = useTransactionBatchActions(); const navigate = useNavigate(); @@ -509,6 +510,12 @@ function SelectedTransactionsFloatingActionBar({ }); }, }); + } else if (type === 'transfer') { + onSetTransfer?.([...selectedTransactions], payees, ids => + showUndoNotification({ + message: `Successfully marked ${ids.length} as transfer`, + }), + ); } setIsMoreOptionsMenuOpen(false); }} @@ -530,6 +537,10 @@ function SelectedTransactionsFloatingActionBar({ text: 'Link schedule', }, ]), + { + name: 'transfer', + text: 'Make transfer', + }, { name: 'delete', text: 'Delete', From 0d5297722d30b0196c2b4b701a3a708617df92ca Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sun, 2 Mar 2025 22:29:31 +0000 Subject: [PATCH 03/12] Don't just blindly allow setting as transfer --- .../mobile/accounts/AccountTransactions.tsx | 1 + .../mobile/budget/CategoryTransactions.tsx | 1 + .../mobile/transactions/TransactionList.tsx | 100 ++++++++++++------ .../TransactionListWithBalances.tsx | 3 + .../components/reports/reports/Calendar.tsx | 1 + 5 files changed, 75 insertions(+), 31 deletions(-) diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx index 9dfd826afb1..29840b1c2b3 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx @@ -347,6 +347,7 @@ function TransactionListWithPreviews({ onSearch={onSearch} onOpenTransaction={onOpenTransaction} onRefresh={onRefresh} + showMakeTransfer={accountId === 'uncategorized'} /> ); } diff --git a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx index 9427fd9fd8b..3ab8c04946b 100644 --- a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx @@ -128,6 +128,7 @@ export function CategoryTransactions({ onLoadMore={loadMoreTransactions} onOpenTransaction={onOpenTransaction} onRefresh={undefined} + showMakeTransfer={true} /> diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx index 1ffe881f43a..9e8c471a0eb 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx @@ -10,7 +10,11 @@ import { ListBox, Section, Header, Collection } from 'react-aria-components'; import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; -import { Menu, type MenuItemObject } from '@actual-app/components/menu'; +import { + Menu, + type MenuItem, + type MenuItemObject, +} from '@actual-app/components/menu'; import { Popover } from '@actual-app/components/popover'; import { styles } from '@actual-app/components/styles'; import { Text } from '@actual-app/components/text'; @@ -41,6 +45,7 @@ import { useScrollListener } from '../../ScrollProvider'; import { FloatingActionBar } from '../FloatingActionBar'; import { TransactionListItem } from './TransactionListItem'; +import { validForTransfer } from 'loot-core/client/transfer'; const NOTIFICATION_BOTTOM_INSET = 75; @@ -72,6 +77,7 @@ type TransactionListProps = { onOpenTransaction?: (transaction: TransactionEntity) => void; isLoadingMore: boolean; onLoadMore: () => void; + showMakeTransfer: boolean; }; export function TransactionList({ @@ -80,6 +86,7 @@ export function TransactionList({ onOpenTransaction, isLoadingMore, onLoadMore, + showMakeTransfer, }: TransactionListProps) { const { t } = useTranslation(); const sections = useMemo(() => { @@ -205,7 +212,10 @@ export function TransactionList({ )} {selectedTransactions.size > 0 && ( - + )} ); @@ -214,11 +224,13 @@ export function TransactionList({ type SelectedTransactionsFloatingActionBarProps = { transactions: readonly TransactionEntity[]; style?: CSSProperties; + showMakeTransfer: boolean; }; function SelectedTransactionsFloatingActionBar({ transactions, style = {}, + showMakeTransfer, }: SelectedTransactionsFloatingActionBarProps) { const editMenuTriggerRef = useRef(null); const [isEditMenuOpen, setIsEditMenuOpen] = useState(false); @@ -289,6 +301,58 @@ function SelectedTransactionsFloatingActionBar({ }; }, [dispatch]); + const canBeTransfer = useMemo(() => { + // only two selected + if (selectedTransactionsArray.length !== 2) { + return false; + } + const fromTrans = transactions.find( + t => t.id === selectedTransactionsArray[0], + ); + const toTrans = transactions.find( + t => t.id === selectedTransactionsArray[1], + ); + + // previously selected transactions aren't always present in current transaction list + if (!fromTrans || !toTrans) { + return false; + } + + return validForTransfer(fromTrans, toTrans); + }, [selectedTransactionsArray, transactions]); + + const moreOptionsMenuItems: MenuItem[] = [ + { + name: 'duplicate', + text: 'Duplicate', + }, + ]; + + if (allTransactionsAreLinked) { + moreOptionsMenuItems.push({ + name: 'unlink-schedule', + text: 'Unlink schedule', + }); + } else { + moreOptionsMenuItems.push({ + name: 'link-schedule', + text: 'Link schedule', + }); + } + + if (showMakeTransfer) { + moreOptionsMenuItems.push({ + name: 'transfer', + text: 'Make transfer', + disabled: !canBeTransfer, + }); + } + + moreOptionsMenuItems.push({ + name: 'delete', + text: 'Delete', + }); + return ( { - let displayValue = value; + let displayValue; switch (name) { case 'account': displayValue = @@ -511,7 +575,7 @@ function SelectedTransactionsFloatingActionBar({ }, }); } else if (type === 'transfer') { - onSetTransfer?.([...selectedTransactions], payees, ids => + onSetTransfer?.(selectedTransactionsArray, payees, ids => showUndoNotification({ message: `Successfully marked ${ids.length} as transfer`, }), @@ -519,33 +583,7 @@ function SelectedTransactionsFloatingActionBar({ } setIsMoreOptionsMenuOpen(false); }} - items={[ - { - name: 'duplicate', - text: 'Duplicate', - }, - ...(allTransactionsAreLinked - ? [ - { - name: 'unlink-schedule', - text: 'Unlink schedule', - }, - ] - : [ - { - name: 'link-schedule', - text: 'Link schedule', - }, - ]), - { - name: 'transfer', - text: 'Make transfer', - }, - { - name: 'delete', - text: 'Delete', - }, - ]} + items={moreOptionsMenuItems} /> diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx index a8da911d1d0..5752014a84b 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx @@ -91,6 +91,7 @@ type TransactionListWithBalancesProps = { onLoadMore: () => void; onOpenTransaction: (transaction: TransactionEntity) => void; onRefresh?: () => void; + showMakeTransfer: boolean; }; export function TransactionListWithBalances({ @@ -105,6 +106,7 @@ export function TransactionListWithBalances({ onLoadMore, onOpenTransaction, onRefresh, + showMakeTransfer, }: TransactionListWithBalancesProps) { const selectedInst = useSelected('transactions', [...transactions], []); @@ -148,6 +150,7 @@ export function TransactionListWithBalances({ isLoadingMore={isLoadingMore} onLoadMore={onLoadMore} onOpenTransaction={onOpenTransaction} + showMakeTransfer={showMakeTransfer} /> diff --git a/packages/desktop-client/src/components/reports/reports/Calendar.tsx b/packages/desktop-client/src/components/reports/reports/Calendar.tsx index a5bcc5d2e2a..006553b1641 100644 --- a/packages/desktop-client/src/components/reports/reports/Calendar.tsx +++ b/packages/desktop-client/src/components/reports/reports/Calendar.tsx @@ -695,6 +695,7 @@ function CalendarInner({ widget, parameters }: CalendarInnerProps) { transactions={allTransactions} onOpenTransaction={onOpenTransaction} isLoadingMore={false} + showMakeTransfer={false} /> From c7d68b6855d85b03e0f0a0296d678742f64f66cd Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sun, 2 Mar 2025 22:59:20 +0000 Subject: [PATCH 04/12] Update disabled colour and missing translations --- .../mobile/transactions/TransactionList.tsx | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx index 9e8c471a0eb..99524e1fb0d 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx @@ -19,8 +19,10 @@ import { Popover } from '@actual-app/components/popover'; import { styles } from '@actual-app/components/styles'; import { Text } from '@actual-app/components/text'; import { View } from '@actual-app/components/view'; +import { t } from 'i18next'; import { setNotificationInset } from 'loot-core/client/actions'; +import { validForTransfer } from 'loot-core/client/transfer'; import * as monthUtils from 'loot-core/shared/months'; import { isPreviewId } from 'loot-core/shared/transactions'; import { groupById, integerToCurrency } from 'loot-core/shared/util'; @@ -45,7 +47,6 @@ import { useScrollListener } from '../../ScrollProvider'; import { FloatingActionBar } from '../FloatingActionBar'; import { TransactionListItem } from './TransactionListItem'; -import { validForTransfer } from 'loot-core/client/transfer'; const NOTIFICATION_BOTTOM_INSET = 75; @@ -240,6 +241,7 @@ function SelectedTransactionsFloatingActionBar({ (item: MenuItemObject) => ({ ...styles.mobileMenuItem, color: theme.mobileHeaderText, + ...(item.disabled === true && { color: theme.buttonBareDisabledText }), ...(item.name === 'delete' && { color: theme.errorTextMenu }), }), [], @@ -324,33 +326,33 @@ function SelectedTransactionsFloatingActionBar({ const moreOptionsMenuItems: MenuItem[] = [ { name: 'duplicate', - text: 'Duplicate', + text: t('Duplicate'), }, ]; if (allTransactionsAreLinked) { moreOptionsMenuItems.push({ name: 'unlink-schedule', - text: 'Unlink schedule', + text: t('Unlink schedule'), }); } else { moreOptionsMenuItems.push({ name: 'link-schedule', - text: 'Link schedule', + text: t('Link schedule'), }); } if (showMakeTransfer) { moreOptionsMenuItems.push({ name: 'transfer', - text: 'Make transfer', + text: t('Make transfer'), disabled: !canBeTransfer, }); } moreOptionsMenuItems.push({ name: 'delete', - text: 'Delete', + text: t('Delete'), }); return ( @@ -399,7 +401,7 @@ function SelectedTransactionsFloatingActionBar({ Date: Tue, 4 Mar 2025 13:11:04 +0000 Subject: [PATCH 12/12] Use Trans component instead of t function Co-authored-by: Matt Fiddaman --- .../src/components/mobile/transactions/TransactionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx index 49683e979f4..28ee59a84b8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx @@ -401,7 +401,7 @@ function SelectedTransactionsFloatingActionBar({ }} {...buttonProps} > - {t('Edit')} + Edit