feat(transactions): add delete button to the transaction edit modal (#697)

## Why

Until now the only way to delete a transaction was the row's three-dots
menu in the list. Many users click a transaction to open the edit modal
and look for a delete option there — but there wasn't one. This adds a
**Delete** button inside the edit modal, with the same confirmation
step.

## What

- Adds a destructive **Delete** button to the transaction edit modal
footer (`edit-transaction-dialog.tsx`), shown only in **edit** mode.
- Clicking it closes the modal and opens the **existing**
delete-confirmation dialog — the same `AlertDialog` (and manual-account
"update balance" checkbox) already used by the three-dots menu. No new
delete logic, confirmation UI, or copy was introduced.
- Wired via a new optional `onDelete` prop, passed as
`setDeleteTransaction` from the two parents that render the edit modal
(`transaction-list.tsx`, `pages/transactions/index.tsx`). Because
`transaction-list` also backs the Accounts and Budgets pages, the button
appears consistently everywhere the edit modal opens.

## Reviews applied

Ran technical + product reviews on the diff. Applied: added
`dark:hover:bg-destructive/20` to match the sibling delete button's
dark-mode hover. Dropped (with reason): premature shared-component
extraction, pre-existing cross-file duplication, and pre-existing "no
toast on delete failure" — all out of scope for this change.

## QA

Real browser QA (Playwright) against the running app on real data:

- Delete button appears in the edit modal (verified on an imported
transaction).
- Delete → confirmation dialog opens and the edit modal closes.
- **Cancel** returns to the list; page stays fully interactive (`body`
pointer-events `auto` — no stuck Radix scroll-lock).
- **Confirm** removes the row from the list and soft-deletes it in the
DB (verified `deleted_at` was set). Restored afterward.
- Page remains interactive after the stacked-dialog flow (reopened
another transaction cleanly).

Unit tests added in `edit-transaction-dialog.test.tsx` cover: click
delegates to `onDelete` + closes the modal; button hidden without a
handler; button hidden in create mode.

## Demo


https://github.com/user-attachments/assets/e13dad30-0779-46f2-8cc4-84ea70568741
This commit is contained in:
Víctor Falcón 2026-07-18 13:03:09 +02:00 committed by GitHub
parent 2d7e905ddd
commit 89174af4e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 109 additions and 1 deletions

View File

@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import type React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EditTransactionDialog } from './edit-transaction-dialog';
@ -342,4 +342,92 @@ describe('EditTransactionDialog', () => {
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
const manualTransaction = {
id: 'tx-manual',
user_id: 'user-1',
account_id: 'account-1',
category_id: null,
description: 'Groceries',
description_iv: null,
transaction_date: '2026-05-27',
amount: -1200,
currency_code: 'EUR',
notes: null,
notes_iv: null,
creditor_name: null,
debtor_name: null,
source: 'manually_created' as const,
created_at: '2026-05-27T00:00:00Z',
updated_at: '2026-05-27T00:00:00Z',
decryptedDescription: 'Groceries',
decryptedNotes: null,
label_ids: [],
};
it('closes the dialog and asks the parent to delete when Delete is clicked', () => {
const onDelete = vi.fn();
const onOpenChange = vi.fn();
render(
<EditTransactionDialog
transaction={manualTransaction}
categories={[]}
accounts={[checkingAccount]}
banks={[]}
labels={[]}
open
onOpenChange={onOpenChange}
onSuccess={vi.fn()}
onDelete={onDelete}
mode="edit"
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Delete' }));
expect(onOpenChange).toHaveBeenCalledWith(false);
expect(onDelete).toHaveBeenCalledWith(manualTransaction);
});
it('hides the Delete button when no onDelete handler is provided', () => {
render(
<EditTransactionDialog
transaction={manualTransaction}
categories={[]}
accounts={[checkingAccount]}
banks={[]}
labels={[]}
open
onOpenChange={vi.fn()}
onSuccess={vi.fn()}
mode="edit"
/>,
);
expect(
screen.queryByRole('button', { name: 'Delete' }),
).not.toBeInTheDocument();
});
it('hides the Delete button in create mode', () => {
render(
<EditTransactionDialog
transaction={null}
categories={[]}
accounts={[checkingAccount]}
banks={[]}
labels={[]}
open
onOpenChange={vi.fn()}
onSuccess={vi.fn()}
onDelete={vi.fn()}
mode="create"
/>,
);
expect(
screen.queryByRole('button', { name: 'Delete' }),
).not.toBeInTheDocument();
});
});

View File

@ -42,6 +42,7 @@ import { formatDate } from '@/utils/date';
import { __ } from '@/utils/i18n';
import { router } from '@inertiajs/react';
import { getYear, parseISO } from 'date-fns';
import { Trash2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
@ -61,6 +62,7 @@ interface EditTransactionDialogProps {
source: 'edit_transaction_modal',
) => void;
onLabelCreated?: (label: Label) => void;
onDelete?: (transaction: DecryptedTransaction) => void;
mode: 'create' | 'edit';
initialAccountId?: string | null;
}
@ -77,6 +79,7 @@ export function EditTransactionDialog({
onSuccess,
onCategorized,
onLabelCreated,
onDelete,
mode,
initialAccountId = null,
}: EditTransactionDialogProps) {
@ -867,6 +870,21 @@ export function EditTransactionDialog({
</div>
<DialogFooter>
{mode === 'edit' && onDelete && transaction && (
<Button
type="button"
variant="ghost"
onClick={() => {
onOpenChange(false);
onDelete(transaction);
}}
disabled={isSubmitting}
className="text-destructive hover:bg-destructive/10 hover:text-destructive sm:mr-auto dark:hover:bg-destructive/20"
>
<Trash2 />
{__('Delete')}
</Button>
)}
<Button
type="button"
variant="outline"

View File

@ -1111,6 +1111,7 @@ export function TransactionList({
onSuccess={updateTransaction}
onCategorized={showAutomatizeToast}
onLabelCreated={handleLabelCreated}
onDelete={setDeleteTransaction}
mode="edit"
/>

View File

@ -1542,6 +1542,7 @@ export default function Transactions({
onSuccess={updateTransaction}
onCategorized={showAutomatizeToast}
onLabelCreated={handleLabelCreated}
onDelete={setDeleteTransaction}
mode="edit"
/>