From 89174af4e687085651a6e37d7ca7db6129f732fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Sat, 18 Jul 2026 13:03:09 +0200 Subject: [PATCH] feat(transactions): add delete button to the transaction edit modal (#697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .../edit-transaction-dialog.test.tsx | 90 ++++++++++++++++++- .../transactions/edit-transaction-dialog.tsx | 18 ++++ .../transactions/transaction-list.tsx | 1 + resources/js/pages/transactions/index.tsx | 1 + 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/resources/js/components/transactions/edit-transaction-dialog.test.tsx b/resources/js/components/transactions/edit-transaction-dialog.test.tsx index c287bb16..b5dc4277 100644 --- a/resources/js/components/transactions/edit-transaction-dialog.test.tsx +++ b/resources/js/components/transactions/edit-transaction-dialog.test.tsx @@ -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( + , + ); + + 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( + , + ); + + expect( + screen.queryByRole('button', { name: 'Delete' }), + ).not.toBeInTheDocument(); + }); + + it('hides the Delete button in create mode', () => { + render( + , + ); + + expect( + screen.queryByRole('button', { name: 'Delete' }), + ).not.toBeInTheDocument(); + }); }); diff --git a/resources/js/components/transactions/edit-transaction-dialog.tsx b/resources/js/components/transactions/edit-transaction-dialog.tsx index db8a673a..8852db7b 100644 --- a/resources/js/components/transactions/edit-transaction-dialog.tsx +++ b/resources/js/components/transactions/edit-transaction-dialog.tsx @@ -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({ + {mode === 'edit' && onDelete && transaction && ( + + )}