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 [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const [showReTagDialog, setShowReTagDialog] = useState(false);
const [targetScheduleId, setTargetScheduleId] = useState<string>("");
@ -70,6 +71,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
void queryClient.invalidateQueries({ queryKey });
setShowBulkDeleteConfirm(false);
setSelectedIds(new Set());
setLastSelectedId(null);
},
});
@ -81,6 +83,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
onSuccess: () => {
setShowReTagDialog(false);
setSelectedIds(new Set());
setLastSelectedId(null);
setTargetScheduleId("");
},
});
@ -92,11 +95,63 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
const toggleSelectAll = () => {
if (selectedIds.size === snapshots.length) {
setSelectedIds(new Set());
setLastSelectedId(null);
} else {
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 = () => {
toast.promise(
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")}
onClick={() => handleRowClick(snapshot.short_id)}
>
<TableCell onClick={(e) => e.stopPropagation()}>
<TableCell onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => {
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshot.short_id)) {
newSelected.delete(snapshot.short_id);
} else {
newSelected.add(snapshot.short_id);
}
setSelectedIds(newSelected);
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleSnapshotSelection(snapshot.short_id, e);
}}
aria-label={`Select snapshot ${snapshot.short_id}` as string}
/>
@ -189,7 +239,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
hidden={!backup}
to={backup ? `/backups/$backupId` : "."}
params={backup ? { backupId: backup.shortId } : {}}
onClick={(e) => e.stopPropagation()}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
className="hover:underline"
>
<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">
<div className="flex items-center justify-end gap-2">
<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>
</TableCell>
</TableRow>
@ -234,7 +286,10 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => setSelectedIds(new Set())}
onClick={() => {
setSelectedIds(new Set());
setLastSelectedId(null);
}}
>
<X className="h-4 w-4" />
</Button>
@ -269,8 +324,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<AlertDialogHeader>
<AlertDialogTitle>Delete {selectedIds.size} snapshots?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the selected snapshots and all their data from
the repository.
This action cannot be undone. This will permanently delete the selected snapshots and all
their data from the repository.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@ -291,8 +346,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<DialogHeader>
<DialogTitle>Re-tag snapshots</DialogTitle>
<DialogDescription>
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All {selectedIds.size}{" "}
selected snapshots will be associated with the chosen schedule.
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All{" "}
{selectedIds.size} selected snapshots will be associated with the chosen schedule.
</DialogDescription>
</DialogHeader>
<div className="py-4">