From 8ae8593b209d01d6e92cd8cd228c03a3b189f28a Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Mon, 30 Sep 2024 17:17:46 -0700 Subject: [PATCH 1/5] [Mobile] Allow updating existing transaction's account --- .../mobile/transactions/TransactionEdit.jsx | 412 ++++++++++-------- 1 file changed, 232 insertions(+), 180 deletions(-) diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index a08bf42a381..72045fdca76 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -5,6 +5,7 @@ import React, { useRef, memo, useMemo, + useCallback, } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useParams } from 'react-router-dom'; @@ -43,6 +44,7 @@ import { import { useAccounts } from '../../../hooks/useAccounts'; import { useCategories } from '../../../hooks/useCategories'; import { useDateFormat } from '../../../hooks/useDateFormat'; +import { useInitialMount } from '../../../hooks/useInitialMount'; import { useNavigate } from '../../../hooks/useNavigate'; import { usePayees } from '../../../hooks/usePayees'; import { @@ -443,9 +445,13 @@ const TransactionEditInner = memo(function TransactionEditInner({ payees, dateFormat, transactions: unserializedTransactions, - navigate, - ...props + onSave, + onUpdate, + onDelete, + onSplit, + onAddSplit, }) { + const navigate = useNavigate(); const dispatch = useDispatch(); const transactions = useMemo( () => @@ -461,82 +467,94 @@ const TransactionEditInner = memo(function TransactionEditInner({ useSingleActiveEditForm(); const [totalAmountFocused, setTotalAmountFocused] = useState(true); const childTransactionElementRefMap = useRef({}); + const hasAccountChanged = useRef(false); const payeesById = useMemo(() => groupById(payees), [payees]); const accountsById = useMemo(() => groupById(accounts), [accounts]); - const onTotalAmountEdit = () => { + const onTotalAmountEdit = useCallback(() => { onRequestActiveEdit?.(getFieldName(transaction.id, 'amount'), () => { setTotalAmountFocused(true); return () => setTotalAmountFocused(false); }); - }; + }, [onRequestActiveEdit, transaction.id]); + + const isInitialMount = useInitialMount(); useEffect(() => { - if (adding) { + if (isInitialMount && adding) { onTotalAmountEdit(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [adding, isInitialMount, onTotalAmountEdit]); - const getAccount = trans => { - return trans?.account && accountsById?.[trans.account]; - }; - - const getPayee = trans => { - return trans?.payee && payeesById?.[trans.payee]; - }; + const getAccount = useCallback( + trans => { + return trans?.account && accountsById?.[trans.account]; + }, + [accountsById], + ); - const getTransferAcct = trans => { - const payee = trans && getPayee(trans); - return payee?.transfer_acct && accountsById?.[payee.transfer_acct]; - }; + const getPayee = useCallback( + trans => { + return trans?.payee && payeesById?.[trans.payee]; + }, + [payeesById], + ); - const getPrettyPayee = trans => { - if (trans && trans.is_parent) { - return 'Split'; - } - const transPayee = trans && getPayee(trans); - const transTransferAcct = trans && getTransferAcct(trans); - return getDescriptionPretty(trans, transPayee, transTransferAcct); - }; + const getTransferAcct = useCallback( + trans => { + const payee = trans && getPayee(trans); + return payee?.transfer_acct && accountsById?.[payee.transfer_acct]; + }, + [accountsById, getPayee], + ); - const isBudgetTransfer = trans => { - const transferAcct = trans && getTransferAcct(trans); - return transferAcct && !transferAcct.offbudget; - }; + const getPrettyPayee = useCallback( + trans => { + if (trans && trans.is_parent) { + return 'Split'; + } + const transPayee = trans && getPayee(trans); + const transTransferAcct = trans && getTransferAcct(trans); + return getDescriptionPretty(trans, transPayee, transTransferAcct); + }, + [getPayee, getTransferAcct], + ); - const getCategory = (trans, isOffBudget) => { - return isOffBudget - ? 'Off Budget' - : isBudgetTransfer(trans) - ? 'Transfer' - : lookupName(categories, trans.category); - }; + const isBudgetTransfer = useCallback( + trans => { + const transferAcct = trans && getTransferAcct(trans); + return transferAcct && !transferAcct.offbudget; + }, + [getTransferAcct], + ); - const onTotalAmountUpdate = value => { - if (transaction.amount !== value) { - onUpdate(transaction, 'amount', value.toString()); - } else { - onClearActiveEdit(); - } - }; + const getCategory = useCallback( + (trans, isOffBudget) => { + return isOffBudget + ? 'Off Budget' + : isBudgetTransfer(trans) + ? 'Transfer' + : lookupName(categories, trans.category); + }, + [categories, isBudgetTransfer], + ); - const onSave = async () => { + const onSaveInner = useCallback(() => { const [unserializedTransaction] = unserializedTransactions; - const onConfirmSave = async () => { + const onConfirmSave = () => { let transactionsToSave = unserializedTransactions; if (adding) { transactionsToSave = realizeTempTransactions(unserializedTransactions); } - props.onSave(transactionsToSave); + onSave(transactionsToSave); - if (adding) { + if (adding || hasAccountChanged.current) { const { account: accountId } = unserializedTransaction; - const account = accountsById[accountId]; - navigate(`/accounts/${account.id}`, { replace: true }); + const account = accountsById?.[accountId]; + navigate(`/accounts/${account.id}`); } else { navigate(-1); } @@ -556,133 +574,166 @@ const TransactionEditInner = memo(function TransactionEditInner({ } else { onConfirmSave(); } - }; - - const onAdd = () => { - onSave(); - }; + }, [ + accountsById, + adding, + dispatch, + navigate, + onSave, + unserializedTransactions, + ]); + + const onUpdateInner = useCallback( + async (serializedTransaction, name, value) => { + const newTransaction = { ...serializedTransaction, [name]: value }; + await onUpdate(newTransaction, name); + onClearActiveEdit(); - const onUpdate = async (serializedTransaction, name, value) => { - const newTransaction = { ...serializedTransaction, [name]: value }; - await props.onUpdate(newTransaction, name); - onClearActiveEdit(); - }; + if (name === 'account') { + hasAccountChanged.current = serializedTransaction.account !== value; + } + }, + [onClearActiveEdit, onUpdate], + ); - const onEditField = (transactionId, name) => { - onRequestActiveEdit?.(getFieldName(transaction.id, name), () => { - const transactionToEdit = transactions.find(t => t.id === transactionId); - const unserializedTransaction = unserializedTransactions.find( - t => t.id === transactionId, - ); - switch (name) { - case 'category': - dispatch( - pushModal('category-autocomplete', { - categoryGroups, - month: monthUtils.monthFromDate(unserializedTransaction.date), - onSelect: categoryId => { - onUpdate(transactionToEdit, name, categoryId); - }, - onClose: () => { - onClearActiveEdit(); - }, - }), - ); - break; - case 'account': - dispatch( - pushModal('account-autocomplete', { - onSelect: accountId => { - onUpdate(transactionToEdit, name, accountId); - }, - onClose: () => { - onClearActiveEdit(); - }, - }), - ); - break; - case 'payee': - dispatch( - pushModal('payee-autocomplete', { - onSelect: payeeId => { - onUpdate(transactionToEdit, name, payeeId); - }, - onClose: () => { - onClearActiveEdit(); - }, - }), - ); - break; - default: - dispatch( - pushModal('edit-field', { - name, - month: monthUtils.monthFromDate(unserializedTransaction.date), - onSubmit: (name, value) => { - onUpdate(transactionToEdit, name, value); - }, - onClose: () => { - onClearActiveEdit(); - }, - }), - ); - break; + const onTotalAmountUpdate = useCallback( + value => { + if (transaction.amount !== value) { + onUpdateInner(transaction, 'amount', value.toString()); + } else { + onClearActiveEdit(); } - }); - }; + }, + [onClearActiveEdit, onUpdateInner, transaction], + ); - const onDelete = id => { - const [unserializedTransaction] = unserializedTransactions; + const onEditFieldInner = useCallback( + (transactionId, name) => { + onRequestActiveEdit?.(getFieldName(transaction.id, name), () => { + const transactionToEdit = transactions.find( + t => t.id === transactionId, + ); + const unserializedTransaction = unserializedTransactions.find( + t => t.id === transactionId, + ); + switch (name) { + case 'category': + dispatch( + pushModal('category-autocomplete', { + categoryGroups, + month: monthUtils.monthFromDate(unserializedTransaction.date), + onSelect: categoryId => { + onUpdateInner(transactionToEdit, name, categoryId); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + case 'account': + dispatch( + pushModal('account-autocomplete', { + onSelect: accountId => { + onUpdateInner(transactionToEdit, name, accountId); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + case 'payee': + dispatch( + pushModal('payee-autocomplete', { + onSelect: payeeId => { + onUpdateInner(transactionToEdit, name, payeeId); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + default: + dispatch( + pushModal('edit-field', { + name, + month: monthUtils.monthFromDate(unserializedTransaction.date), + onSubmit: (name, value) => { + onUpdateInner(transactionToEdit, name, value); + }, + onClose: () => { + onClearActiveEdit(); + }, + }), + ); + break; + } + }); + }, + [ + categoryGroups, + dispatch, + onUpdateInner, + onClearActiveEdit, + onRequestActiveEdit, + transaction.id, + transactions, + unserializedTransactions, + ], + ); - const onConfirmDelete = () => { - dispatch( - pushModal('confirm-transaction-delete', { - onConfirm: () => { - props.onDelete(id); - - if (unserializedTransaction.id !== id) { - // Only a child transaction was deleted. - onClearActiveEdit(); - return; - } + const onDeleteInner = useCallback( + id => { + const [unserializedTransaction] = unserializedTransactions; - navigate(-1); - }, - }), - ); - }; + const onConfirmDelete = () => { + dispatch( + pushModal('confirm-transaction-delete', { + onConfirm: () => { + onDelete(id); - if (unserializedTransaction.reconciled) { - dispatch( - pushModal('confirm-transaction-edit', { - onConfirm: onConfirmDelete, - confirmReason: 'deleteReconciled', - }), - ); - } else { - onConfirmDelete(); - } - }; + if (unserializedTransaction.id !== id) { + // Only a child transaction was deleted. + onClearActiveEdit(); + return; + } - const scrollChildTransactionIntoView = id => { + navigate(-1); + }, + }), + ); + }; + + if (unserializedTransaction.reconciled) { + dispatch( + pushModal('confirm-transaction-edit', { + onConfirm: onConfirmDelete, + confirmReason: 'deleteReconciled', + }), + ); + } else { + onConfirmDelete(); + } + }, + [dispatch, navigate, onClearActiveEdit, onDelete, unserializedTransactions], + ); + + const scrollChildTransactionIntoView = useCallback(id => { const childTransactionEditElement = childTransactionElementRefMap.current?.[id]; childTransactionEditElement?.scrollIntoView({ behavior: 'smooth', }); - }; - - const onAddSplit = id => { - props.onAddSplit(id); - }; - - const onSplit = id => { - props.onSplit(id); - }; + }, []); - const onEmptySplitFound = id => { - scrollChildTransactionIntoView(id); - }; + const onEmptySplitFound = useCallback( + id => { + scrollChildTransactionIntoView(id); + }, + [scrollChildTransactionIntoView], + ); useEffect(() => { const noAmountChildTransaction = childTransactions.find( @@ -691,7 +742,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ if (noAmountChildTransaction) { scrollChildTransactionIntoView(noAmountChildTransaction.id); } - }, [childTransactions]); + }, [childTransactions, scrollChildTransactionIntoView]); // Child transactions should always default to the signage // of the parent transaction @@ -730,13 +781,13 @@ const TransactionEditInner = memo(function TransactionEditInner({