Merge 739d828e22 into 2318b6bdd0
This commit is contained in:
commit
9219b9d397
1 changed files with 71 additions and 16 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue