Skip to content

Commit

Permalink
Add rules with splits (actualbudget#2059)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jfdoming authored Feb 8, 2024
1 parent 4566f7a commit 4de423d
Show file tree
Hide file tree
Showing 19 changed files with 565 additions and 112 deletions.
355 changes: 288 additions & 67 deletions packages/desktop-client/src/components/modals/EditRule.jsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function ExperimentalFeatures() {
Goal templates
</FeatureToggle>
<FeatureToggle flag="simpleFinSync">SimpleFIN sync</FeatureToggle>
<FeatureToggle flag="splitsInRules">Splits in rules</FeatureToggle>
</View>
) : (
<LinkButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { type CSSProperties, styles } from '../../style';
import { Text } from '../common/Text';
import { ConditionalPrivacyFilter } from '../PrivacyFilter';

import { useFormat } from './useFormat';
import { type FormatType, useFormat } from './useFormat';
import { useSheetName } from './useSheetName';
import { useSheetValue } from './useSheetValue';

import { type Binding } from '.';

type CellValueProps = {
binding: string | Binding;
type?: string;
type?: FormatType;
formatter?: (value) => ReactNode;
style?: CSSProperties;
getStyle?: (value) => CSSProperties;
Expand Down
10 changes: 8 additions & 2 deletions packages/desktop-client/src/components/spreadsheet/useFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { useSelector } from 'react-redux';
import { selectNumberFormat } from 'loot-core/src/client/selectors';
import { integerToCurrency } from 'loot-core/src/shared/util';

export type FormatType =
| 'string'
| 'number'
| 'financial'
| 'financial-with-sign';

function format(
value: unknown,
type = 'string',
type: FormatType = 'string',
formatter?: Intl.NumberFormat,
): string {
switch (type) {
Expand Down Expand Up @@ -49,7 +55,7 @@ export function useFormat() {
const numberFormat = useSelector(selectNumberFormat);

return useCallback(
(value: unknown, type = 'string') =>
(value: unknown, type: FormatType = 'string') =>
format(value, type, numberFormat.formatter),
[numberFormat],
);
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop-client/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
mergeConditionalPrivacyFilterProps,
} from './PrivacyFilter';
import { type Binding } from './spreadsheet';
import { useFormat } from './spreadsheet/useFormat';
import { type FormatType, useFormat } from './spreadsheet/useFormat';
import { useSheetValue } from './spreadsheet/useSheetValue';
import { Tooltip, IntersectionBoundary } from './tooltips';

Expand Down Expand Up @@ -660,7 +660,7 @@ export function SelectCell({

type SheetCellValueProps = {
binding: Binding;
type: string;
type: FormatType;
getValueStyle?: (value: string | number) => CSSProperties;
formatExpr?: (value) => string;
unformatExpr?: (value: string) => unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,27 +992,31 @@ function TransactionEditUnconnected(props) {
return null;
}

const onEdit = async transaction => {
let newTransaction = transaction;
const onEdit = async serializedTransaction => {
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.
if (isTemporary(transaction)) {
const afterRules = await send('rules-run', { transaction });
const diff = getChangedValues(transaction, afterRules);

newTransaction = { ...transaction };
if (diff) {
Object.keys(diff).forEach(field => {
if (newTransaction[field] == null) {
newTransaction[field] = diff[field];
if (transaction[field] == null) {
transaction[field] = diff[field];
}
});
}
}

const { data: newTransactions } = updateTransaction(
transactions,
deserializeTransaction(newTransaction, null, dateFormat),
transaction,
);
setTransactions(newTransactions);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
updateTransaction,
deleteTransaction,
addSplitTransaction,
groupTransaction,
ungroupTransactions,
} from 'loot-core/src/shared/transactions';
import {
integerToCurrency,
Expand Down Expand Up @@ -705,6 +707,7 @@ function PayeeIcons({
const Transaction = memo(function Transaction(props) {
const {
transaction: originalTransaction,
subtransactions,
editing,
showAccount,
showBalance,
Expand Down Expand Up @@ -843,7 +846,8 @@ const Transaction = memo(function Transaction(props) {
// Run the transaction through the formatting so that we know
// it's always showing the formatted result
setTransaction(serializeTransaction(deserialized, showZeroInDeposit));
onSave(deserialized);

onSave(deserialized, subtransactions);
}
}

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

const emptyChildTransactions = transactions.filter(
t => t.parent_id === transactions[0].id && t.amount === 0,
const childTransactions = transactions.filter(
t => t.parent_id === transactions[0].id,
);
const emptyChildTransactions = childTransactions.filter(t => t.amount === 0);

return (
<View
Expand All @@ -1513,6 +1518,7 @@ function NewTransaction({
key={transaction.id}
editing={editingTransaction === transaction.id}
transaction={transaction}
subtransactions={transaction.is_parent ? childTransactions : null}
showAccount={showAccount}
showCategory={showCategory}
showBalance={showBalance}
Expand Down Expand Up @@ -2064,18 +2070,28 @@ export const TransactionTable = forwardRef((props, ref) => {
}, [props.onAdd, newNavigator.onEdit]);

const onSave = useCallback(
async transaction => {
async (transaction, subtransactions = null) => {
savePending.current = true;

let groupedTransaction = subtransactions
? groupTransaction([transaction, ...subtransactions])
: transaction;

if (isTemporaryId(transaction.id)) {
if (props.onApplyRules) {
transaction = await props.onApplyRules(transaction);
groupedTransaction = await props.onApplyRules(groupedTransaction);
}

const newTrans = latestState.current.newTransactions;
setNewTransactions(updateTransaction(newTrans, transaction).data);
// Future refactor: we shouldn't need to iterate through the entire
// transaction list to ungroup, just the new transactions.
setNewTransactions(
ungroupTransactions(
updateTransaction(newTrans, groupedTransaction).data,
),
);
} else {
props.onSave(transaction);
props.onSave(groupedTransaction);
}
},
[props.onSave],
Expand Down
1 change: 1 addition & 0 deletions packages/desktop-client/src/hooks/useFeatureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
goalTemplatesEnabled: false,
customReports: false,
simpleFinSync: false,
splitsInRules: false,
};

export function useFeatureFlag(name: FeatureFlag): boolean {
Expand Down
Loading

0 comments on commit 4de423d

Please sign in to comment.