# Bulk Actions with Pagination - Best Practices


**Last Updated:** 2025-11-20

## Overview

This guide documents the implementation pattern for bulk actions that work seamlessly with pagination, filtering, and search functionality. This pattern was implemented for the Performance Dashboard and can be reused in other data table interfaces.

## Problem Statement

When implementing bulk actions (select all, analyze selected, export selected) in paginated tables, several challenges arise:

1. **Selection State Loss**: Selections are lost when navigating between pages if state is only stored in DOM checkbox elements
2. **Scope Confusion**: Users need clarity on whether "Select All" applies to visible rows, filtered rows, or all rows
3. **Cross-Page Operations**: Bulk actions must work on items selected across multiple pages
4. **State Persistence**: Selection should persist across pagination, filtering, and sorting changes

## Solution Architecture

### Core Components

1. **Persistent Selection State**: Use a JavaScript `Set` to track selected items independently of DOM state
2. **State Synchronization**: Sync checkbox DOM state with persistent state when rows become visible
3. **Scope-Aware Selection**: Support multiple selection scopes (visible, filtered, all)
4. **Session Persistence**: Store selection in sessionStorage for page refresh resilience

### Implementation Pattern

```javascript
// 1. Persistent selection state storage
const selectedPagesSet = new Set();
const SELECTION_STORAGE_KEY = 'performance_dashboard_selection';

// 2. Get selected items (works across all pages)
function getSelectedPages() {
    return Array.from(selectedPagesSet);
}

// 3. Sync checkbox state with persistent state
function syncCheckboxState() {
    // Use requestAnimationFrame for smooth updates
    requestAnimationFrame(() => {
        const visibleCheckboxes = document.querySelectorAll('.page-checkbox:not([style*="display: none"])');
        visibleCheckboxes.forEach(checkbox => {
            const url = checkbox.value;
            checkbox.checked = selectedPagesSet.has(url);
        });
    });
}

// 4. Handle checkbox changes
function handleCheckboxChange(checkbox) {
    const url = checkbox.value;
    if (checkbox.checked) {
        selectedPagesSet.add(url);
    } else {
        selectedPagesSet.delete(url);
    }
    saveSelectionToStorage();
    updateBulkActionsToolbar();
}

// 5. Scope-aware select all
function toggleSelectAllPages(scope = 'visible') {
    let urlsToToggle = [];
    
    if (scope === 'visible') {
        // Only currently visible rows
        const visibleCheckboxes = document.querySelectorAll('.page-checkbox:not([style*="display: none"])');
        urlsToToggle = Array.from(visibleCheckboxes).map(cb => cb.value);
    } else if (scope === 'filtered') {
        // All rows matching current filters (across all pages)
        const allRows = document.querySelectorAll('tr.clickable-row');
        urlsToToggle = Array.from(allRows)
            .filter(row => !row.hasAttribute('data-filtered'))
            .map(row => row.getAttribute('data-url'));
    } else if (scope === 'all') {
        // All rows regardless of filters
        const allCheckboxes = document.querySelectorAll('.page-checkbox');
        urlsToToggle = Array.from(allCheckboxes).map(cb => cb.value);
    }
    
    // Toggle selection
    urlsToToggle.forEach(url => {
        if (shouldSelect) {
            selectedPagesSet.add(url);
        } else {
            selectedPagesSet.delete(url);
        }
    });
    
    syncCheckboxState();
    updateBulkActionsToolbar();
}
```

## Key Functions

### Selection State Management

- **`selectedPagesSet`**: JavaScript Set storing selected item identifiers (URLs, IDs, etc.)
- **`getSelectedPages()`**: Returns array of all selected items (works across all pages)
- **`syncCheckboxState()`**: Syncs DOM checkbox state with persistent selection state
- **`handleCheckboxChange(checkbox)`**: Updates both checkbox and persistent state

### Selection Scopes

