diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png index 75ecd7a13fb384b69ba28fd7d7d9005bb33dede2..0520ca7e91b5d90eb5b823ac02c0c6a30c26c631 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png index 7ac8627924a87e0f4feb8dd51f08a036e3af33eb..89beb5a8f561a406377291e822922b891f1bc593 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png index ce38feb390e46032673d4d0513ee7bfdca025c65..c761f2225e748e7d1651eca8ffd21a49fdbfad69 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png index be2af3a31ee60ed3fe18a39c2429d2426becdb91..3cb6f9c634549ef7b3bdb91030a6eb669d885aef 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png index 4722fad3d167f4015a48706b75a63062ddeb0585..e2988c222b7199a9c462dd7405d531760834030e 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png index 0f2889138a096e405286cd418bc585aceabfa1cb..717ce7b1abd35c8ecce65631d76106fc959eb0cf 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 643ea94784facd7d919aae559fc09e58f0c6107b..d42fe09eb87a8a970f5d2cd6abe1321dc83212c0 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -21,6 +21,7 @@ import { CategoryGroupMenuModal } from './modals/CategoryGroupMenuModal'; import { CategoryMenuModal } from './modals/CategoryMenuModal'; import { CloseAccountModal } from './modals/CloseAccountModal'; import { ConfirmCategoryDelete } from './modals/ConfirmCategoryDelete'; +import { ConfirmTransactionDelete } from './modals/ConfirmTransactionDelete'; import { ConfirmTransactionEdit } from './modals/ConfirmTransactionEdit'; import { ConfirmUnlinkAccount } from './modals/ConfirmUnlinkAccount'; import { CoverModal } from './modals/CoverModal'; @@ -177,6 +178,15 @@ export function Modals() { /> ); + case 'confirm-transaction-delete': + return ( + <ConfirmTransactionDelete + key={name} + modalProps={modalProps} + onConfirm={options.onConfirm} + /> + ); + case 'load-backup': return ( <LoadBackup diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx index 6acb901fc2aa431723b83607c26eece63121635a..bb2a45be2e0814f9a885039ef53a4aba5155fdac 100644 --- a/packages/desktop-client/src/components/Notifications.tsx +++ b/packages/desktop-client/src/components/Notifications.tsx @@ -95,6 +95,7 @@ function Notification({ sticky, internal, button, + timeout, } = notification; const [loading, setLoading] = useState(false); @@ -106,7 +107,7 @@ function Notification({ } if (!sticky) { - setTimeout(onRemove, 6500); + setTimeout(onRemove, timeout || 6500); } }, []); diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 821e54de6d6de2616da4cdd75af75586286aea42..033e0045d910b6f00a4fb053b4c41c22512e8d57 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -50,7 +50,7 @@ import { useSingleActiveEditForm, } from '../../../hooks/useSingleActiveEditForm'; import { SvgSplit } from '../../../icons/v0'; -import { SvgAdd, SvgTrash } from '../../../icons/v1'; +import { SvgAdd, SvgPiggyBank, SvgTrash } from '../../../icons/v1'; import { SvgPencilWriteAlternate } from '../../../icons/v2'; import { styles, theme } from '../../../style'; import { Button } from '../../common/Button'; @@ -170,6 +170,7 @@ function Footer({ onAddSplit, onEmptySplitFound, editingField, + onEditField, }) { const [transaction, ...childTransactions] = transactions; const onClickRemainingSplit = () => { @@ -200,7 +201,7 @@ function Footer({ {transaction.error?.type === 'SplitTransactionError' ? ( <Button type="primary" - style={{ height: 40 }} + style={{ height: styles.mobileMinHeight }} disabled={editingField} onClick={onClickRemainingSplit} onPointerDown={e => e.preventDefault()} @@ -220,10 +221,28 @@ function Footer({ )} </Text> </Button> + ) : !transaction.account ? ( + <Button + type="primary" + style={{ height: styles.mobileMinHeight }} + disabled={editingField} + onClick={() => onEditField(transaction.id, 'account')} + onPointerDown={e => e.preventDefault()} + > + <SvgPiggyBank width={17} height={17} /> + <Text + style={{ + ...styles.text, + marginLeft: 6, + }} + > + Select account + </Text> + </Button> ) : adding ? ( <Button type="primary" - style={{ height: 40 }} + style={{ height: styles.mobileMinHeight }} disabled={editingField} onClick={onAdd} onPointerDown={e => e.preventDefault()} @@ -241,7 +260,7 @@ function Footer({ ) : ( <Button type="primary" - style={{ height: 40 }} + style={{ height: styles.mobileMinHeight }} disabled={editingField} onClick={onSave} onPointerDown={e => e.preventDefault()} @@ -270,8 +289,8 @@ const ChildTransactionEdit = forwardRef( getPrettyPayee, isOffBudget, isBudgetTransfer, - onClick, - onEdit, + onEditField, + onUpdate, onDelete, }, ref, @@ -302,7 +321,7 @@ const ChildTransactionEdit = forwardRef( editingField !== getFieldName(transaction.id, 'payee') } value={getPrettyPayee(transaction)} - onClick={() => onClick(transaction.id, 'payee')} + onClick={() => onEditField(transaction.id, 'payee')} data-testid={`payee-field-${transaction.id}`} /> </View> @@ -328,7 +347,7 @@ const ChildTransactionEdit = forwardRef( onUpdate={value => { const amount = integerToAmount(value); if (transaction.amount !== amount) { - onEdit(transaction, 'amount', amount); + onUpdate(transaction, 'amount', amount); } else { onClearActiveEdit(); } @@ -355,7 +374,7 @@ const ChildTransactionEdit = forwardRef( isOffBudget || isBudgetTransfer(transaction) } - onClick={() => onClick(transaction.id, 'category')} + onClick={() => onEditField(transaction.id, 'category')} data-testid={`category-field-${transaction.id}`} /> </View> @@ -371,7 +390,7 @@ const ChildTransactionEdit = forwardRef( onFocus={() => onRequestActiveEdit(getFieldName(transaction.id, 'notes')) } - onUpdate={value => onEdit(transaction, 'notes', value)} + onUpdate={value => onUpdate(transaction, 'notes', value)} /> </View> @@ -489,7 +508,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ const onTotalAmountUpdate = value => { if (transaction.amount !== value) { - onEdit(transaction, 'amount', value.toString()); + onUpdate(transaction, 'amount', value.toString()); } else { onClearActiveEdit(); } @@ -502,12 +521,6 @@ const TransactionEditInner = memo(function TransactionEditInner({ const { account: accountId } = unserializedTransaction; const account = accountsById[accountId]; - if (unserializedTransactions.find(t => t.account == null)) { - // Ignore transactions if any of them don't have an account - // TODO: Should we display validation error? - return; - } - let transactionsToSave = unserializedTransactions; if (adding) { transactionsToSave = realizeTempTransactions(unserializedTransactions); @@ -537,13 +550,13 @@ const TransactionEditInner = memo(function TransactionEditInner({ onSave(); }; - const onEdit = async (serializedTransaction, name, value) => { + const onUpdate = async (serializedTransaction, name, value) => { const newTransaction = { ...serializedTransaction, [name]: value }; - await props.onEdit(newTransaction); + await props.onUpdate(newTransaction); onClearActiveEdit(); }; - const onClick = (transactionId, name) => { + const onEditField = (transactionId, name) => { onRequestActiveEdit?.(getFieldName(transaction.id, name), () => { const transactionToEdit = transactions.find(t => t.id === transactionId); const unserializedTransaction = unserializedTransactions.find( @@ -556,7 +569,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ categoryGroups, month: monthUtils.monthFromDate(unserializedTransaction.date), onSelect: categoryId => { - onEdit(transactionToEdit, name, categoryId); + onUpdate(transactionToEdit, name, categoryId); }, onClose: () => { onClearActiveEdit(); @@ -568,7 +581,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ dispatch( pushModal('account-autocomplete', { onSelect: accountId => { - onEdit(transactionToEdit, name, accountId); + onUpdate(transactionToEdit, name, accountId); }, onClose: () => { onClearActiveEdit(); @@ -580,7 +593,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ dispatch( pushModal('payee-autocomplete', { onSelect: payeeId => { - onEdit(transactionToEdit, name, payeeId); + onUpdate(transactionToEdit, name, payeeId); }, onClose: () => { onClearActiveEdit(); @@ -594,7 +607,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ name, month: monthUtils.monthFromDate(unserializedTransaction.date), onSubmit: (name, value) => { - onEdit(transactionToEdit, name, value); + onUpdate(transactionToEdit, name, value); }, onClose: () => { onClearActiveEdit(); @@ -610,20 +623,26 @@ const TransactionEditInner = memo(function TransactionEditInner({ const [unserializedTransaction] = unserializedTransactions; const onConfirmDelete = () => { - props.onDelete(id); - - if (unserializedTransaction.id !== id) { - // Only a child transaction was deleted. - onClearActiveEdit(); - return; - } + dispatch( + pushModal('confirm-transaction-delete', { + onConfirm: () => { + props.onDelete(id); + + if (unserializedTransaction.id !== id) { + // Only a child transaction was deleted. + onClearActiveEdit(); + return; + } - const { account: accountId } = unserializedTransaction; - if (accountId) { - navigate(`/accounts/${accountId}`, { replace: true }); - } else { - navigate(-1); - } + const { account: accountId } = unserializedTransaction; + if (accountId) { + navigate(`/accounts/${accountId}`, { replace: true }); + } else { + navigate(-1); + } + }, + }), + ); }; if (unserializedTransaction.reconciled) { @@ -710,6 +729,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ onAddSplit={onAddSplit} onEmptySplitFound={onEmptySplitFound} editingField={editingField} + onEditField={onEditField} /> } padding={0} @@ -746,7 +766,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ editingField !== getFieldName(transaction.id, 'payee') } value={getPrettyPayee(transaction)} - onClick={() => onClick(transaction.id, 'payee')} + onClick={() => onEditField(transaction.id, 'payee')} data-testid="payee-field" /> </View> @@ -769,7 +789,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ isOffBudget || isBudgetTransfer(transaction) } - onClick={() => onClick(transaction.id, 'category')} + onClick={() => onEditField(transaction.id, 'category')} data-testid="category-field" /> </View> @@ -790,8 +810,8 @@ const TransactionEditInner = memo(function TransactionEditInner({ getCategory={getCategory} getPrettyPayee={getPrettyPayee} isBudgetTransfer={isBudgetTransfer} - onEdit={onEdit} - onClick={onClick} + onUpdate={onUpdate} + onEditField={onEditField} onDelete={onDelete} /> ))} @@ -838,7 +858,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ editingField !== getFieldName(transaction.id, 'account')) } value={account?.name} - onClick={() => onClick(transaction.id, 'account')} + onClick={() => onEditField(transaction.id, 'account')} data-testid="account-field" /> </View> @@ -859,7 +879,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ onRequestActiveEdit(getFieldName(transaction.id, 'date')) } onUpdate={value => - onEdit( + onUpdate( transaction, 'date', formatDate(parseISO(value), dateFormat), @@ -886,7 +906,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ <BooleanField disabled={editingField} checked={transaction.cleared} - onUpdate={checked => onEdit(transaction, 'cleared', checked)} + onUpdate={checked => onUpdate(transaction, 'cleared', checked)} style={{ margin: 'auto', width: 22, @@ -908,7 +928,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ onFocus={() => { onRequestActiveEdit(getFieldName(transaction.id, 'notes')); }} - onUpdate={value => onEdit(transaction, 'notes', value)} + onUpdate={value => onUpdate(transaction, 'notes', value)} /> </View> @@ -1030,7 +1050,7 @@ function TransactionEditUnconnected({ return null; } - const onEdit = async serializedTransaction => { + const onUpdate = async serializedTransaction => { const transaction = deserializeTransaction( serializedTransaction, null, @@ -1135,7 +1155,7 @@ function TransactionEditUnconnected({ payees={payees} navigate={navigate} dateFormat={dateFormat} - onEdit={onEdit} + onUpdate={onUpdate} onSave={onSave} onDelete={onDelete} onSplit={onSplit} diff --git a/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6d465b8e0e288de98be1bb86c5f61bcdf67f1523 --- /dev/null +++ b/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import { useResponsive } from '../../ResponsiveProvider'; +import { styles } from '../../style'; +import { Button } from '../common/Button'; +import { Modal } from '../common/Modal'; +import { Paragraph } from '../common/Paragraph'; +import { View } from '../common/View'; +import { type CommonModalProps } from '../Modals'; + +type ConfirmTransactionDeleteProps = { + modalProps: CommonModalProps; + onConfirm: () => void; +}; + +export function ConfirmTransactionDelete({ + modalProps, + onConfirm, +}: ConfirmTransactionDeleteProps) { + const { isNarrowWidth } = useResponsive(); + const narrowButtonStyle = isNarrowWidth + ? { + height: styles.mobileMinHeight, + } + : {}; + + return ( + <Modal title="Confirm Delete" {...modalProps}> + <View style={{ lineHeight: 1.5 }}> + <Paragraph>Are you sure you want to delete the transaction?</Paragraph> + <View + style={{ + flexDirection: 'row', + justifyContent: 'flex-end', + }} + > + <Button + style={{ + marginRight: 10, + ...narrowButtonStyle, + }} + onClick={modalProps.onClose} + > + Cancel + </Button> + <Button + type="primary" + style={narrowButtonStyle} + onClick={() => { + onConfirm(); + modalProps.onClose(); + }} + > + Delete + </Button> + </View> + </View> + </Modal> + ); +} diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index 4f0b190dcabdae9de61f040bb87d9304eb490ba7..4e607993c8c6a0ef31c1bff2170018248ff26120 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -247,6 +247,13 @@ type FinanceModals = { onEditNotes: (month: string) => void; }; 'budget-list'; + 'confirm-transaction-edit': { + onConfirm: () => void; + confirmReason: string; + }; + 'confirm-transaction-delete': { + onConfirm: () => void; + }; }; export type PushModalAction = { diff --git a/packages/loot-core/src/client/state-types/notifications.d.ts b/packages/loot-core/src/client/state-types/notifications.d.ts index 236f714059e26bb2e6f625a0dcff9388766e672b..551916e9f4249c852107290c1913899383e53aec 100644 --- a/packages/loot-core/src/client/state-types/notifications.d.ts +++ b/packages/loot-core/src/client/state-types/notifications.d.ts @@ -15,7 +15,7 @@ export type Notification = { }; messageActions?: Record<string, () => void>; onClose?: () => void; - internal?: unknown; + internal?: string; }; type NotificationWithId = Notification & { id: string }; diff --git a/upcoming-release-notes/2753.md b/upcoming-release-notes/2753.md new file mode 100644 index 0000000000000000000000000000000000000000..9af1b9480e93beb4747893ced198ced142760b4a --- /dev/null +++ b/upcoming-release-notes/2753.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Require account in mobile transaction entry + confirm transaction delete.