diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png index 2e18a7006d6..de7d6de98a8 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png index 4f3a11ef166..48aa0d6f4c9 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-3-chromium-linux.png index 83b958b5e86..d730ad8bdb9 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-from-accounts-id-page-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-1-chromium-linux.png index 62d41df64aa..53683550415 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-2-chromium-linux.png index 89beb5a8f56..e1a77f25843 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-3-chromium-linux.png index c761f2225e7..b713d51076e 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-4-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-4-chromium-linux.png index ad745da2d72..d3832395c55 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-4-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-5-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-5-chromium-linux.png index 63457edde02..44b75e38918 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-5-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-6-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-6-chromium-linux.png index 471578b58c4..fb8b0175644 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-6-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.js-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-6-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx index e83bf2b6ad3..d6fe0d8315b 100644 --- a/packages/desktop-client/src/components/common/Menu.tsx +++ b/packages/desktop-client/src/components/common/Menu.tsx @@ -207,15 +207,20 @@ export function Menu({ ) : ( - <> + - !item.disabled && @@ -224,7 +229,7 @@ export function Menu({ onMenuSelect?.(item.name) } /> - + )} {item.key && } diff --git a/packages/desktop-client/src/components/common/Toggle.tsx b/packages/desktop-client/src/components/common/Toggle.tsx index 1bf72175e8d..594c8d9086b 100644 --- a/packages/desktop-client/src/components/common/Toggle.tsx +++ b/packages/desktop-client/src/components/common/Toggle.tsx @@ -2,29 +2,34 @@ import React from 'react'; import { css } from 'glamor'; -import { theme, type CSSProperties } from '../../style'; +import { type CSSProperties, theme } from '../../style'; + +import { View } from './View'; type ToggleProps = { id: string; - checked: boolean; - onToggle?: () => void; - onColor?: string; + isOn: boolean; + isDisabled?: boolean; + onToggle?: (isOn: boolean) => void; + className?: string; style?: CSSProperties; }; export const Toggle = ({ id, - checked, + isOn, + isDisabled = false, onToggle, - onColor, + className, style, }: ToggleProps) => { return ( -
+ onToggle?.(e.target.checked)} className={`${css({ height: 0, width: 0, @@ -33,43 +38,53 @@ export const Toggle = ({ type="checkbox" /> -
+
); }; diff --git a/packages/desktop-client/src/components/mobile/MobileForms.tsx b/packages/desktop-client/src/components/mobile/MobileForms.tsx index b2ed06cf730..7307ebae558 100644 --- a/packages/desktop-client/src/components/mobile/MobileForms.tsx +++ b/packages/desktop-client/src/components/mobile/MobileForms.tsx @@ -1,4 +1,5 @@ import React, { + type ComponentPropsWithoutRef, type ComponentPropsWithRef, forwardRef, type ReactNode, @@ -10,6 +11,7 @@ import { theme, styles, type CSSProperties } from '../../style'; import { Button } from '../common/Button'; import { Input } from '../common/Input'; import { Text } from '../common/Text'; +import { Toggle } from '../common/Toggle'; import { View } from '../common/View'; type FieldLabelProps = { @@ -131,55 +133,38 @@ export const TapField = forwardRef( TapField.displayName = 'TapField'; -type BooleanFieldProps = { - checked: boolean; - disabled?: boolean; - onUpdate?: (checked: boolean) => void; - style?: CSSProperties; -}; +type ToggleFieldProps = ComponentPropsWithoutRef; -export function BooleanField({ - checked, - onUpdate, +export function ToggleField({ + id, + isOn, + onToggle, style, - disabled = false, -}: BooleanFieldProps) { + className, + isDisabled = false, +}: ToggleFieldProps) { return ( - onUpdate?.(e.target.checked)} - className={`${css([ - { - marginInline: styles.mobileEditingPadding, - flexShrink: 0, - appearance: 'none', - outline: 0, - border: '1px solid ' + theme.formInputBorder, - borderRadius: 4, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: theme.checkboxText, - backgroundColor: theme.tableBackground, - ':checked': { - border: '1px solid ' + theme.checkboxBorderSelected, - backgroundColor: theme.checkboxBackgroundSelected, - '::after': { - display: 'block', - background: - theme.checkboxBackgroundSelected + - // eslint-disable-next-line rulesdir/typography - ' url(\'data:image/svg+xml; utf8,\') 15px 15px', - width: 15, - height: 15, - content: ' ', + ); } diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 0e1c97d04c5..e151d15b0f2 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -57,11 +57,12 @@ import { SvgPencilWriteAlternate } from '../../../icons/v2'; import { styles, theme } from '../../../style'; import { Button } from '../../common/Button'; import { Text } from '../../common/Text'; +import { Toggle } from '../../common/Toggle'; import { View } from '../../common/View'; import { MobilePageHeader, Page } from '../../Page'; import { AmountInput } from '../../util/AmountInput'; import { MobileBackButton } from '../MobileBackButton'; -import { FieldLabel, TapField, InputField, BooleanField } from '../MobileForms'; +import { FieldLabel, TapField, InputField, ToggleField } from '../MobileForms'; import { FocusableAmountInput } from './FocusableAmountInput'; @@ -959,32 +960,17 @@ const TransactionEditInner = memo(function TransactionEditInner({ /> {transaction.reconciled ? ( - + - + ) : ( - + - - onUpdateInner(transaction, 'cleared', checked) - } - style={{ - margin: 'auto', - width: 22, - height: 22, - }} + onUpdateInner(transaction, 'cleared', on)} /> )} @@ -1075,6 +1061,8 @@ function TransactionEditUnconnected({ const deleted = useRef(false); useEffect(() => { + let unmounted = false; + async function fetchTransaction() { // Query for the transaction based on the ID with grouped splits. // @@ -1091,15 +1079,22 @@ function TransactionEditUnconnected({ .select('*') .options({ splits: 'grouped' }), ); - const fetchedTransactions = ungroupTransactions(data); - setTransactions(fetchedTransactions); - setFetchedTransactions(fetchedTransactions); + + if (!unmounted) { + const fetchedTransactions = ungroupTransactions(data); + setTransactions(fetchedTransactions); + setFetchedTransactions(fetchedTransactions); + } } if (transactionId !== 'new') { fetchTransaction(); } else { adding.current = true; } + + return () => { + unmounted = true; + }; }, [transactionId]); useEffect(() => { @@ -1114,131 +1109,146 @@ function TransactionEditUnconnected({ } }, [locationState?.accountId, locationState?.categoryId, lastTransaction]); - if ( - categories.length === 0 || - accounts.length === 0 || - transactions.length === 0 - ) { - return null; - } - - const onUpdate = async (serializedTransaction, updatedField) => { - const transaction = deserializeTransaction( - serializedTransaction, - null, - dateFormat, - ); + const onUpdate = useCallback( + async (serializedTransaction, updatedField) => { + const transaction = deserializeTransaction( + serializedTransaction, + null, + dateFormat, + ); - // Run the rules to auto-fill in any data. Right now we only do - // this on new transactions because that's how desktop works. - const newTransaction = { ...transaction }; - if (isTemporary(newTransaction)) { - const afterRules = await send('rules-run', { - transaction: newTransaction, - }); - const diff = getChangedValues(newTransaction, afterRules); + // Run the rules to auto-fill in any data. Right now we only do + // this on new transactions because that's how desktop works. + const newTransaction = { ...transaction }; + if (isTemporary(newTransaction)) { + const afterRules = await send('rules-run', { + transaction: newTransaction, + }); + const diff = getChangedValues(newTransaction, afterRules); + + if (diff) { + Object.keys(diff).forEach(field => { + if ( + newTransaction[field] == null || + newTransaction[field] === '' || + newTransaction[field] === 0 || + newTransaction[field] === false + ) { + newTransaction[field] = diff[field]; + } + }); - if (diff) { - Object.keys(diff).forEach(field => { + // When a rule updates a parent transaction, overwrite all changes to the current field in subtransactions. if ( - newTransaction[field] == null || - newTransaction[field] === '' || - newTransaction[field] === 0 || - newTransaction[field] === false + newTransaction.is_parent && + diff.subtransactions !== undefined && + updatedField !== null ) { - newTransaction[field] = diff[field]; - } - }); - - // When a rule updates a parent transaction, overwrite all changes to the current field in subtransactions. - if ( - newTransaction.is_parent && - diff.subtransactions !== undefined && - updatedField !== null - ) { - newTransaction.subtransactions = diff.subtransactions.map( - (st, idx) => ({ - ...(newTransaction.subtransactions[idx] || st), - ...(st[updatedField] != null && { - [updatedField]: st[updatedField], + newTransaction.subtransactions = diff.subtransactions.map( + (st, idx) => ({ + ...(newTransaction.subtransactions[idx] || st), + ...(st[updatedField] != null && { + [updatedField]: st[updatedField], + }), }), - }), - ); + ); + } } } - } - const { data: newTransactions } = updateTransaction( - transactions, - newTransaction, - ); - setTransactions(newTransactions); - }; + const { data: newTransactions } = updateTransaction( + transactions, + newTransaction, + ); + setTransactions(newTransactions); + }, + [dateFormat, transactions], + ); - const onSave = async newTransactions => { - if (deleted.current) { - return; - } + const onSave = useCallback( + async newTransactions => { + if (deleted.current) { + return; + } - const changes = diffItems(fetchedTransactions || [], newTransactions); - if ( - changes.added.length > 0 || - changes.updated.length > 0 || - changes.deleted.length - ) { - const _remoteUpdates = await send('transactions-batch-update', { - added: changes.added, - deleted: changes.deleted, - updated: changes.updated, - }); + const changes = diffItems(fetchedTransactions || [], newTransactions); + if ( + changes.added.length > 0 || + changes.updated.length > 0 || + changes.deleted.length + ) { + const _remoteUpdates = await send('transactions-batch-update', { + added: changes.added, + deleted: changes.deleted, + updated: changes.updated, + }); - // if (onTransactionsChange) { - // onTransactionsChange({ - // ...changes, - // updated: changes.updated.concat(remoteUpdates), - // }); - // } - } + // if (onTransactionsChange) { + // onTransactionsChange({ + // ...changes, + // updated: changes.updated.concat(remoteUpdates), + // }); + // } + } - if (adding.current) { - // The first one is always the "parent" and the only one we care - // about - dispatch(setLastTransaction(newTransactions[0])); - } - }; + if (adding.current) { + // The first one is always the "parent" and the only one we care + // about + dispatch(setLastTransaction(newTransactions[0])); + } + }, + [dispatch, fetchedTransactions], + ); - const onDelete = async id => { - const changes = deleteTransaction(transactions, id); + const onDelete = useCallback( + async id => { + const changes = deleteTransaction(transactions, id); - if (adding.current) { - // Adding a new transactions, this disables saving when the component unmounts - deleted.current = true; - } else { - const _remoteUpdates = await send('transactions-batch-update', { - deleted: changes.diff.deleted, - }); + if (adding.current) { + // Adding a new transactions, this disables saving when the component unmounts + deleted.current = true; + } else { + const _remoteUpdates = await send('transactions-batch-update', { + deleted: changes.diff.deleted, + }); - // if (onTransactionsChange) { - // onTransactionsChange({ ...changes, updated: remoteUpdates }); - // } - } + // if (onTransactionsChange) { + // onTransactionsChange({ ...changes, updated: remoteUpdates }); + // } + } - setTransactions(changes.data); - }; + setTransactions(changes.data); + }, + [transactions], + ); - const onAddSplit = id => { - const changes = addSplitTransaction(transactions, id); - setTransactions(changes.data); - }; + const onAddSplit = useCallback( + id => { + const changes = addSplitTransaction(transactions, id); + setTransactions(changes.data); + }, + [transactions], + ); - const onSplit = id => { - const changes = splitTransaction(transactions, id, parent => [ - makeChild(parent), - makeChild(parent), - ]); + const onSplit = useCallback( + id => { + const changes = splitTransaction(transactions, id, parent => [ + makeChild(parent), + makeChild(parent), + ]); - setTransactions(changes.data); - }; + setTransactions(changes.data); + }, + [transactions], + ); + + if ( + categories.length === 0 || + accounts.length === 0 || + transactions.length === 0 + ) { + return null; + } return (