From 13d2c858ccdf6baefe238ccff00ecc8a03c0c74e Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Mon, 27 Apr 2026 03:11:29 +0300 Subject: [PATCH 1/4] Add range selection functionality to repository page --- app/client/components/snapshots-table.tsx | 70 ++++++++++++++++++----- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 56bb00c9..60b81a31 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -52,6 +52,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots const { formatDateTime } = useTimeFormat(); const [selectedIds, setSelectedIds] = useState>(new Set()); + const [lastSelectedId, setLastSelectedId] = useState(null); const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false); const [showReTagDialog, setShowReTagDialog] = useState(false); const [targetScheduleId, setTargetScheduleId] = useState(""); @@ -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,45 @@ 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; + + 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 = () => { toast.promise( 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")} onClick={() => handleRowClick(snapshot.short_id)} > - e.stopPropagation()}> + e.stopPropagation()}> { - const newSelected = new Set(selectedIds); - if (newSelected.has(snapshot.short_id)) { - newSelected.delete(snapshot.short_id); - } else { - newSelected.add(snapshot.short_id); + handleSnapshotSelection(snapshot.short_id); + }} + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + if (e.shiftKey) { + handleSnapshotSelection(snapshot.short_id, e); } - setSelectedIds(newSelected); }} aria-label={`Select snapshot ${snapshot.short_id}` as string} /> @@ -189,7 +226,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" > {backup ? backup.name : "-"} @@ -216,7 +253,9 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
- {formatDuration(snapshot.duration / 1000)} + + {formatDuration(snapshot.duration / 1000)} +
@@ -234,7 +273,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); + }} > @@ -269,8 +311,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots Delete {selectedIds.size} snapshots? - 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. @@ -291,8 +333,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots Re-tag snapshots - 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.
From 4ae9a1f7e39f0fd0b54db75f4d85f0d50b9bae0d Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Mon, 27 Apr 2026 13:57:30 +0300 Subject: [PATCH 2/4] Fix two issues identified by coderabbit --- app/client/components/snapshots-table.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 60b81a31..273c11ed 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -119,6 +119,15 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots const newSelected = new Set(selectedIds); rangeIds.forEach((id) => newSelected.add(id)); setSelectedIds(newSelected); + } else { + // Fallback to single-toggle when range selection fails due to stale lastSelectedId + const newSelected = new Set(selectedIds); + if (newSelected.has(snapshotId)) { + newSelected.delete(snapshotId); + } else { + newSelected.add(snapshotId); + } + setSelectedIds(newSelected); } } else { // Single selection toggle @@ -202,14 +211,9 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots e.stopPropagation()}> { - handleSnapshotSelection(snapshot.short_id); - }} onClick={(e: React.MouseEvent) => { e.stopPropagation(); - if (e.shiftKey) { - handleSnapshotSelection(snapshot.short_id, e); - } + handleSnapshotSelection(snapshot.short_id, e); }} aria-label={`Select snapshot ${snapshot.short_id}` as string} /> From 02934e0426a7366b0f38ef9d3fcfd0eb382d71b3 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Tue, 28 Apr 2026 23:27:23 +0300 Subject: [PATCH 3/4] fix one more coderabbit identified issue --- app/client/components/snapshots-table.tsx | 31 +++++++++-------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 273c11ed..7962aa42 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -105,12 +105,13 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots const handleSnapshotSelection = (snapshotId: string, event?: React.MouseEvent | React.KeyboardEvent) => { const isShiftClick = event && "shiftKey" in event && event.shiftKey; + // Attempt range selection first 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) { + // Valid range selection 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)); @@ -119,27 +120,19 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots const newSelected = new Set(selectedIds); rangeIds.forEach((id) => newSelected.add(id)); setSelectedIds(newSelected); - } else { - // Fallback to single-toggle when range selection fails due to stale lastSelectedId - const newSelected = new Set(selectedIds); - if (newSelected.has(snapshotId)) { - newSelected.delete(snapshotId); - } else { - newSelected.add(snapshotId); - } - setSelectedIds(newSelected); + setLastSelectedId(snapshotId); + return; } - } else { - // Single selection toggle - const newSelected = new Set(selectedIds); - if (newSelected.has(snapshotId)) { - newSelected.delete(snapshotId); - } else { - newSelected.add(snapshotId); - } - setSelectedIds(newSelected); } + // 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); }; From 0d13eb20e0e331c5294ae47485f8641097f867e2 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Thu, 4 Jun 2026 00:58:31 +0300 Subject: [PATCH 4/4] Add initial selection, and deselection functionality --- app/client/components/snapshots-table.tsx | 36 ++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 7962aa42..d48299a4 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -106,20 +106,36 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots const isShiftClick = event && "shiftKey" in event && event.shiftKey; // Attempt range selection first - if (isShiftClick && lastSelectedId) { - const lastIndex = snapshots.findIndex((s) => s.short_id === lastSelectedId); + if (isShiftClick && snapshots.length > 0) { const currentIndex = snapshots.findIndex((s) => s.short_id === snapshotId); - if (lastIndex !== -1 && currentIndex !== -1) { - // Valid range selection - const start = Math.min(lastIndex, currentIndex); - const end = Math.max(lastIndex, currentIndex); + 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)); - // Add selected range to existing selection - const newSelected = new Set(selectedIds); - rangeIds.forEach((id) => newSelected.add(id)); - setSelectedIds(newSelected); + setSelectedIds(rangeIds); setLastSelectedId(snapshotId); return; }