Skip to content

Commit ea177bc

Browse files
committed
Rework inbox bulk selection UX
Made-with: Cursor
1 parent da7746d commit ea177bc

File tree

5 files changed

+264
-134
lines changed

5 files changed

+264
-134
lines changed

apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useInboxReportSelectionStore } from "@features/inbox/stores/inboxReport
1111
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
1212
import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore";
1313
import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore";
14+
import { getEffectiveBulkSelectionIds } from "@features/inbox/utils/bulkSelection";
1415
import {
1516
buildSignalReportListOrdering,
1617
buildStatusFilterParam,
@@ -110,11 +111,26 @@ export function InboxSignalsTab() {
110111
const selectedReportIds = useInboxReportSelectionStore(
111112
(s) => s.selectedReportIds ?? [],
112113
);
114+
const setSelectedReportIds = useInboxReportSelectionStore(
115+
(s) => s.setSelectedReportIds,
116+
);
113117
const toggleReportSelection = useInboxReportSelectionStore(
114118
(s) => s.toggleReportSelection,
115119
);
116120
const pruneSelection = useInboxReportSelectionStore((s) => s.pruneSelection);
117121

122+
// When true, an empty store means "nothing selected" — no virtual fallback.
123+
// Set once the user first explicitly interacts with any checkbox.
124+
// Resets when the open report changes while the store is still empty (fresh report).
125+
const [selectionExplicitlyActivated, setSelectionExplicitlyActivated] =
126+
useState(false);
127+
128+
// Stable refs so callbacks don't need re-registration on every render
129+
const selectedReportIdsRef = useRef(selectedReportIds);
130+
selectedReportIdsRef.current = selectedReportIds;
131+
const selectionExplicitlyActivatedRef = useRef(false);
132+
selectionExplicitlyActivatedRef.current = selectionExplicitlyActivated;
133+
118134
useEffect(() => {
119135
if (reports.length === 0) {
120136
setSelectedReportId(null);
@@ -135,11 +151,79 @@ export function InboxSignalsTab() {
135151
pruneSelection(reports.map((report) => report.id));
136152
}, [reports, pruneSelection]);
137153

154+
// Reset to virtual mode when a different report is opened while the store is empty.
155+
// selectedReportIdsRef is read (not declared) in the callback — biome can't see it
156+
// depends on selectedReportId, so the dep is intentional.
157+
// biome-ignore lint/correctness/useExhaustiveDependencies: selectedReportId is the trigger; store length is read via a ref to avoid adding it as a dep
158+
useEffect(() => {
159+
if (selectedReportIdsRef.current.length === 0) {
160+
setSelectionExplicitlyActivated(false);
161+
}
162+
}, [selectedReportId]);
163+
138164
const selectedReport = useMemo(
139165
() => reports.find((report) => report.id === selectedReportId) ?? null,
140166
[reports, selectedReportId],
141167
);
142168

169+
const effectiveBulkIds = useMemo(
170+
() =>
171+
getEffectiveBulkSelectionIds(
172+
selectedReportIds,
173+
selectedReportId,
174+
selectionExplicitlyActivated,
175+
),
176+
[selectedReportIds, selectedReportId, selectionExplicitlyActivated],
177+
);
178+
179+
// Toggle a report's checkbox, handling the virtual → explicit mode transition.
180+
// When the first explicit toggle happens in virtual mode (store empty, a report is open):
181+
// - toggling a DIFFERENT report: seed the open report into the store too (keep it checked)
182+
// - toggling the OPEN report itself: transition to explicit-empty (uncheck it)
183+
const handleToggleReportSelection = useCallback(
184+
(reportId: string) => {
185+
if (
186+
!selectionExplicitlyActivatedRef.current &&
187+
selectedReportIdsRef.current.length === 0
188+
) {
189+
setSelectionExplicitlyActivated(true);
190+
if (
191+
selectedReportIdRef.current !== null &&
192+
reportId !== selectedReportIdRef.current
193+
) {
194+
// Seed the open report + add the newly toggled one
195+
setSelectedReportIds([selectedReportIdRef.current, reportId]);
196+
}
197+
// If toggling the open report's own checkbox, the store stays empty
198+
// and explicit = true → effective = [] (it becomes unchecked)
199+
} else {
200+
toggleReportSelection(reportId);
201+
}
202+
},
203+
[setSelectedReportIds, toggleReportSelection],
204+
);
205+
206+
// Handle the select-all checkbox. Parent owns all state transitions.
207+
const handleToggleSelectAll = useCallback(
208+
(checked: boolean) => {
209+
if (checked) {
210+
setSelectedReportIds(reportsRef.current.map((r) => r.id));
211+
setSelectionExplicitlyActivated(true);
212+
} else {
213+
setSelectedReportIds([]);
214+
if (!selectionExplicitlyActivatedRef.current) {
215+
// Was in virtual mode (open report only virtually selected):
216+
// close the report so there is truly nothing selected.
217+
setSelectedReportId(null);
218+
setSelectionExplicitlyActivated(false);
219+
}
220+
// If already in explicit mode, keep the flag true so the empty store
221+
// means nothing selected — no fallback to the virtual open report.
222+
}
223+
},
224+
[setSelectedReportIds],
225+
);
226+
143227
// ── Sidebar resize ─────────────────────────────────────────────────────
144228
const sidebarWidth = useInboxSignalsSidebarStore((state) => state.width);
145229
const sidebarIsResizing = useInboxSignalsSidebarStore(
@@ -279,14 +363,17 @@ export function InboxSignalsTab() {
279363
} else if (e.key === "ArrowUp") {
280364
e.preventDefault();
281365
navigateReport(-1);
282-
} else if (e.key === " " && selectedReportIdRef.current) {
366+
} else if (
367+
(e.key === " " || e.key === "Enter") &&
368+
selectedReportIdRef.current
369+
) {
283370
e.preventDefault();
284-
toggleReportSelection(selectedReportIdRef.current);
371+
handleToggleReportSelection(selectedReportIdRef.current);
285372
}
286373
};
287374
window.addEventListener("keydown", handler);
288375
return () => window.removeEventListener("keydown", handler);
289-
}, [navigateReport, toggleReportSelection]);
376+
}, [navigateReport, handleToggleReportSelection]);
290377

291378
const searchDisabledReason =
292379
!hasReports && !searchQuery.trim()
@@ -381,6 +468,8 @@ export function InboxSignalsTab() {
381468
readyCount={readyCount}
382469
processingCount={processingCount}
383470
reports={reports}
471+
effectiveBulkIds={effectiveBulkIds}
472+
onToggleSelectAll={handleToggleSelectAll}
384473
/>
385474
</Box>
386475
<ReportListPane
@@ -397,9 +486,9 @@ export function InboxSignalsTab() {
397486
searchQuery={searchQuery}
398487
hasActiveFilters={hasActiveFilters}
399488
selectedReportId={selectedReportId}
400-
selectedReportIds={selectedReportIds}
489+
selectedReportIds={effectiveBulkIds}
401490
onSelectReport={setSelectedReportId}
402-
onToggleReportSelection={toggleReportSelection}
491+
onToggleReportSelection={handleToggleReportSelection}
403492
/>
404493
</Flex>
405494
</ScrollArea>

0 commit comments

Comments
 (0)