Skip to content

Commit 2bb7b3c

Browse files
authored
Add rules with splits (actualbudget#2059)
* Add split creation UI to rule creation modal * Support applying splits when rules execute * fix: deserialize transaction before running rules According to how rules are run in other places in the app, we should be supplying a "deserialized" (i.e., integer-for-amount and ISO date) transaction rather than a "serialized" (amount-plus-formatted-date) one. This fixes a crash in how split transactions are applied, as well as date-based rules not applying correctly previously (any rule with a date condition would never match on mobile). * Add release notes * Fix missing types pulled in from master * PR feedback: use `getActions` * PR feedback: use `flatMap` * Fix action deletion * Don't flicker upon split deletion * Let users specify parent transaction actions (e.g. linking schedules) * Support empty splits * Revert adding `no-op` action type * Support splits by percent * Fix types * Fix crash on transactions page when posting a transaction The crash would probably have occurred in other places too with auto-posting schedules :/ * Fix a bug where schedules wouldn't be marked as completed This was because the query that we previously used didn't select parent transactions, so no transaction was marked as being scheduled (since only parent transactions have schedule IDs). * Add feature flag * Limit set actions within splits to fewer fields * Fix merge conflict * Don't run split rules if feature is disabled * Fix percent-based splits not applying * Fix crash when editing parent transaction amount * Auto-format * Attempt to fix failing tests * More test/bug fixes * Add an extra split at the end if there is a remaining amount * Make sure split has correct values for dynamic remainder * Remove extraneous console.log
1 parent d6f610a commit 2bb7b3c

19 files changed

Lines changed: 565 additions & 112 deletions

File tree

packages/desktop-client/src/components/modals/EditRule.jsx

Lines changed: 288 additions & 67 deletions
Large diffs are not rendered by default.

packages/desktop-client/src/components/settings/Experimental.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export function ExperimentalFeatures() {
9999
Goal templates
100100
</FeatureToggle>
101101
<FeatureToggle flag="simpleFinSync">SimpleFIN sync</FeatureToggle>
102+
<FeatureToggle flag="splitsInRules">Splits in rules</FeatureToggle>
102103
</View>
103104
) : (
104105
<LinkButton

packages/desktop-client/src/components/spreadsheet/CellValue.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { type CSSProperties, styles } from '../../style';
55
import { Text } from '../common/Text';
66
import { ConditionalPrivacyFilter } from '../PrivacyFilter';
77

8-
import { useFormat } from './useFormat';
8+
import { type FormatType, useFormat } from './useFormat';
99
import { useSheetName } from './useSheetName';
1010
import { useSheetValue } from './useSheetValue';
1111

1212
import { type Binding } from '.';
1313

1414
type CellValueProps = {
1515
binding: string | Binding;
16-
type?: string;
16+
type?: FormatType;
1717
formatter?: (value) => ReactNode;
1818
style?: CSSProperties;
1919
getStyle?: (value) => CSSProperties;

packages/desktop-client/src/components/spreadsheet/useFormat.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import { useSelector } from 'react-redux';
44
import { selectNumberFormat } from 'loot-core/src/client/selectors';
55
import { integerToCurrency } from 'loot-core/src/shared/util';
66

7+
export type FormatType =
8+
| 'string'
9+
| 'number'
10+
| 'financial'
11+
| 'financial-with-sign';
12+
713
function format(
814
value: unknown,
9-
type = 'string',
15+
type: FormatType = 'string',
1016
formatter?: Intl.NumberFormat,
1117
): string {
1218
switch (type) {
@@ -49,7 +55,7 @@ export function useFormat() {
4955
const numberFormat = useSelector(selectNumberFormat);
5056

5157
return useCallback(
52-
(value: unknown, type = 'string') =>
58+
(value: unknown, type: FormatType = 'string') =>
5359
format(value, type, numberFormat.formatter),
5460
[numberFormat],
5561
);

packages/desktop-client/src/components/table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
mergeConditionalPrivacyFilterProps,
4141
} from './PrivacyFilter';
4242
import { type Binding } from './spreadsheet';
43-
import { useFormat } from './spreadsheet/useFormat';
43+
import { type FormatType, useFormat } from './spreadsheet/useFormat';
4444
import { useSheetValue } from './spreadsheet/useSheetValue';
4545
import { Tooltip, IntersectionBoundary } from './tooltips';
4646

@@ -660,7 +660,7 @@ export function SelectCell({
660660

661661
type SheetCellValueProps = {
662662
binding: Binding;
663-
type: string;
663+
type: FormatType;
664664
getValueStyle?: (value: string | number) => CSSProperties;
665665
formatExpr?: (value) => string;
666666
unformatExpr?: (value: string) => unknown;

packages/desktop-client/src/components/transactions/MobileTransaction.jsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -992,27 +992,31 @@ function TransactionEditUnconnected(props) {
992992
return null;
993993
}
994994

995-
const onEdit = async transaction => {
996-
let newTransaction = transaction;
995+
const onEdit = async serializedTransaction => {
996+
const transaction = deserializeTransaction(
997+
serializedTransaction,
998+
null,
999+
dateFormat,
1000+
);
1001+
9971002
// Run the rules to auto-fill in any data. Right now we only do
9981003
// this on new transactions because that's how desktop works.
9991004
if (isTemporary(transaction)) {
10001005
const afterRules = await send('rules-run', { transaction });
10011006
const diff = getChangedValues(transaction, afterRules);
10021007

1003-
newTransaction = { ...transaction };
10041008
if (diff) {
10051009
Object.keys(diff).forEach(field => {
1006-
if (newTransaction[field] == null) {
1007-
newTransaction[field] = diff[field];
1010+
if (transaction[field] == null) {
1011+
transaction[field] = diff[field];
10081012
}
10091013
});
10101014
}
10111015
}
10121016

10131017
const { data: newTransactions } = updateTransaction(
10141018
transactions,
1015-
deserializeTransaction(newTransaction, null, dateFormat),
1019+
transaction,
10161020
);
10171021
setTransactions(newTransactions);
10181022
};

packages/desktop-client/src/components/transactions/TransactionsTable.jsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
updateTransaction,
3636
deleteTransaction,
3737
addSplitTransaction,
38+
groupTransaction,
39+
ungroupTransactions,
3840
} from 'loot-core/src/shared/transactions';
3941
import {
4042
integerToCurrency,
@@ -705,6 +707,7 @@ function PayeeIcons({
705707
const Transaction = memo(function Transaction(props) {
706708
const {
707709
transaction: originalTransaction,
710+
subtransactions,
708711
editing,
709712
showAccount,
710713
showBalance,
@@ -843,7 +846,8 @@ const Transaction = memo(function Transaction(props) {
843846
// Run the transaction through the formatting so that we know
844847
// it's always showing the formatted result
845848
setTransaction(serializeTransaction(deserialized, showZeroInDeposit));
846-
onSave(deserialized);
849+
850+
onSave(deserialized, subtransactions);
847851
}
848852
}
849853

@@ -1489,9 +1493,10 @@ function NewTransaction({
14891493
const error = transactions[0].error;
14901494
const isDeposit = transactions[0].amount > 0;
14911495

1492-
const emptyChildTransactions = transactions.filter(
1493-
t => t.parent_id === transactions[0].id && t.amount === 0,
1496+
const childTransactions = transactions.filter(
1497+
t => t.parent_id === transactions[0].id,
14941498
);
1499+
const emptyChildTransactions = childTransactions.filter(t => t.amount === 0);
14951500

14961501
return (
14971502
<View
@@ -1513,6 +1518,7 @@ function NewTransaction({
15131518
key={transaction.id}
15141519
editing={editingTransaction === transaction.id}
15151520
transaction={transaction}
1521+
subtransactions={transaction.is_parent ? childTransactions : null}
15161522
showAccount={showAccount}
15171523
showCategory={showCategory}
15181524
showBalance={showBalance}
@@ -2064,18 +2070,28 @@ export const TransactionTable = forwardRef((props, ref) => {
20642070
}, [props.onAdd, newNavigator.onEdit]);
20652071

20662072
const onSave = useCallback(
2067-
async transaction => {
2073+
async (transaction, subtransactions = null) => {
20682074
savePending.current = true;
20692075

2076+
let groupedTransaction = subtransactions
2077+
? groupTransaction([transaction, ...subtransactions])
2078+
: transaction;
2079+
20702080
if (isTemporaryId(transaction.id)) {
20712081
if (props.onApplyRules) {
2072-
transaction = await props.onApplyRules(transaction);
2082+
groupedTransaction = await props.onApplyRules(groupedTransaction);
20732083
}
20742084

20752085
const newTrans = latestState.current.newTransactions;
2076-
setNewTransactions(updateTransaction(newTrans, transaction).data);
2086+
// Future refactor: we shouldn't need to iterate through the entire
2087+
// transaction list to ungroup, just the new transactions.
2088+
setNewTransactions(
2089+
ungroupTransactions(
2090+
updateTransaction(newTrans, groupedTransaction).data,
2091+
),
2092+
);
20772093
} else {
2078-
props.onSave(transaction);
2094+
props.onSave(groupedTransaction);
20792095
}
20802096
},
20812097
[props.onSave],

packages/desktop-client/src/hooks/useFeatureFlag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
1212
goalTemplatesEnabled: false,
1313
customReports: false,
1414
simpleFinSync: false,
15+
splitsInRules: false,
1516
};
1617

1718
export function useFeatureFlag(name: FeatureFlag): boolean {

0 commit comments

Comments
 (0)