diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx index 83cd8799d0e79d369e47b6ac506db61ce7c835ff..ce34999b62a910fa1b8179753aeaf5bb92699a49 100644 --- a/packages/desktop-client/src/components/modals/EditRule.jsx +++ b/packages/desktop-client/src/components/modals/EditRule.jsx @@ -21,6 +21,7 @@ import { makeValue, FIELD_TYPES, TYPE_INFO, + ALLOCATION_METHODS, } from 'loot-core/src/shared/rules'; import { integerToCurrency, @@ -329,11 +330,7 @@ const parentOnlyFields = ['amount', 'cleared', 'account', 'date']; const splitActionFields = actionFields.filter( ([field]) => !parentOnlyFields.includes(field), ); -const splitAmountTypes = [ - ['fixed-amount', 'a fixed amount'], - ['fixed-percent', 'a fixed percentage'], - ['remainder', 'an equal portion of the remainder'], -]; +const allocationMethodOptions = Object.entries(ALLOCATION_METHODS); function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) { const { field, @@ -379,7 +376,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) { </View> <SplitAmountMethodSelect - options={splitAmountTypes} + options={allocationMethodOptions} value={options.method} onChange={onChange} /> diff --git a/packages/desktop-client/src/components/rules/ActionExpression.tsx b/packages/desktop-client/src/components/rules/ActionExpression.tsx index 82421112c0f9f8d5fe685781689aab0278725c9a..fa55e6d858de0e6e6446277fb92a2c6e83f3d16d 100644 --- a/packages/desktop-client/src/components/rules/ActionExpression.tsx +++ b/packages/desktop-client/src/components/rules/ActionExpression.tsx @@ -1,7 +1,12 @@ import React from 'react'; -import { mapField, friendlyOp } from 'loot-core/src/shared/rules'; import { + mapField, + friendlyOp, + ALLOCATION_METHODS, +} from 'loot-core/src/shared/rules'; +import { + type SetSplitAmountRuleActionEntity, type LinkScheduleRuleActionEntity, type RuleActionEntity, type SetRuleActionEntity, @@ -40,6 +45,8 @@ export function ActionExpression({ style, ...props }: ActionExpressionProps) { > {props.op === 'set' ? ( <SetActionExpression {...props} /> + ) : props.op === 'set-split-amount' ? ( + <SetSplitAmountActionExpression {...props} /> ) : props.op === 'link-schedule' ? ( <LinkScheduleActionExpression {...props} /> ) : null} @@ -63,6 +70,29 @@ function SetActionExpression({ ); } +function SetSplitAmountActionExpression({ + op, + value, + options, +}: SetSplitAmountRuleActionEntity) { + const method = options?.method; + if (!method) { + return null; + } + + return ( + <> + <Text>{friendlyOp(op)}</Text>{' '} + <Text style={valueStyle}>{ALLOCATION_METHODS[method]}</Text> + {method !== 'remainder' && ': '} + {method === 'fixed-amount' && ( + <Value style={valueStyle} value={value} field="amount" /> + )} + {method === 'fixed-percent' && <Text style={valueStyle}>{value}%</Text>} + </> + ); +} + function LinkScheduleActionExpression({ op, value, diff --git a/packages/desktop-client/src/components/rules/RuleRow.tsx b/packages/desktop-client/src/components/rules/RuleRow.tsx index d8a0713ccf1719bd95d75444e727988daff5b85f..4af4d31050c8f36a5ab30dc60ce0e2f75a03f16f 100644 --- a/packages/desktop-client/src/components/rules/RuleRow.tsx +++ b/packages/desktop-client/src/components/rules/RuleRow.tsx @@ -1,12 +1,14 @@ // @ts-strict-ignore import React, { memo } from 'react'; +import { v4 as uuid } from 'uuid'; + import { friendlyOp } from 'loot-core/src/shared/rules'; import { type RuleEntity } from 'loot-core/src/types/models'; import { useSelectedDispatch } from '../../hooks/useSelected'; import { SvgRightArrow2 } from '../../icons/v0'; -import { theme } from '../../style'; +import { styles, theme } from '../../style'; import { Button } from '../common/Button'; import { Stack } from '../common/Stack'; import { Text } from '../common/Text'; @@ -30,6 +32,17 @@ export const RuleRow = memo( const borderColor = selected ? theme.tableBorderSelected : 'none'; const backgroundFocus = hovered; + const actionSplits = rule.actions.reduce( + (acc, action) => { + const splitIndex = action['options']?.splitIndex ?? 0; + acc[splitIndex] = acc[splitIndex] ?? { id: uuid(), actions: [] }; + acc[splitIndex].actions.push(action); + return acc; + }, + [] as { id: string; actions: RuleEntity['actions'] }[], + ); + const hasSplits = actionSplits.length > 1; + return ( <Row height="auto" @@ -103,13 +116,47 @@ export const RuleRow = memo( style={{ flex: 1, alignItems: 'flex-start' }} data-testid="actions" > - {rule.actions.map((action, i) => ( - <ActionExpression - key={i} - {...action} - style={i !== 0 && { marginTop: 3 }} - /> - ))} + {hasSplits + ? actionSplits.map((split, i) => ( + <View + key={split.id} + style={{ + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + marginTop: i > 0 ? 3 : 0, + padding: '5px', + borderColor: theme.tableBorder, + borderWidth: '1px', + borderRadius: '5px', + }} + > + <Text + style={{ + ...styles.verySmallText, + color: theme.pageTextLight, + marginBottom: 6, + }} + > + {i ? `Split ${i}` : 'Before split'} + </Text> + {split.actions.map((action, j) => ( + <ActionExpression + key={j} + {...action} + style={j !== 0 && { marginTop: 3 }} + /> + ))} + </View> + )) + : rule.actions.map((action, i) => ( + <ActionExpression + key={i} + {...action} + style={i !== 0 && { marginTop: 3 }} + /> + ))} </View> </Stack> </Field> diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts index 926b6797cabe6d14be3c85fe60a68f24a3e196b2..3119cdd69c0fb662b1a160f528a3dc08da9ee29c 100644 --- a/packages/loot-core/src/server/accounts/transaction-rules.ts +++ b/packages/loot-core/src/server/accounts/transaction-rules.ts @@ -505,7 +505,15 @@ export async function applyActions( } try { - if (action.op === 'link-schedule') { + if (action.op === 'set-split-amount') { + return new Action( + action.op, + null, + action.value, + action.options, + FIELD_TYPES, + ); + } else if (action.op === 'link-schedule') { return new Action(action.op, null, action.value, null, FIELD_TYPES); } diff --git a/packages/loot-core/src/server/rules/app.ts b/packages/loot-core/src/server/rules/app.ts index 8fb059c5a94ef60942a7fd079a80adb9b9ed17de..abd02e0c15ca4d9a32f04ec2450542811c99d398 100644 --- a/packages/loot-core/src/server/rules/app.ts +++ b/packages/loot-core/src/server/rules/app.ts @@ -46,15 +46,23 @@ function validateRule(rule: Partial<RuleEntity>) { ); const actionErrors = runValidation(rule.actions, action => - action.op === 'link-schedule' - ? new Action(action.op, null, action.value, null, ruleFieldTypes) - : new Action( + action.op === 'set-split-amount' + ? new Action( action.op, - action.field, + null, action.value, action.options, ruleFieldTypes, - ), + ) + : action.op === 'link-schedule' + ? new Action(action.op, null, action.value, null, ruleFieldTypes) + : new Action( + action.op, + action.field, + action.value, + action.options, + ruleFieldTypes, + ), ); if (conditionErrors || actionErrors) { diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts index 9eb16dade64b75ce30bd46f02e9607b9d7e820a6..c54b8723a3a6ea09963a27aa14e4615d24d93ba8 100644 --- a/packages/loot-core/src/shared/rules.ts +++ b/packages/loot-core/src/shared/rules.ts @@ -47,6 +47,12 @@ export const FIELD_TYPES = new Map( }), ); +export const ALLOCATION_METHODS = { + 'fixed-amount': 'a fixed amount', + 'fixed-percent': 'a fixed percent', + remainder: 'an equal portion of the remainder', +}; + export function mapField(field, opts?) { opts = opts || {}; @@ -113,6 +119,8 @@ export function friendlyOp(op, type?) { return 'is false'; case 'set': return 'set'; + case 'set-split-amount': + return 'allocate'; case 'link-schedule': return 'link schedule'; case 'and': diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts index 66fa53f99a24de57afe9dd8aa8510ceb07af0ca8..86e9348d30a0e718edc62d99c48be16c9a2154cd 100644 --- a/packages/loot-core/src/types/models/rule.d.ts +++ b/packages/loot-core/src/types/models/rule.d.ts @@ -40,6 +40,7 @@ export interface RuleConditionEntity { export type RuleActionEntity = | SetRuleActionEntity + | SetSplitAmountRuleActionEntity | LinkScheduleRuleActionEntity; export interface SetRuleActionEntity { diff --git a/upcoming-release-notes/2368.md b/upcoming-release-notes/2368.md new file mode 100644 index 0000000000000000000000000000000000000000..562ffeebbe94cdbc0d405205d29493a9d5f24656 --- /dev/null +++ b/upcoming-release-notes/2368.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [jfdoming] +--- + +Show rules with splits on rules overview page