fix(transactions): keep saved-filter delete button visible on touch and confirm before deleting

The delete button on a saved filter was only revealed on hover, leaving it
invisible but clickable on touch devices, causing accidental deletions.

- Gate the hover-reveal behind @media (hover: hover) so the button stays
  visible on touch devices while keeping the hover-reveal on desktop.
- Ask for confirmation via an AlertDialog before deleting a saved filter,
  matching the existing delete-confirmation pattern used elsewhere.
This commit is contained in:
Víctor Falcón 2026-07-06 11:31:49 +02:00
parent 84b688b7b7
commit 0c9890cabd
2 changed files with 60 additions and 3 deletions

View File

@ -118,6 +118,7 @@
"Your bank account is connected.": "Tu cuenta bancaria está conectada.",
"A filter with that name already exists": "Ya existe un filtro con ese nombre",
"Delete saved filter": "Eliminar filtro guardado",
"Are you sure you want to delete “:name”? This action cannot be undone.": "¿Seguro que quieres eliminar «:name»? Esta acción no se puede deshacer.",
"Failed to delete the saved filter": "No se pudo eliminar el filtro guardado",
"Failed to save the filter": "No se pudo guardar el filtro",
"Failed to update the saved filter": "No se pudo actualizar el filtro guardado",

View File

@ -1,3 +1,13 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -36,7 +46,7 @@ import {
Save,
Trash2,
} from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'sonner';
interface SavedFilter {
@ -54,6 +64,15 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) {
const [savedFilters, setSavedFilters] = useState<SavedFilter[]>([]);
const [activeId, setActiveId] = useState<UUID | null>(null);
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const [pendingDelete, setPendingDelete] = useState<SavedFilter | null>(
null,
);
// Retain the last name so it stays visible during the close animation,
// once pendingDelete has already been cleared to null.
const pendingDeleteName = useRef('');
if (pendingDelete) {
pendingDeleteName.current = pendingDelete.name;
}
const [name, setName] = useState('');
const [isSaving, setIsSaving] = useState(false);
@ -253,11 +272,11 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) {
<button
type="button"
aria-label={__('Delete saved filter')}
className="shrink-0 rounded p-1 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 hover:text-destructive"
className="shrink-0 rounded p-1 text-muted-foreground transition-opacity hover:text-destructive [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleDelete(savedFilter);
setPendingDelete(savedFilter);
}}
>
<Trash2 className="h-3.5 w-3.5" />
@ -332,6 +351,43 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) {
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog
open={pendingDelete !== null}
onOpenChange={(open) => {
if (!open) {
setPendingDelete(null);
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{__('Delete saved filter')}
</AlertDialogTitle>
<AlertDialogDescription>
{__(
'Are you sure you want to delete “:name”? This action cannot be undone.',
{ name: pendingDeleteName.current },
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{__('Cancel')}</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={() => {
if (pendingDelete) {
handleDelete(pendingDelete);
}
setPendingDelete(null);
}}
>
{__('Delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}