From ee21155d1afc36aa3ec903f8b9a404902fcdc3b4 Mon Sep 17 00:00:00 2001 From: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com> Date: Sat, 17 Aug 2024 16:30:27 -0400 Subject: [PATCH] Display splits in previews (#2923) * Display splits in mobile previews * Display splits in desktop previews * UI fix: hide checkboxes on preview child transactions * UI fix: show notes on preview transactions * Add release notes * Update vrt for mobile padding fix * Fix status display * Collapse split previews by default * PR feedback: fix issues with split payee * Update new VRT with spacing from this PR --- .../src/components/accounts/Account.jsx | 14 ++- .../mobile/transactions/Transaction.jsx | 3 +- .../mobile/transactions/TransactionEdit.jsx | 4 +- .../mobile/transactions/TransactionList.jsx | 4 + .../transactions/TransactionsTable.jsx | 116 ++++++++++-------- .../src/hooks/usePreviewTransactions.ts | 72 ++++++++--- .../src/hooks/useSplitsExpanded.jsx | 11 ++ upcoming-release-notes/2923.md | 6 + 8 files changed, 148 insertions(+), 82 deletions(-) create mode 100644 upcoming-release-notes/2923.md diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx index 3153fd749..28f169dd7 100644 --- a/packages/desktop-client/src/components/accounts/Account.jsx +++ b/packages/desktop-client/src/components/accounts/Account.jsx @@ -101,12 +101,15 @@ function AllTransactions({ showBalances, filtered, children, + collapseTransactions, }) { const accountId = account.id; - const prependTransactions = usePreviewTransactions().map(trans => ({ - ...trans, - _inverse: accountId ? accountId !== trans.account : false, - })); + const prependTransactions = usePreviewTransactions(collapseTransactions).map( + trans => ({ + ...trans, + _inverse: accountId ? accountId !== trans.account : false, + }), + ); transactions ??= []; @@ -1453,6 +1456,9 @@ class AccountInternal extends PureComponent { balances={balances} showBalances={showBalances} filtered={transactionsFiltered} + collapseTransactions={ids => + this.props.splitsExpandedDispatch({ type: 'close-splits', ids }) + } > {(allTransactions, allBalances) => ( <SelectedProviderWithItems diff --git a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx index 285bbc4e7..85c8100e9 100644 --- a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx @@ -70,7 +70,6 @@ export const Transaction = memo(function Transaction({ cleared, is_parent: isParent, is_child: isChild, - notes, schedule, } = transaction; @@ -183,7 +182,7 @@ export const Transaction = memo(function Transaction({ </TextOneLine> </View> {isPreview ? ( - <Status status={notes} /> + <Status status={categoryId} isSplit={isParent || isChild} /> ) : ( <View style={{ diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 3cb7db764..b21dfbd3b 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -132,7 +132,7 @@ export function lookupName(items, id) { return items.find(item => item.id === id)?.name; } -export function Status({ status }) { +export function Status({ status, isSplit }) { let color; switch (status) { @@ -157,7 +157,7 @@ export function Status({ status }) { textAlign: 'left', }} > - {titleFirst(status)} + {titleFirst(status) + (isSplit ? ' (Split)' : '')} </Text> ); } diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx index 34a310e53..48c1dd97b 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx @@ -138,6 +138,10 @@ export function TransactionList({ key={section.id} > {section.data.map((transaction, index, transactions) => { + if (isPreviewId(transaction.id) && transaction.is_child) { + return null; + } + return ( <Item key={transaction.id} diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index c421da17e..20ba4a194 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -575,9 +575,11 @@ function PayeeCell({ alignSelf: 'flex-start', borderRadius: 4, border: '1px solid transparent', // so it doesn't shift on hover - ':hover': { - border: '1px solid ' + theme.buttonNormalBorder, - }, + ':hover': isPreview + ? {} + : { + border: '1px solid ' + theme.buttonNormalBorder, + }, }} disabled={isPreview} onSelect={() => @@ -601,6 +603,12 @@ function PayeeCell({ color: theme.pageTextSubdued, }} > + <PayeeIcons + transaction={transaction} + transferAccount={transferAccount} + onNavigateToTransferAccount={onNavigateToTransferAccount} + onNavigateToSchedule={onNavigateToSchedule} + /> <SvgSplit style={{ color: 'inherit', @@ -1110,6 +1118,8 @@ const Transaction = memo(function Transaction({ ) : ( <Cell width={20} /> ) + ) : isPreview && isChild ? ( + <Cell width={20} /> ) : ( <SelectCell /* Checkmark field for non-child transaction */ @@ -1246,45 +1256,52 @@ const Transaction = memo(function Transaction({ /> ))()} - {isPreview ? ( - /* Notes field for all transactions */ - <Cell name="notes" width="flex" /> - ) : ( - <InputCell + <InputCell + width="flex" + name="notes" + textAlign="flex" + exposed={focusedField === 'notes'} + focused={focusedField === 'notes'} + value={notes || ''} + valueStyle={valueStyle} + formatter={value => notesTagFormatter(value, onNotesTagClick)} + onExpose={name => !isPreview && onEdit(id, name)} + inputProps={{ + value: notes || '', + onUpdate: onUpdate.bind(null, 'notes'), + }} + /> + + {(isPreview && !isChild) || isParent ? ( + <Cell + /* Category field (Split button) for parent transactions */ + name="category" width="flex" - name="notes" - textAlign="flex" - exposed={focusedField === 'notes'} - focused={focusedField === 'notes'} - value={notes || ''} - valueStyle={valueStyle} - formatter={value => notesTagFormatter(value, onNotesTagClick)} - onExpose={name => !isPreview && onEdit(id, name)} - inputProps={{ - value: notes || '', - onUpdate: onUpdate.bind(null, 'notes'), + focused={focusedField === 'category'} + style={{ + padding: 0, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + height: '100%', }} - /> - )} - - {isPreview ? ( - // Category field for preview transactions - <Cell width="flex" style={{ alignItems: 'flex-start' }} exposed={true}> - {() => ( + plain + > + {isPreview && ( <View style={{ color: - notes === 'missed' + categoryId === 'missed' ? theme.errorText - : notes === 'due' + : categoryId === 'due' ? theme.warningText : selected ? theme.formLabelText : theme.upcomingText, backgroundColor: - notes === 'missed' + categoryId === 'missed' ? theme.errorBackground - : notes === 'due' + : categoryId === 'due' ? theme.warningBackground : selected ? theme.formLabelBackground @@ -1294,23 +1311,12 @@ const Transaction = memo(function Transaction({ borderRadius: 4, }} > - {titleFirst(notes)} + {titleFirst(categoryId)} </View> )} - </Cell> - ) : isParent ? ( - <Cell - /* Category field (Split button) for parent transactions */ - name="category" - width="flex" - focused={focusedField === 'category'} - style={{ padding: 0 }} - plain - > <CellButton bare style={{ - alignSelf: 'flex-start', borderRadius: 4, border: '1px solid transparent', // so it doesn't shift on hover ':hover': { @@ -1318,7 +1324,7 @@ const Transaction = memo(function Transaction({ }, }} disabled={isTemporaryId(transaction.id)} - onEdit={() => onEdit(id, 'category')} + onEdit={() => !isPreview && onEdit(id, 'category')} onSelect={() => onToggleSplit(id)} > <View @@ -1343,15 +1349,17 @@ const Transaction = memo(function Transaction({ }} /> )} - <Text - style={{ - fontStyle: 'italic', - fontWeight: 300, - userSelect: 'none', - }} - > - Split - </Text> + {!isPreview && ( + <Text + style={{ + fontStyle: 'italic', + fontWeight: 300, + userSelect: 'none', + }} + > + Split + </Text> + )} </View> </CellButton> </Cell> @@ -1402,7 +1410,7 @@ const Transaction = memo(function Transaction({ : '' } exposed={focusedField === 'category'} - onExpose={name => onEdit(id, name)} + onExpose={name => !isPreview && onEdit(id, name)} valueStyle={ !categoryId ? { @@ -1532,7 +1540,7 @@ const Transaction = memo(function Transaction({ isPreview={isPreview} status={ isPreview - ? notes + ? categoryId : reconciled ? 'reconciled' : cleared diff --git a/packages/desktop-client/src/hooks/usePreviewTransactions.ts b/packages/desktop-client/src/hooks/usePreviewTransactions.ts index 4f4f57b3e..786e369c3 100644 --- a/packages/desktop-client/src/hooks/usePreviewTransactions.ts +++ b/packages/desktop-client/src/hooks/usePreviewTransactions.ts @@ -1,34 +1,66 @@ -import { useMemo } from 'react'; +import { useState } from 'react'; import { type ScheduleStatuses, useCachedSchedules, } from 'loot-core/client/data-hooks/schedules'; +import { send } from 'loot-core/platform/client/fetch'; +import { ungroupTransactions } from 'loot-core/shared/transactions'; import { type ScheduleEntity } from 'loot-core/types/models'; -export function usePreviewTransactions() { +import { type TransactionEntity } from '../../../loot-core/src/types/models/transaction.d'; + +export function usePreviewTransactions( + collapseTransactions: (ids: string[]) => void, +) { const scheduleData = useCachedSchedules(); + const [previousScheduleData, setPreviousScheduleData] = + useState<ReturnType<typeof useCachedSchedules>>(scheduleData); + const [previewTransactions, setPreviewTransactions] = useState< + TransactionEntity[] + >([]); + + if (scheduleData !== previousScheduleData) { + setPreviousScheduleData(scheduleData); + + if (scheduleData) { + // Kick off an async rules application + const schedules = + scheduleData.schedules.filter(s => + isForPreview(s, scheduleData.statuses), + ) || []; - return useMemo(() => { - if (!scheduleData) { - return []; + const baseTrans = schedules.map(schedule => ({ + id: 'preview/' + schedule.id, + payee: schedule._payee, + account: schedule._account, + amount: schedule._amount, + date: schedule.next_date, + schedule: schedule.id, + })); + + Promise.all( + baseTrans.map(transaction => send('rules-run', { transaction })), + ).then(newTrans => { + const withDefaults = newTrans.map(t => ({ + ...t, + category: scheduleData.statuses.get(t.schedule), + schedule: t.schedule, + subtransactions: t.subtransactions?.map((st: TransactionEntity) => ({ + ...st, + id: 'preview/' + st.id, + schedule: t.schedule, + })), + })); + setPreviewTransactions(ungroupTransactions(withDefaults)); + collapseTransactions(withDefaults.map(t => t.id)); + }); } - const schedules = - scheduleData.schedules.filter(s => - isForPreview(s, scheduleData.statuses), - ) || []; - - return schedules.map(schedule => ({ - id: 'preview/' + schedule.id, - payee: schedule._payee, - account: schedule._account, - amount: schedule._amount, - date: schedule.next_date, - notes: scheduleData.statuses.get(schedule.id), - schedule: schedule.id, - })); - }, [scheduleData]); + return previewTransactions; + } + + return previewTransactions; } function isForPreview(schedule: ScheduleEntity, statuses: ScheduleStatuses) { diff --git a/packages/desktop-client/src/hooks/useSplitsExpanded.jsx b/packages/desktop-client/src/hooks/useSplitsExpanded.jsx index 15e1a3cde..8b8bb4ed2 100644 --- a/packages/desktop-client/src/hooks/useSplitsExpanded.jsx +++ b/packages/desktop-client/src/hooks/useSplitsExpanded.jsx @@ -51,6 +51,17 @@ export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { } return { ...state, ids }; } + case 'close-splits': { + const ids = new Set([...state.ids]); + action.ids.forEach(id => { + if (state.mode === 'collapse') { + ids.add(id); + } else { + ids.delete(id); + } + }); + return { ...state, ids }; + } case 'set-mode': { return { ...state, diff --git a/upcoming-release-notes/2923.md b/upcoming-release-notes/2923.md new file mode 100644 index 000000000..3fffdc22d --- /dev/null +++ b/upcoming-release-notes/2923.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [jfdoming] +--- + +Show split transactions in schedule previews. -- GitLab