Add range selection functionality to repository page
This commit is contained in:
parent
82e027d872
commit
13d2c858cc
1 changed files with 56 additions and 14 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,45 @@ 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;
|
||||||
|
|
||||||
|
if (isShiftClick && lastSelectedId) {
|
||||||
|
// Range selection
|
||||||
|
const lastIndex = snapshots.findIndex((s) => s.short_id === lastSelectedId);
|
||||||
|
const currentIndex = snapshots.findIndex((s) => s.short_id === snapshotId);
|
||||||
|
|
||||||
|
if (lastIndex !== -1 && currentIndex !== -1) {
|
||||||
|
const start = Math.min(lastIndex, currentIndex);
|
||||||
|
const end = Math.max(lastIndex, currentIndex);
|
||||||
|
const rangeIds = new Set(snapshots.slice(start, end + 1).map((s) => s.short_id));
|
||||||
|
|
||||||
|
// Add selected range to existing selection
|
||||||
|
const newSelected = new Set(selectedIds);
|
||||||
|
rangeIds.forEach((id) => newSelected.add(id));
|
||||||
|
setSelectedIds(newSelected);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single selection toggle
|
||||||
|
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 +199,17 @@ 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={() => {
|
onCheckedChange={() => {
|
||||||
const newSelected = new Set(selectedIds);
|
handleSnapshotSelection(snapshot.short_id);
|
||||||
if (newSelected.has(snapshot.short_id)) {
|
}}
|
||||||
newSelected.delete(snapshot.short_id);
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
} else {
|
e.stopPropagation();
|
||||||
newSelected.add(snapshot.short_id);
|
if (e.shiftKey) {
|
||||||
|
handleSnapshotSelection(snapshot.short_id, e);
|
||||||
}
|
}
|
||||||
setSelectedIds(newSelected);
|
|
||||||
}}
|
}}
|
||||||
aria-label={`Select snapshot ${snapshot.short_id}` as string}
|
aria-label={`Select snapshot ${snapshot.short_id}` as string}
|
||||||
/>
|
/>
|
||||||
|
|
@ -189,7 +226,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 +253,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 +273,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 +311,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 +333,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