Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/desktop-client/src/components/ManageRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ function ruleToString(rule, data) {
friendlyOp(action.op),
'“' + mapValue(action.field, action.value, data) + '”',
];
} else if (action.op === 'delete-transaction') {
return [friendlyOp(action.op), '(delete)'];
} else {
return [];
}
Expand Down
3 changes: 2 additions & 1 deletion packages/desktop-client/src/components/accounts/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,8 @@ class AccountInternal extends PureComponent<

// sync the reconciliation transaction
await send('transactions-batch-update', {
added: ruledTransactions,
added: ruledTransactions.filter(trans => !trans.tombstone),
deleted: ruledTransactions.filter(trans => trans.tombstone),
});
await this.refetchTransactions();
};
Expand Down
34 changes: 23 additions & 11 deletions packages/desktop-client/src/components/modals/EditRuleModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
{op === 'set' ? (
<>
<OpSelect
ops={['set', 'prepend-notes', 'append-notes']}
ops={['set', 'prepend-notes', 'append-notes', 'delete-transaction']}
value={op}
onChange={onChange}
/>
Expand Down Expand Up @@ -491,7 +491,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
) : op === 'prepend-notes' || op === 'append-notes' ? (
<>
<OpSelect
ops={['set', 'prepend-notes', 'append-notes']}
ops={['set', 'prepend-notes', 'append-notes', 'delete-transaction']}
value={op}
onChange={onChange}
/>
Expand All @@ -507,17 +507,28 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
/>
</View>
</>
) : op === 'delete-transaction' ? (
<OpSelect
ops={['set', 'prepend-notes', 'append-notes', 'delete-transaction']}
value={op}
onChange={onChange}
/>
) : null}

