Skip to content

improve: add master checkbox for bulk selection in manual cleanup #49

@rdfitted

Description

@rdfitted

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:

  1. Discoverability: Checkbox aligned with column header follows universal table patterns (Gmail, file managers, data grids)
  2. Visual State Indication: Tri-state checkbox immediately communicates selection status:
    • Checked: all groups selected
    • Unchecked: no groups selected
    • Indeterminate: partial selection
  3. Muscle Memory: Users expect master checkbox pattern in table-like interfaces
  4. Consistency: Aligns with existing individual checkboxes per group (lines 139-147)
  5. Reduced Interaction Cost: Single click to toggle all vs. scanning for text buttons

Impact Assessment

Minimal Impact - Supplements Existing Logic:

  • Existing selectAll() and deselectAll() functions will be reused
  • selectedGroups state (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-checked attribute: 'true', 'false', or 'mixed'
  • aria-label or 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 useMemo for state calculations (allSelected, isIndeterminate)
  • Use useRef + useEffect for indeterminate property (DOM-only, not attribute)

Impact: Negligible - O(1) additional state calculations per render

Technical Approach

Implementation Steps

  1. 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;
  1. Add Indeterminate Effect (after line 57):
useEffect(() => {
  if (masterCheckboxRef.current) {
    masterCheckboxRef.current.indeterminate = isIndeterminate;
  }
}, [isIndeterminate]);
  1. Add Master Toggle Handler (after useEffect):
const handleMasterToggle = () => {
  if (allSelected) {
    deselectAll();
  } else {
    selectAll();
  }
};
  1. 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 useMemo to prevent unnecessary state recalculations
  • useEffect with isIndeterminate dependency 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions