fix(transactions): keep saved-filter delete button visible on touch and confirm before deleting (#648)
## Problem
The delete button on a saved filter was only revealed on **hover**. On
desktop that's fine, but on touch devices the button is invisible yet
still clickable, so users kept deleting saved filters by accident. There
was also no confirmation step before a filter was deleted.
## Changes
- **Always visible on touch** — the hover-reveal is now gated behind
`@media (hover: hover)`. On hover-capable devices the trash icon still
appears only on hover; on touch devices (`hover: none`) it stays
visible.
- **Confirm before deleting** — deleting a saved filter now opens an
`AlertDialog` confirmation ("Are you sure you want to delete "…"?"),
matching the existing delete-confirmation pattern used across the app.
Cancel/Escape dismiss without deleting.
## QA
Tested end-to-end in a real browser (mobile emulation, `hover: none`):
- Desktop (`hover: hover`): delete button computes `opacity: 0` until
hover ✓
- Touch (`hover: none`): delete button computes `opacity: 1`, visible
without hover ✓
- Tapping the trash icon opens the confirmation dialog with the correct
filter name ✓
- **Cancel** keeps the filter (verified in DB) ✓
- **Delete** removes it (verified in DB) ✓
## Demo
<img width="1265" height="1277" alt="qa-01-desktop-dropdown"
src="https://github.com/user-attachments/assets/29915c68-0d6e-49da-839d-ee75462338ae"
/>
<img width="1265" height="1277" alt="qa-02-confirm-dialog"
src="https://github.com/user-attachments/assets/a1bee93c-e4fe-4652-a545-35136b427772"
/>
<img width="1265" height="1277" alt="qa-03-after-delete"
src="https://github.com/user-attachments/assets/c7d66a4c-d27a-4d04-8506-05d797d4630f"
/>
<img width="389" height="844" alt="qa-04-mobile-dropdown"
src="https://github.com/user-attachments/assets/9d172e69-3a34-4fdf-a6ce-5771be23f45b"
/>
This commit is contained in:
parent
84b688b7b7
commit
362ac445ea
|
|
@ -118,6 +118,7 @@
|
||||||
"Your bank account is connected.": "Tu cuenta bancaria está conectada.",
|
"Your bank account is connected.": "Tu cuenta bancaria está conectada.",
|
||||||
"A filter with that name already exists": "Ya existe un filtro con ese nombre",
|
"A filter with that name already exists": "Ya existe un filtro con ese nombre",
|
||||||
"Delete saved filter": "Eliminar filtro guardado",
|
"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 delete the saved filter": "No se pudo eliminar el filtro guardado",
|
||||||
"Failed to save the filter": "No se pudo guardar el filtro",
|
"Failed to save the filter": "No se pudo guardar el filtro",
|
||||||
"Failed to update the saved filter": "No se pudo actualizar el filtro guardado",
|
"Failed to update the saved filter": "No se pudo actualizar el filtro guardado",
|
||||||
|
|
|
||||||
|
|
@ -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 { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -36,7 +46,7 @@ import {
|
||||||
Save,
|
Save,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface SavedFilter {
|
interface SavedFilter {
|
||||||
|
|
@ -54,6 +64,15 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) {
|
||||||
const [savedFilters, setSavedFilters] = useState<SavedFilter[]>([]);
|
const [savedFilters, setSavedFilters] = useState<SavedFilter[]>([]);
|
||||||
const [activeId, setActiveId] = useState<UUID | null>(null);
|
const [activeId, setActiveId] = useState<UUID | null>(null);
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
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 [name, setName] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
|
@ -253,11 +272,11 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={__('Delete saved filter')}
|
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) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleDelete(savedFilter);
|
setPendingDelete(savedFilter);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
|
@ -332,6 +351,43 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue