feat: Bulk delete with type-to-confirm modal (#110)

## Summary

- Replaces the simple confirmation dialog for bulk transaction deletion
with a type-to-confirm modal requiring users to type `delete X
transactions` (where X is the count) before the delete button enables
- Removes the admin-only restriction on bulk delete — now available to
all users
- Single-transaction delete keeps the existing simple AlertDialog
This commit is contained in:
Víctor Falcón 2026-02-12 10:53:27 +01:00 committed by GitHub
parent abd7a2f9aa
commit 03fec11705
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 31 deletions

View File

@ -15,7 +15,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { isAdmin } from '@/hooks/use-admin';
import { type Category } from '@/types/category';
import { type Label } from '@/types/label';
import { __ } from '@/utils/i18n';
@ -60,8 +59,6 @@ export function BulkActionsBar({
return null;
}
const isDeleteEnabled = isAdmin();
const displayCount = isSelectingAll ? totalFilteredCount : selectedCount;
const canSelectAll =
!isSelectingAll &&
@ -149,15 +146,13 @@ export function BulkActionsBar({
{__('Re-evaluate rules')}
</DropdownMenuItem>
{isDeleteEnabled && (
<DropdownMenuItem
variant="destructive"
onSelect={onDelete}
>
<Trash2 />
{__('Delete')}
</DropdownMenuItem>
)}
<DropdownMenuItem
variant="destructive"
onSelect={onDelete}
>
<Trash2 />
{__('Delete')}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -47,6 +47,15 @@ import {
import { DataTable } from '@/components/ui/data-table';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import { Spinner } from '@/components/ui/spinner';
import { TableCell, TableRow } from '@/components/ui/table';
@ -393,6 +402,7 @@ export default function Transactions({
const [deleteTransaction, setDeleteTransaction] =
useState<DecryptedTransaction | null>(null);
const [isBulkDeleteMode, setIsBulkDeleteMode] = useState(false);
const [bulkDeleteConfirmation, setBulkDeleteConfirmation] = useState('');
const [isDeleting, setIsDeleting] = useState(false);
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
const [isBulkUpdating, setIsBulkUpdating] = useState(false);
@ -1466,6 +1476,7 @@ export default function Transactions({
);
setDeleteTransaction(null);
setIsBulkDeleteMode(false);
setBulkDeleteConfirmation('');
setRowSelection({});
setRefreshKey((prev) => prev + 1);
@ -1771,11 +1782,10 @@ export default function Transactions({
/>
<AlertDialog
open={!!deleteTransaction}
open={!!deleteTransaction && !isBulkDeleteMode}
onOpenChange={(open) => {
if (!open) {
setDeleteTransaction(null);
setIsBulkDeleteMode(false);
}
}}
>
@ -1783,38 +1793,86 @@ export default function Transactions({
<AlertDialogHeader>
<AlertDialogTitle>
{__('Delete Transaction')}
{isBulkDeleteMode ? 's' : ''}
</AlertDialogTitle>
<AlertDialogDescription>
{isBulkDeleteMode
? `Are you sure you want to delete ${selectedCount} transactions? This action cannot be undone.`
: 'Are you sure you want to delete this transaction? This action cannot be undone.'}
{__(
'Are you sure you want to delete this transaction? This action cannot be undone.',
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
disabled={isDeleting || isBulkDeleting}
>
<AlertDialogCancel disabled={isDeleting}>
{__('Cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={
isBulkDeleteMode
? handleBulkDelete
: handleDelete
}
disabled={isDeleting || isBulkDeleting}
onClick={handleDelete}
disabled={isDeleting}
className="bg-red-600 hover:bg-red-700"
>
{isDeleting || isBulkDeleting
? 'Deleting...'
: 'Delete'}
{isDeleting ? __('Deleting...') : __('Delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Dialog
open={!!deleteTransaction && isBulkDeleteMode}
onOpenChange={(open) => {
if (!open) {
setDeleteTransaction(null);
setIsBulkDeleteMode(false);
setBulkDeleteConfirmation('');
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{__('Delete Transactions')}</DialogTitle>
<DialogDescription>
{__(
'This action cannot be undone. To confirm, type',
)}{' '}
<span className="font-semibold text-foreground">
delete {selectedCount} transactions
</span>
</DialogDescription>
</DialogHeader>
<Input
value={bulkDeleteConfirmation}
onChange={(e) =>
setBulkDeleteConfirmation(e.target.value)
}
placeholder={`delete ${selectedCount} transactions`}
disabled={isBulkDeleting}
autoFocus
/>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setDeleteTransaction(null);
setIsBulkDeleteMode(false);
setBulkDeleteConfirmation('');
}}
disabled={isBulkDeleting}
>
{__('Cancel')}
</Button>
<Button
variant="destructive"
onClick={handleBulkDelete}
disabled={
isBulkDeleting ||
bulkDeleteConfirmation !==
`delete ${selectedCount} transactions`
}
>
{isBulkDeleting ? __('Deleting...') : __('Delete')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<BulkActionsBar
selectedCount={selectedCount}
totalFilteredCount={sortedTransactions.length}