<Stack direction="row">
<EditorButtons
onAdd={onAdd}
onDelete={
(op === 'set' || op === 'prepend-notes' || op === 'append-notes') &&
onDelete
}
/>
</Stack>
{op !== 'delete-transaction' && (
<Stack direction="row">
<EditorButtons
onAdd={onAdd}
onDelete={
(op === 'set' ||
op === 'prepend-notes' ||
op === 'append-notes' ||
op === 'delete-transaction') &&
onDelete
}
/>
</Stack>
)}
</Editor>
);
}
Expand Down Expand Up @@ -1182,6 +1193,7 @@ export function EditRuleModal({
'link-schedule',
'prepend-notes',
'append-notes',
'delete-transaction',
]}
action={action}
editorStyle={styles.editorPill}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ export function ImportTransactionsModal({
// (reconciled transactions or no change detected)
current_trx.ignored = entry?.ignored || false;

current_trx.tombstone = entry?.tombstone || false;

current_trx.selected = !current_trx.ignored;
current_trx.selected_merge = current_trx.existing;

Expand Down Expand Up @@ -1056,7 +1058,10 @@ export function ImportTransactionsModal({
autoFocus
isDisabled={
transactions?.filter(
trans => !trans.isMatchedTransaction && trans.selected,
trans =>
!trans.isMatchedTransaction &&
trans.selected &&
!trans.tombstone,
).length === 0
}
isLoading={loadingState === 'importing'}
Expand All @@ -1067,7 +1072,10 @@ export function ImportTransactionsModal({
Import{' '}
{
transactions?.filter(
trans => !trans.isMatchedTransaction && trans.selected,
trans =>
!trans.isMatchedTransaction &&
trans.selected &&
!trans.tombstone,
).length
}{' '}
{t('transactions')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ export function Transaction({
backgroundColor: theme.tableBackground,
color:
(transaction.isMatchedTransaction && !transaction.selected_merge) ||
!transaction.selected
!transaction.selected ||
transaction.tombstone
? theme.tableTextInactive
: theme.tableText,
}}
Expand All @@ -107,62 +108,70 @@ export function Transaction({
{!transaction.isMatchedTransaction && (
<Tooltip
content={
!transaction.existing && !transaction.ignored
? 'New transaction. You can import it, or skip it.'
: transaction.ignored
? 'Already imported transaction. You can skip it, or import it again.'
: transaction.existing
? 'Updated transaction. You can update it, import it again, or skip it.'
: ''
transaction.tombstone
? 'This transaction will be deleted by Rules'
: !transaction.existing && !transaction.ignored
? 'New transaction. You can import it, or skip it.'
: transaction.ignored
? 'Already imported transaction. You can skip it, or import it again.'
: transaction.existing
? 'Updated transaction. You can update it, import it again, or skip it.'
: ''
}
placement="right top"
>
<Checkbox
checked={transaction.selected}
checked={transaction.selected && !transaction.tombstone}
onChange={() => onCheckTransaction(transaction.trx_id)}
style={
transaction.selected_merge
transaction.tombstone
? {
':checked': {
'::after': {
background:
theme.checkboxBackgroundSelected +
// update sign from packages/desktop-client/src/icons/v1/layer.svg
// eslint-disable-next-line rulesdir/typography
' url(\'data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="white" d="M10 1l10 6-10 6L0 7l10-6zm6.67 10L20 13l-10 6-10-6 3.33-2L10 15l6.67-4z" /></svg>\') 9px 9px',
},
'&': {
background: theme.buttonNormalDisabledBorder,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this better if there are better ways to set this checkbox up

},
}
: {
'&': {
border:
'1px solid ' + theme.buttonNormalDisabledBorder,
backgroundColor: theme.buttonNormalDisabledBorder,
'::after': {
display: 'block',
background:
theme.buttonNormalDisabledBorder +
// minus sign adapted from packages/desktop-client/src/icons/v1/add.svg
// eslint-disable-next-line rulesdir/typography
' url(\'data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" className="path" d="M23,11.5 L23,11.5 L23,11.5 C23,12.3284271 22.3284271,13 21.5,13 L1.5,13 L1.5,13 C0.671572875,13 1.01453063e-16,12.3284271 0,11.5 L0,11.5 L0,11.5 C-1.01453063e-16,10.6715729 0.671572875,10 1.5,10 L21.5,10 L21.5,10 C22.3284271,10 23,10.6715729 23,11.5 Z" /></svg>\') 9px 9px',
width: 9,
height: 9,
// eslint-disable-next-line rulesdir/typography
content: '" "',
: transaction.selected_merge
? {
':checked': {
'::after': {
background:
theme.checkboxBackgroundSelected +
// update sign from packages/desktop-client/src/icons/v1/layer.svg
// eslint-disable-next-line rulesdir/typography
' url(\'data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="white" d="M10 1l10 6-10 6L0 7l10-6zm6.67 10L20 13l-10 6-10-6 3.33-2L10 15l6.67-4z" /></svg>\') 9px 9px',
},
},
},
':checked': {
border: '1px solid ' + theme.checkboxBorderSelected,
backgroundColor: theme.checkboxBackgroundSelected,
'::after': {
background:
theme.checkboxBackgroundSelected +
// plus sign from packages/desktop-client/src/icons/v1/add.svg
}
: {
'&': {
border:
'1px solid ' + theme.buttonNormalDisabledBorder,
backgroundColor: theme.buttonNormalDisabledBorder,
'::after': {
display: 'block',
background:
theme.buttonNormalDisabledBorder +
// minus sign adapted from packages/desktop-client/src/icons/v1/add.svg
// eslint-disable-next-line rulesdir/typography
' url(\'data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" className="path" d="M23,11.5 L23,11.5 L23,11.5 C23,12.3284271 22.3284271,13 21.5,13 L1.5,13 L1.5,13 C0.671572875,13 1.01453063e-16,12.3284271 0,11.5 L0,11.5 L0,11.5 C-1.01453063e-16,10.6715729 0.671572875,10 1.5,10 L21.5,10 L21.5,10 C22.3284271,10 23,10.6715729 23,11.5 Z" /></svg>\') 9px 9px',
width: 9,
height: 9,
// eslint-disable-next-line rulesdir/typography
' url(\'data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" className="path" d="M23,11.5 L23,11.5 L23,11.5 C23,12.3284271 22.3284271,13 21.5,13 L1.5,13 L1.5,13 C0.671572875,13 1.01453063e-16,12.3284271 0,11.5 L0,11.5 L0,11.5 C-1.01453063e-16,10.6715729 0.671572875,10 1.5,10 L21.5,10 L21.5,10 C22.3284271,10 23,10.6715729 23,11.5 Z" /><path fill="white" className="path" d="M11.5,23 C10.6715729,23 10,22.3284271 10,21.5 L10,1.5 C10,0.671572875 10.6715729,1.52179594e-16 11.5,0 C12.3284271,-1.52179594e-16 13,0.671572875 13,1.5 L13,21.5 C13,22.3284271 12.3284271,23 11.5,23 Z" /></svg>\') 9px 9px',
content: '" "',
},
},
},
}
':checked': {
border: '1px solid ' + theme.checkboxBorderSelected,
backgroundColor: theme.checkboxBackgroundSelected,
'::after': {
background:
theme.checkboxBackgroundSelected +
// plus sign from packages/desktop-client/src/icons/v1/add.svg
// eslint-disable-next-line rulesdir/typography
' url(\'data:image/svg+xml; utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" className="path" d="M23,11.5 L23,11.5 L23,11.5 C23,12.3284271 22.3284271,13 21.5,13 L1.5,13 L1.5,13 C0.671572875,13 1.01453063e-16,12.3284271 0,11.5 L0,11.5 L0,11.5 C-1.01453063e-16,10.6715729 0.671572875,10 1.5,10 L21.5,10 L21.5,10 C22.3284271,10 23,10.6715729 23,11.5 Z" /><path fill="white" className="path" d="M11.5,23 C10.6715729,23 10,22.3284271 10,21.5 L10,1.5 C10,0.671572875 10.6715729,1.52179594e-16 11.5,0 C12.3284271,-1.52179594e-16 13,0.671572875 13,1.5 L13,21.5 C13,22.3284271 12.3284271,23 11.5,23 Z" /></svg>\') 9px 9px',
},
},
}
}
/>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type SetRuleActionEntity,
type AppendNoteRuleActionEntity,
type PrependNoteRuleActionEntity,
type DeleteTransactionRuleActionEntity,
} from 'loot-core/types/models';

import { ScheduleValue } from './ScheduleValue';
Expand Down Expand Up @@ -56,6 +57,8 @@ export function ActionExpression({ style, ...props }: ActionExpressionProps) {
<PrependNoteActionExpression {...props} />
) : props.op === 'append-notes' ? (
<AppendNoteActionExpression {...props} />
) : props.op === 'delete-transaction' ? (
<DeleteTransactionActionExpression {...props} />
) : null}
</View>
);
Expand Down Expand Up @@ -139,3 +142,9 @@ function AppendNoteActionExpression({ op, value }: AppendNoteRuleActionEntity) {
</>
);
}

function DeleteTransactionActionExpression({
op,
}: DeleteTransactionRuleActionEntity) {
return <Text>{friendlyOp(op)}</Text>;
}
9 changes: 9 additions & 0 deletions packages/loot-core/src/server/accounts/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ export type ReconcileTransactionsResult = {
transaction: TransactionEntity;
existing?: TransactionEntity;
ignored?: boolean;
tombstone?: boolean;
}>;
};

Expand Down Expand Up @@ -565,6 +566,14 @@ export async function reconcileTransactions(
updated.push({ id: child.id, cleared: updates.cleared });
}
}
} else if (trans.tombstone) {
if (isPreview) {
updatedPreview.push({
transaction: trans,
existing: false,
tombstone: true,
});
}
} else {
// Insert a new transaction
const { forceAddTransaction, ...newTrans } = trans;
Expand Down
14 changes: 8 additions & 6 deletions packages/loot-core/src/server/rules/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ function validateRule(rule: Partial<RuleEntity>) {
);

const actionErrors = runValidation(rule.actions, action =>
action.op === 'set-split-amount'
? new Action(action.op, null, action.value, action.options)
: action.op === 'link-schedule'
? new Action(action.op, null, action.value, null)
: action.op === 'prepend-notes' || action.op === 'append-notes'
action.op === 'delete-transaction'
? new Action(action.op, null, null, null)
: action.op === 'set-split-amount'
? new Action(action.op, null, action.value, action.options)
: action.op === 'link-schedule'
? new Action(action.op, null, action.value, null)
: new Action(action.op, action.field, action.value, action.options),
: action.op === 'prepend-notes' || action.op === 'append-notes'
? new Action(action.op, null, action.value, null)
: new Action(action.op, action.field, action.value, action.options),
);

if (conditionErrors || actionErrors) {
Expand Down
4 changes: 4 additions & 0 deletions packages/loot-core/src/server/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ const ACTION_OPS = [
'link-schedule',
'prepend-notes',
'append-notes',
'delete-transaction',
] as const;
type ActionOperator = (typeof ACTION_OPS)[number];

Expand Down Expand Up @@ -742,6 +743,9 @@ export class Action {
? object[this.field] + this.value
: this.value;
break;
case 'delete-transaction':
object['tombstone'] = 1;
break;
default:
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,8 @@ export async function applyActions(
action.op === 'append-notes'
) {
return new Action(action.op, null, action.value, null);
} else if (action.op === 'delete-transaction') {
return new Action(action.op, null, null, null);
}

return new Action(
Expand Down
2 changes: 2 additions & 0 deletions packages/loot-core/src/shared/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ export function friendlyOp(op, type?) {
return t('is on budget');
case 'offBudget':
return t('is off budget');
case 'delete-transaction':
return 'delete transaction';
default:
return '';
}
Expand Down
8 changes: 7 additions & 1 deletion packages/loot-core/src/types/models/rule.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ export type RuleActionEntity =
| SetSplitAmountRuleActionEntity
| LinkScheduleRuleActionEntity
| PrependNoteRuleActionEntity
| AppendNoteRuleActionEntity;
| AppendNoteRuleActionEntity
| DeleteTransactionRuleActionEntity;

export interface SetRuleActionEntity {
field: string;
Expand Down Expand Up @@ -177,3 +178,8 @@ export interface AppendNoteRuleActionEntity {
op: 'append-notes';
value: string;
}

export interface DeleteTransactionRuleActionEntity {
op: 'delete-transaction';
value: string;
}
Loading