mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 01:58:40 -05:00
[Mobile] Allow updating existing transaction's account (#3549)
* [Mobile] Allow updating existing transaction's account * Release notes * Update packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
051c8a6ed0
commit
1dce3183e5
@@ -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,101 @@ 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 getAccount = useCallback(
|
||||
trans => {
|
||||
return trans?.account && accountsById?.[trans.account];
|
||||
},
|
||||
[accountsById],
|
||||
);
|
||||
|
||||
const getPayee = trans => {
|
||||
return trans?.payee && payeesById?.[trans.payee];
|
||||
};
|
||||
const getPayee = useCallback(
|
||||
trans => {
|
||||
return trans?.payee && payeesById?.[trans.payee];
|
||||
},
|
||||
[payeesById],
|
||||
);
|
||||
|
||||
const getTransferAcct = trans => {
|
||||
const payee = trans && getPayee(trans);
|
||||
return payee?.transfer_acct && accountsById?.[payee.transfer_acct];
|
||||
};
|
||||
const getTransferAcct = useCallback(
|
||||
trans => {
|
||||
const payee = trans && getPayee(trans);
|
||||
return payee?.transfer_acct && accountsById?.[payee.transfer_acct];
|
||||
},
|
||||
[accountsById, getPayee],
|
||||
);
|
||||
|
||||
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 getPrettyPayee = useCallback(
|
||||
trans => {
|
||||
if (trans?.is_parent) {
|
||||
return 'Split';
|
||||
}
|
||||
const transPayee = trans && getPayee(trans);
|
||||
const transTransferAcct = trans && getTransferAcct(trans);
|
||||
return getDescriptionPretty(trans, transPayee, transTransferAcct);
|
||||
},
|
||||
[getPayee, getTransferAcct],
|
||||
);
|
||||
|
||||
const isBudgetTransfer = trans => {
|
||||
const transferAcct = trans && getTransferAcct(trans);
|
||||
return transferAcct && !transferAcct.offbudget;
|
||||
};
|
||||
const isBudgetTransfer = useCallback(
|
||||
trans => {
|
||||
const transferAcct = trans && getTransferAcct(trans);
|
||||
return transferAcct && !transferAcct.offbudget;
|
||||
},
|
||||
[getTransferAcct],
|
||||
);
|
||||
|
||||
const getCategory = (trans, isOffBudget) => {
|
||||
return isOffBudget
|
||||
? 'Off Budget'
|
||||
: isBudgetTransfer(trans)
|
||||
? 'Transfer'
|
||||
: lookupName(categories, trans.category);
|
||||
};
|
||||
const getCategory = useCallback(
|
||||
(trans, isOffBudget) => {
|
||||
if (isOffBudget) {
|
||||
return 'Off Budget';
|
||||
} else if (isBudgetTransfer(trans)) {
|
||||
return 'Transfer';
|
||||
} else {
|
||||
return lookupName(categories, trans.category);
|
||||
}
|
||||
},
|
||||
[categories, isBudgetTransfer],
|
||||
);
|
||||
|
||||
const onTotalAmountUpdate = value => {
|
||||
if (transaction.amount !== value) {
|
||||
onUpdate(transaction, 'amount', value.toString());
|
||||
} else {
|
||||
onClearActiveEdit();
|
||||
}
|
||||
};
|
||||
|
||||
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];
|
||||
if (account) {
|
||||
navigate(`/accounts/${account.id}`);
|
||||
} else {
|
||||
// Handle the case where account is undefined
|
||||
navigate(-1);
|
||||
}
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
@@ -556,133 +581,166 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
} else {
|
||||
onConfirmSave();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
accountsById,
|
||||
adding,
|
||||
dispatch,
|
||||
navigate,
|
||||
onSave,
|
||||
unserializedTransactions,
|
||||
]);
|
||||
|
||||
const onAdd = () => {
|
||||
onSave();
|
||||
};
|
||||
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();
|
||||
};
|
||||
|
||||
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;
|
||||
if (name === 'account') {
|
||||
hasAccountChanged.current = serializedTransaction.account !== value;
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
[onClearActiveEdit, onUpdate],
|
||||
);
|
||||
|
||||
const onDelete = id => {
|
||||
const [unserializedTransaction] = unserializedTransactions;
|
||||
const onTotalAmountUpdate = useCallback(
|
||||
value => {
|
||||
if (transaction.amount !== value) {
|
||||
onUpdateInner(transaction, 'amount', value.toString());
|
||||
} else {
|
||||
onClearActiveEdit();
|
||||
}
|
||||
},
|
||||
[onClearActiveEdit, onUpdateInner, transaction],
|
||||
);
|
||||
|
||||
const onConfirmDelete = () => {
|
||||
dispatch(
|
||||
pushModal('confirm-transaction-delete', {
|
||||
onConfirm: () => {
|
||||
props.onDelete(id);
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
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 +749,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 +788,13 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<Footer
|
||||
transactions={transactions}
|
||||
adding={adding}
|
||||
onAdd={onAdd}
|
||||
onSave={onSave}
|
||||
onAdd={onSaveInner}
|
||||
onSave={onSaveInner}
|
||||
onSplit={onSplit}
|
||||
onAddSplit={onAddSplit}
|
||||
onEmptySplitFound={onEmptySplitFound}
|
||||
editingField={editingField}
|
||||
onEditField={onEditField}
|
||||
onEditField={onEditFieldInner}
|
||||
/>
|
||||
}
|
||||
padding={0}
|
||||
@@ -779,7 +837,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'payee')
|
||||
}
|
||||
onClick={() => onEditField(transaction.id, 'payee')}
|
||||
onClick={() => onEditFieldInner(transaction.id, 'payee')}
|
||||
data-testid="payee-field"
|
||||
/>
|
||||
</View>
|
||||
@@ -802,7 +860,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
isOffBudget ||
|
||||
isBudgetTransfer(transaction)
|
||||
}
|
||||
onClick={() => onEditField(transaction.id, 'category')}
|
||||
onClick={() => onEditFieldInner(transaction.id, 'category')}
|
||||
data-testid="category-field"
|
||||
/>
|
||||
</View>
|
||||
@@ -824,9 +882,9 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
getCategory={getCategory}
|
||||
getPrettyPayee={getPrettyPayee}
|
||||
isBudgetTransfer={isBudgetTransfer}
|
||||
onUpdate={onUpdate}
|
||||
onEditField={onEditField}
|
||||
onDelete={onDelete}
|
||||
onUpdate={onUpdateInner}
|
||||
onEditField={onEditFieldInner}
|
||||
onDelete={onDeleteInner}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -867,12 +925,11 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<FieldLabel title="Account" />
|
||||
<TapField
|
||||
disabled={
|
||||
!adding ||
|
||||
(editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'account'))
|
||||
editingField &&
|
||||
editingField !== getFieldName(transaction.id, 'account')
|
||||
}
|
||||
value={account?.name}
|
||||
onClick={() => onEditField(transaction.id, 'account')}
|
||||
onClick={() => onEditFieldInner(transaction.id, 'account')}
|
||||
data-testid="account-field"
|
||||
/>
|
||||
</View>
|
||||
@@ -893,7 +950,7 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
onRequestActiveEdit(getFieldName(transaction.id, 'date'))
|
||||
}
|
||||
onUpdate={value =>
|
||||
onUpdate(
|
||||
onUpdateInner(
|
||||
transaction,
|
||||
'date',
|
||||
formatDate(parseISO(value), dateFormat),
|
||||
@@ -920,7 +977,9 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
<BooleanField
|
||||
disabled={editingField}
|
||||
checked={transaction.cleared}
|
||||
onUpdate={checked => onUpdate(transaction, 'cleared', checked)}
|
||||
onUpdate={checked =>
|
||||
onUpdateInner(transaction, 'cleared', checked)
|
||||
}
|
||||
style={{
|
||||
margin: 'auto',
|
||||
width: 22,
|
||||
@@ -942,14 +1001,14 @@ const TransactionEditInner = memo(function TransactionEditInner({
|
||||
onFocus={() => {
|
||||
onRequestActiveEdit(getFieldName(transaction.id, 'notes'));
|
||||
}}
|
||||
onUpdate={value => onUpdate(transaction, 'notes', value)}
|
||||
onUpdate={value => onUpdateInner(transaction, 'notes', value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{!adding && (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Button
|
||||
onClick={() => onDelete(transaction.id)}
|
||||
onClick={() => onDeleteInner(transaction.id)}
|
||||
style={{
|
||||
height: 40,
|
||||
borderWidth: 0,
|
||||
|
||||
6
upcoming-release-notes/3549.md
Normal file
6
upcoming-release-notes/3549.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Mobile] Allow updating existing transaction's account
|
||||
Reference in New Issue
Block a user