-
Notifications
You must be signed in to change notification settings - Fork 0
improve: add master checkbox for bulk selection in manual cleanup #49
Description
Description
Add a master checkbox (tri-state: checked/unchecked/indeterminate) to the header of the DuplicateGroupsPreview component to improve bulk selection UX in manual cleanup mode. This checkbox will provide a standard table-like pattern for selecting all duplicate groups at once, supplementing or replacing the existing "Select All"/"Deselect All" text buttons.
Context
Type: enhancement
Scope: small
Complexity: low
Priority: medium
Current Implementation
The DuplicateGroupsPreview component already has functional bulk selection via text buttons (lines 93-108):
- "Select All" button calls
selectAll()(line 51-53) - "Deselect All" button calls
deselectAll()(line 55-57) - Buttons only visible when
mode === 'manual' && !dryRun
While functional, text buttons are less discoverable and don't provide visual state indication.
Relevant Files
Files identified by multi-agent investigation (ranked by relevance):
| File | Lines | Relevance | Notes |
|---|---|---|---|
app/components/migration/DuplicateGroupsPreview.tsx |
51-57, 93-108, 139-147 | HIGH | Primary file - contains selection state, bulk selection functions, and UI |
app/components/migration/CleanupPanel.tsx |
185-198 | MEDIUM | Parent component - provides mode and dryRun props |
app/hooks/useCleanup.ts |
full file | MEDIUM | State management hook - no changes needed |
lib/migration/cleanup/types.ts |
full file | LOW | Type definitions - reference only |
Analysis
Feature Rationale
UX Benefits of Master Checkbox:
- Discoverability: Checkbox aligned with column header follows universal table patterns (Gmail, file managers, data grids)
- Visual State Indication: Tri-state checkbox immediately communicates selection status:
- Checked: all groups selected
- Unchecked: no groups selected
- Indeterminate: partial selection
- Muscle Memory: Users expect master checkbox pattern in table-like interfaces
- Consistency: Aligns with existing individual checkboxes per group (lines 139-147)
- Reduced Interaction Cost: Single click to toggle all vs. scanning for text buttons
Impact Assessment
Minimal Impact - Supplements Existing Logic:
- Existing
selectAll()anddeselectAll()functions will be reused selectedGroupsstate (Set) remains unchanged- Individual checkboxes continue to work as-is
- No changes needed to useCleanup hook or parent components
- No API or backend changes required
Edge Cases
| Scenario | Groups Count | Selected Count | Master State | Behavior |
|---|---|---|---|---|
| Empty list | 0 | 0 | Hidden/disabled | No interaction |
| Single group, unselected | 1 | 0 | Unchecked | Click → selectAll() |
| Single group, selected | 1 | 1 | Checked | Click → deselectAll() |
| Multiple, all selected | N | N | Checked | Click → deselectAll() |
| Multiple, partial | N | K (0<K<N) | Indeterminate | Click → selectAll() |
Accessibility
Requirements (WAI-ARIA 1.2):
aria-checkedattribute:'true','false', or'mixed'aria-labelor associated label for screen readers- Keyboard navigation support (Tab to focus, Space to toggle)
- Focus indicator visible in light and dark modes
Implementation Pattern:
const masterCheckboxRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (masterCheckboxRef.current) {
masterCheckboxRef.current.indeterminate = isIndeterminate;
}
}, [isIndeterminate]);
<input
ref={masterCheckboxRef}
type="checkbox"
checked={allSelected}
onChange={handleMasterToggle}
aria-label="Select all duplicate groups"
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>Performance
Optimizations Required:
- Use
useMemofor state calculations (allSelected, isIndeterminate) - Use
useRef+useEffectfor indeterminate property (DOM-only, not attribute)
Impact: Negligible - O(1) additional state calculations per render
Technical Approach
Implementation Steps
- Add Derived State and Ref (after line 29):
const masterCheckboxRef = useRef<HTMLInputElement>(null);
const allSelected = groups.length > 0 && selectedGroups.size === groups.length;
const isIndeterminate = selectedGroups.size > 0 && selectedGroups.size < groups.length;- Add Indeterminate Effect (after line 57):
useEffect(() => {
if (masterCheckboxRef.current) {
masterCheckboxRef.current.indeterminate = isIndeterminate;
}
}, [isIndeterminate]);- Add Master Toggle Handler (after useEffect):
const handleMasterToggle = () => {
if (allSelected) {
deselectAll();
} else {
selectAll();
}
};- Replace Text Buttons with Master Checkbox (lines 93-108):
{mode === 'manual' && !dryRun && (
<div className="flex items-center gap-2">
<input
ref={masterCheckboxRef}
type="checkbox"
checked={allSelected}
onChange={handleMasterToggle}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
aria-label="Select all duplicate groups"
/>
<label className="text-sm text-gray-600 dark:text-gray-400 select-none">
Select All
</label>
</div>
)}Alternative Approaches
Option A: Keep Text Buttons + Add Master Checkbox (Redundancy)
- Pros: No UX regression, provides both patterns
- Cons: Cluttered UI, duplicate functionality
Option B: Replace Text Buttons with Master Checkbox (Recommended)
- Pros: Cleaner UI, follows standard patterns
- Cons: None (existing functionality fully preserved)
Acceptance Criteria
- Master checkbox appears in header of DuplicateGroupsPreview component
- Clicking master checkbox when unchecked selects all groups
- Clicking master checkbox when checked deselects all groups
- When some (but not all) groups selected, master shows indeterminate state
- Manually selecting all individual checkboxes updates master to checked
- Manually deselecting one checkbox updates master to indeterminate
- Checkbox only visible when
mode === 'manual' && !dryRun && groups.length > 0 - Keyboard navigation works (Tab to focus, Space to toggle)
- Screen reader announces "Select all duplicate groups" with current state
- Dark mode styling matches existing component theme
- Visual alignment with individual group checkboxes
Testing Requirements
Tri-State Behavior
- Unchecked state: no groups selected
- Checked state: all groups selected
- Indeterminate state: partial selection (dash icon displays)
Interaction
- Click master (unchecked) → all selected, master checked
- Click master (checked) → none selected, master unchecked
- Click master (indeterminate) → all selected, master checked
- Select all individual checkboxes → master auto-updates to checked
- Deselect one checkbox (from all selected) → master auto-updates to indeterminate
Accessibility
- Master checkbox focusable via Tab key
- Master checkbox toggleable via Space bar
- Screen reader announces label and state correctly
- Focus indicator visible in light and dark modes
Edge Cases
- Empty groups (0 groups) → master checkbox hidden/disabled
- Single group → tri-state simplifies to binary (works correctly)
- Mode switch (manual → automated) → master checkbox disappears
- Dry run toggle → master checkbox visibility updates
Additional Considerations
Security
No security implications - purely client-side UI enhancement.
Performance
- Use
useMemoto prevent unnecessary state recalculations useEffectwithisIndeterminatedependency only runs when state changes- No performance regression expected
Documentation
- Update component JSDoc to mention master checkbox
- Update migration workflow user guide (if exists)
Investigation Summary:
- Agents used: 9 (Gemini Flash, Gemini Lite, Codex, OpenCode BigPickle, OpenCode GLM 4.7, OpenCode Grok Code, Codex Scope, Gemini Implementation, Opus Deep Analysis)
- Files identified: 4 (1 HIGH, 2 MEDIUM, 1 LOW relevance)
- Confidence: HIGH
Issue created with multi-agent investigation using Claude Code