- **`toggleSelectAllPages(scope)`**: Select/deselect items based on scope
  - `'visible'`: Only currently visible rows on current page
  - `'filtered'`: All rows matching current filters (across all pages)
  - `'all'`: All rows regardless of filters or pagination

### State Persistence

- **`saveSelectionToStorage()`**: Saves selection to sessionStorage
- **`loadSelectionFromStorage()`**: Restores selection on page load
- **`clearSelection()`**: Clears both persistent state and DOM checkboxes

### Integration Points

- **`updatePagination()`**: Call `syncCheckboxState()` after pagination changes
- **`applyFilters()`**: Call `syncCheckboxState()` after filtering, show notification if selection exists
- **`initializeDashboard()`**: Call `loadSelectionFromStorage()` on page load

## UI Components

### Select All Dropdown

Provide a dropdown menu with three options:
- "Select all visible" - Selects only rows on current page
- "Select all filtered" - Selects all rows matching current filters
- "Select all" - Selects all rows regardless of filters

### Bulk Actions Toolbar

Display:
- Total selected count (including items on other pages)
- Count of selected items on current page
- Selection scope indicator (optional, for advanced users)
- Action buttons (Analyze, Export, Clear)

### Visual Feedback

- Show selected count in pagination info: "Page 2 (3 selected on this page)"
- Highlight selected rows with subtle background color
- Show toast notifications when filters change with existing selection

## Performance Optimizations

1. **Debouncing**: Debounce `syncCheckboxState()` to prevent race conditions (50ms)
2. **RequestAnimationFrame**: Use `requestAnimationFrame` for smooth DOM updates
3. **Selective Syncing**: For large selections (>100 items), only sync visible checkboxes
4. **Progress Indicators**: Show progress for bulk operations on >100 items

## Best Practices

### 1. Always Use Persistent State

❌ **Don't**: Query DOM for checked checkboxes
```javascript
// BAD: Only works for visible items
const selected = document.querySelectorAll('.checkbox:checked');
```

✅ **Do**: Use persistent Set
```javascript
// GOOD: Works across all pages
const selected = Array.from(selectedPagesSet);
```

### 2. Sync State After Visibility Changes

Always call `syncCheckboxState()` after:
- Pagination changes
- Filter changes
- Sort changes
- Any operation that shows/hides rows

### 3. Provide Clear Scope Indicators

Users should understand what "Select All" means:
- Show tooltips explaining each scope
- Display selection breakdown in toolbar
- Show count of selected items on current page

### 4. Handle Edge Cases

- **Deleted Items**: Remove invalid selections when items are deleted
- **Empty States**: Disable "Select All" when no rows match filters
- **Large Selections**: Show progress indicators for >100 items
- **Filter Changes**: Preserve selection but notify user

### 5. Session Persistence

- Save selection to sessionStorage on every change
- Restore on page load
- Clear on explicit "Clear Selection" action
- Handle storage quota errors gracefully

## Testing Checklist

- [ ] Selection persists across pagination
- [ ] Selection persists across filter changes
- [ ] "Select All" works with different scopes
- [ ] Bulk actions work on items across multiple pages
- [ ] Selection count is accurate (includes items on other pages)
- [ ] Visual feedback is clear
- [ ] Performance is acceptable with large selections (>100 items)
- [ ] SessionStorage persistence works
- [ ] Deleted items are removed from selection
- [ ] Keyboard shortcuts work correctly

## Example Usage

See `v2/js/performance-dashboard.js` for complete implementation:
- Lines 1661-1671: Selection state storage
- Lines 2815-2905: Selection functions
- Lines 3023-3120: State synchronization
- Lines 3349-3396: Bulk actions using persistent state

## Related Files

- `v2/pages/performance-dashboard.php`: UI components
- `v2/css/performance-dashboard.css`: Styling for selection UI
- `v2/js/performance-dashboard.js`: Implementation

## Future Enhancements

- Server-side selection persistence (for very large datasets)
- Selection presets (save/load common selections)
- Selection sharing (URL parameters for sharing selected items)
- Undo/redo for selection changes

