This commit is contained in:
Raj Dave 2026-06-05 18:10:57 +02:00 committed by GitHub
commit 9219b9d397
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -52,6 +52,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
const { formatDateTime } = useTimeFormat(); const { formatDateTime } = useTimeFormat();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false); const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const [showReTagDialog, setShowReTagDialog] = useState(false); const [showReTagDialog, setShowReTagDialog] = useState(false);
const [targetScheduleId, setTargetScheduleId] = useState<string>(""); const [targetScheduleId, setTargetScheduleId] = useState<string>("");
@ -70,6 +71,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
void queryClient.invalidateQueries({ queryKey }); void queryClient.invalidateQueries({ queryKey });
setShowBulkDeleteConfirm(false); setShowBulkDeleteConfirm(false);
setSelectedIds(new Set()); setSelectedIds(new Set());
setLastSelectedId(null);
}, },
}); });
@ -81,6 +83,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
onSuccess: () => { onSuccess: () => {
setShowReTagDialog(false); setShowReTagDialog(false);
setSelectedIds(new Set()); setSelectedIds(new Set());
setLastSelectedId(null);
setTargetScheduleId(""); setTargetScheduleId("");
}, },
}); });
@ -92,11 +95,63 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
const toggleSelectAll = () => { const toggleSelectAll = () => {
if (selectedIds.size === snapshots.length) { if (selectedIds.size === snapshots.length) {
setSelectedIds(new Set()); setSelectedIds(new Set());
setLastSelectedId(null);
} else { } else {
setSelectedIds(new Set(snapshots.map((s) => s.short_id))); setSelectedIds(new Set(snapshots.map((s) => s.short_id)));
setLastSelectedId(snapshots.length > 0 ? snapshots[snapshots.length - 1].short_id : null);
} }
}; };
const handleSnapshotSelection = (snapshotId: string, event?: React.MouseEvent | React.KeyboardEvent) => {
const isShiftClick = event && "shiftKey" in event && event.shiftKey;
// Attempt range selection first
if (isShiftClick && snapshots.length > 0) {
const currentIndex = snapshots.findIndex((s) => s.short_id === snapshotId);
if (currentIndex !== -1) {
// If lastSelectedId exists, use it; otherwise start from the first item (index 0)
let startIndex: number;
if (lastSelectedId) {
startIndex = snapshots.findIndex((s) => s.short_id === lastSelectedId);
// If lastSelectedId no longer exists in snapshots (stale reference), fall back to single selection
if (startIndex === -1) {
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshotId)) {
newSelected.delete(snapshotId);
} else {
newSelected.add(snapshotId);
}
setSelectedIds(newSelected);
setLastSelectedId(snapshotId);
return;
}
} else {
startIndex = 0;
}
// Valid range selection - replace the entire selection with the new range
const start = Math.min(startIndex, currentIndex);
const end = Math.max(startIndex, currentIndex);
const rangeIds = new Set(snapshots.slice(start, end + 1).map((s) => s.short_id));
setSelectedIds(rangeIds);
setLastSelectedId(snapshotId);
return;
}
}
// Single selection toggle (used as fallback or when shift-click not applicable)
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshotId)) {
newSelected.delete(snapshotId);
} else {
newSelected.add(snapshotId);
}
setSelectedIds(newSelected);
setLastSelectedId(snapshotId);
};
const handleBulkDelete = () => { const handleBulkDelete = () => {
toast.promise( toast.promise(
deleteSnapshots.mutateAsync({ deleteSnapshots.mutateAsync({
@ -162,17 +217,12 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
className={cn("hover:bg-accent/50 cursor-pointer", isSelected && "bg-accent/30")} className={cn("hover:bg-accent/50 cursor-pointer", isSelected && "bg-accent/30")}
onClick={() => handleRowClick(snapshot.short_id)} onClick={() => handleRowClick(snapshot.short_id)}
> >
<TableCell onClick={(e) => e.stopPropagation()}> <TableCell onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
onCheckedChange={() => { onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
const newSelected = new Set(selectedIds); e.stopPropagation();
if (newSelected.has(snapshot.short_id)) { handleSnapshotSelection(snapshot.short_id, e);
newSelected.delete(snapshot.short_id);
} else {
newSelected.add(snapshot.short_id);
}
setSelectedIds(newSelected);
}} }}
aria-label={`Select snapshot ${snapshot.short_id}` as string} aria-label={`Select snapshot ${snapshot.short_id}` as string}
/> />
@ -189,7 +239,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
hidden={!backup} hidden={!backup}
to={backup ? `/backups/$backupId` : "."} to={backup ? `/backups/$backupId` : "."}
params={backup ? { backupId: backup.shortId } : {}} params={backup ? { backupId: backup.shortId } : {}}
onClick={(e) => e.stopPropagation()} onClick={(e: React.MouseEvent) => e.stopPropagation()}
className="hover:underline" className="hover:underline"
> >
<span className="text-sm">{backup ? backup.name : "-"}</span> <span className="text-sm">{backup ? backup.name : "-"}</span>
@ -216,7 +266,9 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<TableCell className="hidden md:table-cell"> <TableCell className="hidden md:table-cell">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<Clock className="h-4 w-4 text-muted-foreground" /> <Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span> <span className="text-sm text-muted-foreground">
{formatDuration(snapshot.duration / 1000)}
</span>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -234,7 +286,10 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 rounded-full" className="h-8 w-8 rounded-full"
onClick={() => setSelectedIds(new Set())} onClick={() => {
setSelectedIds(new Set());
setLastSelectedId(null);
}}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
@ -269,8 +324,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete {selectedIds.size} snapshots?</AlertDialogTitle> <AlertDialogTitle>Delete {selectedIds.size} snapshots?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone. This will permanently delete the selected snapshots and all their data from This action cannot be undone. This will permanently delete the selected snapshots and all
the repository. their data from the repository.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@ -291,8 +346,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<DialogHeader> <DialogHeader>
<DialogTitle>Re-tag snapshots</DialogTitle> <DialogTitle>Re-tag snapshots</DialogTitle>
<DialogDescription> <DialogDescription>
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All {selectedIds.size}{" "} Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All{" "}
selected snapshots will be associated with the chosen schedule. {selectedIds.size} selected snapshots will be associated with the chosen schedule.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4">