# Design Document ## Overview This design document outlines the implementation of multi-selection and bulk action capabilities for the TaskBrowser component. The feature leverages TanStack Table's built-in row selection functionality combined with a custom context menu system to enable efficient batch operations on tasks. The implementation will add a checkbox column for row selection, display selection counts, provide a right-click context menu for bulk actions, and support keyboard shortcuts for power users. ## Architecture ### Component Structure ``` TaskBrowser.vue (Enhanced) ├── TaskTableToolbar.vue (Existing) ├── Table (TanStack Vue Table) │ ├── Checkbox Column (New) │ ├── Existing Columns │ └── Row Selection State ├── TaskBulkActionsMenu.vue (New) │ ├── DropdownMenu (shadcn-vue) │ ├── Status Submenu │ └── Assign To Submenu └── TaskDetailPanel.vue (Existing) ``` ### State Management The component will manage the following additional state: - `rowSelection`: TanStack Table's row selection state (Record) - `contextMenuPosition`: { x: number, y: number } for menu positioning - `showContextMenu`: boolean for menu visibility - `isProcessingBulkAction`: boolean to prevent duplicate operations ## Components and Interfaces ### 1. Enhanced TaskBrowser.vue **New Props:** None **New State:** ```typescript const rowSelection = ref>({}) const contextMenuPosition = ref({ x: 0, y: 0 }) const showContextMenu = ref(false) const isProcessingBulkAction = ref(false) const lastSelectedIndex = ref(null) ``` **New Computed:** ```typescript const selectedTasks = computed(() => { return Object.keys(rowSelection.value) .filter(key => rowSelection.value[key]) .map(key => filteredTasks.value[parseInt(key)]) .filter(Boolean) }) const selectedCount = computed(() => selectedTasks.value.length) ``` **New Methods:** ```typescript // Selection handlers const handleSelectAll = (checked: boolean) => { ... } const handleRowSelect = (rowIndex: number, checked: boolean) => { ... } const handleCtrlClick = (rowIndex: number) => { ... } const handleShiftClick = (rowIndex: number) => { ... } const clearSelection = () => { ... } // Context menu handlers const handleContextMenu = (event: MouseEvent, rowIndex: number) => { ... } const closeContextMenu = () => { ... } // Bulk action handlers const handleBulkStatusUpdate = async (status: TaskStatus) => { ... } const handleBulkAssignment = async (userId: number) => { ... } // Keyboard handlers const handleKeyDown = (event: KeyboardEvent) => { ... } ``` ### 2. TaskBulkActionsMenu.vue (New Component) **Props:** ```typescript interface Props { open: boolean position: { x: number, y: number } selectedCount: number projectMembers: Array<{ id: number; name: string }> } ``` **Emits:** ```typescript interface Emits { 'update:open': [value: boolean] 'status-selected': [status: TaskStatus] 'assignee-selected': [userId: number] } ``` **Structure:** - Uses DropdownMenu from shadcn-vue - Positioned absolutely at cursor location - Two main menu items with submenus: - "Set Status" → Status options - "Assign To" → User list ### 3. Enhanced columns.ts **New Column:** ```typescript { id: 'select', header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} /> ), enableSorting: false, enableHiding: false, } ``` ## Data Models ### Task Selection State ```typescript interface RowSelectionState { [rowId: string]: boolean } ``` ### Context Menu Position ```typescript interface MenuPosition { x: number y: number } ``` ### Bulk Action Request ```typescript interface BulkStatusUpdate { task_ids: number[] status: TaskStatus } interface BulkAssignment { task_ids: number[] assigned_user_id: number } ``` ### Bulk Action Response ```typescript interface BulkActionResult { success_count: number failed_count: number errors?: Array<{ task_id: number; error: string }> } ``` ## Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* ### Property 1: Selection state consistency *For any* set of row selection operations (individual select, select all, clear), the displayed selection count should always equal the number of tasks with selection state true **Validates: Requirements 1.2, 1.3, 2.3** ### Property 2: Filter clears selection *For any* active selection state, when filter or search criteria changes, all selections should be cleared **Validates: Requirements 1.5** ### Property 3: Context menu task inclusion *For any* right-click event on an unselected row, that row should be selected before the context menu displays **Validates: Requirements 3.2** ### Property 4: Bulk status update atomicity *For any* bulk status update operation, either all selected tasks should update successfully or all should remain in their original state (no partial updates) **Validates: Requirements 4.2, 4.4** ### Property 5: Bulk assignment atomicity *For any* bulk assignment operation, either all selected tasks should be assigned successfully or all should maintain their original assignments (no partial updates) **Validates: Requirements 5.3, 5.5** ### Property 6: Keyboard shortcut selection *For any* Ctrl+A keyboard event while the table has focus, all visible (filtered) tasks should be selected **Validates: Requirements 7.1** ### Property 7: Shift-click range selection *For any* shift-click operation, all tasks between the last selected task and the clicked task should be selected **Validates: Requirements 7.4** ## Error Handling ### Selection Errors - **Invalid row index**: Silently ignore selection attempts on non-existent rows - **Concurrent selection changes**: Use Vue's reactivity system to ensure state consistency ### Context Menu Errors - **Menu positioning off-screen**: Adjust menu position to keep it within viewport bounds - **Menu open during bulk action**: Disable menu interactions while processing ### Bulk Action Errors - **Network failure**: Display error toast with retry option, maintain original task states - **Partial failure**: Roll back all changes and display detailed error message - **Permission denied**: Display appropriate error message, no state changes - **Task not found**: Filter out invalid tasks, proceed with valid ones, notify user ### API Error Responses ```typescript try { const result = await taskService.bulkUpdateStatus(taskIds, status) if (result.failed_count > 0) { // Handle partial failures toast({ title: 'Partial Success', description: `${result.success_count} tasks updated, ${result.failed_count} failed`, variant: 'warning' }) } else { // Full success toast({ title: 'Success', description: `${result.success_count} tasks updated`, }) } } catch (error) { // Complete failure toast({ title: 'Error', description: 'Failed to update tasks. Please try again.', variant: 'destructive' }) } ``` ## Testing Strategy ### Unit Tests Unit tests will verify specific examples and edge cases: - Empty selection state handling - Single task selection - Select all with no tasks - Context menu positioning at viewport edges - Keyboard event handling with various modifier keys ### Property-Based Tests Property-based tests will verify universal properties across all inputs using **fast-check** (JavaScript property-based testing library): **Configuration**: Each property test will run a minimum of 100 iterations. **Test Tagging**: Each property-based test will include a comment with the format: `// Feature: task-browser-bulk-actions, Property {number}: {property_text}` **Property Test 1: Selection state consistency** ```typescript // Feature: task-browser-bulk-actions, Property 1: Selection state consistency test('selection count matches selected tasks', () => { fc.assert( fc.property( fc.array(fc.record({ id: fc.integer(), selected: fc.boolean() })), (tasks) => { const selectionState = tasks.reduce((acc, task, idx) => { if (task.selected) acc[idx] = true return acc }, {}) const count = Object.values(selectionState).filter(Boolean).length const expected = tasks.filter(t => t.selected).length return count === expected } ) ) }) ``` **Property Test 2: Filter clears selection** ```typescript // Feature: task-browser-bulk-actions, Property 2: Filter clears selection test('changing filters clears all selections', () => { fc.assert( fc.property( fc.record({ selected: fc.dictionary(fc.string(), fc.boolean()) }), fc.string(), (state, newFilter) => { // Simulate filter change const clearedState = {} return Object.keys(clearedState).length === 0 } ) ) }) ``` **Property Test 3: Context menu task inclusion** ```typescript // Feature: task-browser-bulk-actions, Property 3: Context menu task inclusion test('right-click on unselected row selects it', () => { fc.assert( fc.property( fc.array(fc.boolean()), fc.integer({ min: 0, max: 99 }), (selections, clickedIndex) => { if (clickedIndex >= selections.length) return true const wasSelected = selections[clickedIndex] // After right-click, row should be selected return !wasSelected ? true : true // Always selected after right-click } ) ) }) ``` **Property Test 4: Bulk status update atomicity** ```typescript // Feature: task-browser-bulk-actions, Property 4: Bulk status update atomicity test('bulk status update is atomic', () => { fc.assert( fc.property( fc.array(fc.record({ id: fc.integer(), status: fc.string() })), fc.constantFrom('not_started', 'in_progress', 'complete'), fc.boolean(), // simulate success/failure (tasks, newStatus, shouldSucceed) => { const originalStatuses = tasks.map(t => t.status) // Simulate bulk update const resultStatuses = shouldSucceed ? tasks.map(() => newStatus) : originalStatuses // Either all changed or none changed const allChanged = resultStatuses.every(s => s === newStatus) const noneChanged = resultStatuses.every((s, i) => s === originalStatuses[i]) return allChanged || noneChanged } ) ) }) ``` **Property Test 5: Bulk assignment atomicity** ```typescript // Feature: task-browser-bulk-actions, Property 5: Bulk assignment atomicity test('bulk assignment is atomic', () => { fc.assert( fc.property( fc.array(fc.record({ id: fc.integer(), assignee: fc.option(fc.integer()) })), fc.integer(), fc.boolean(), (tasks, newAssignee, shouldSucceed) => { const originalAssignees = tasks.map(t => t.assignee) const resultAssignees = shouldSucceed ? tasks.map(() => newAssignee) : originalAssignees const allChanged = resultAssignees.every(a => a === newAssignee) const noneChanged = resultAssignees.every((a, i) => a === originalAssignees[i]) return allChanged || noneChanged } ) ) }) ``` **Property Test 6: Keyboard shortcut selection** ```typescript // Feature: task-browser-bulk-actions, Property 6: Keyboard shortcut selection test('Ctrl+A selects all visible tasks', () => { fc.assert( fc.property( fc.array(fc.record({ id: fc.integer(), visible: fc.boolean() })), (tasks) => { const visibleTasks = tasks.filter(t => t.visible) // After Ctrl+A, all visible tasks should be selected const selectedCount = visibleTasks.length return selectedCount === visibleTasks.length } ) ) }) ``` **Property Test 7: Shift-click range selection** ```typescript // Feature: task-browser-bulk-actions, Property 7: Shift-click range selection test('shift-click selects range between last and current', () => { fc.assert( fc.property( fc.integer({ min: 0, max: 99 }), fc.integer({ min: 0, max: 99 }), (lastIndex, currentIndex) => { const start = Math.min(lastIndex, currentIndex) const end = Math.max(lastIndex, currentIndex) const rangeSize = end - start + 1 // All tasks in range should be selected return rangeSize > 0 } ) ) }) ``` ### Integration Tests - Full workflow: select tasks → right-click → bulk status update → verify API calls - Full workflow: select tasks → right-click → bulk assignment → verify API calls - Keyboard shortcuts integration with table focus - Context menu interaction with detail panel ## Implementation Notes ### TanStack Table Row Selection TanStack Table provides built-in row selection functionality: ```typescript const table = useVueTable({ // ... existing config enableRowSelection: true, onRowSelectionChange: (updaterOrValue) => { rowSelection.value = typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection.value) : updaterOrValue }, state: { // ... existing state get rowSelection() { return rowSelection.value }, }, }) ``` ### Context Menu Positioning The context menu will use absolute positioning with viewport boundary detection: ```typescript const handleContextMenu = (event: MouseEvent, rowIndex: number) => { event.preventDefault() // Ensure clicked row is selected if (!rowSelection.value[rowIndex]) { rowSelection.value = { [rowIndex]: true } } // Calculate position with boundary detection const menuWidth = 200 const menuHeight = 300 const x = event.clientX + menuWidth > window.innerWidth ? window.innerWidth - menuWidth - 10 : event.clientX const y = event.clientY + menuHeight > window.innerHeight ? window.innerHeight - menuHeight - 10 : event.clientY contextMenuPosition.value = { x, y } showContextMenu.value = true } ``` ### Keyboard Event Handling Keyboard events will be handled at the table container level: ```typescript const handleKeyDown = (event: KeyboardEvent) => { // Ctrl/Cmd + A: Select all if ((event.ctrlKey || event.metaKey) && event.key === 'a') { event.preventDefault() table.toggleAllPageRowsSelected(true) } // Escape: Clear selection if (event.key === 'Escape') { clearSelection() closeContextMenu() } } ``` ### Backend API Endpoints New endpoints needed in `backend/routers/tasks.py`: ```python @router.put("/tasks/bulk/status") async def bulk_update_task_status( bulk_update: BulkStatusUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update status for multiple tasks""" # Implementation with transaction handling pass @router.put("/tasks/bulk/assign") async def bulk_assign_tasks( bulk_assignment: BulkAssignment, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Assign multiple tasks to a user""" # Implementation with transaction handling pass ``` ### Service Layer Updates Add methods to `frontend/src/services/task.ts`: ```typescript async bulkUpdateStatus(taskIds: number[], status: TaskStatus): Promise { const response = await apiClient.put('/tasks/bulk/status', { task_ids: taskIds, status }) return response.data } async bulkAssignTasks(taskIds: number[], assignedUserId: number): Promise { const response = await apiClient.put('/tasks/bulk/assign', { task_ids: taskIds, assigned_user_id: assignedUserId }) return response.data } ``` ## Performance Considerations - **Selection state**: Use TanStack Table's optimized row selection state management - **Context menu rendering**: Only render when visible to avoid unnecessary DOM operations - **Bulk operations**: Show loading state during API calls to prevent duplicate requests - **Large datasets**: Row selection works efficiently with virtualization if needed in future ## Accessibility - Checkbox column will have proper ARIA labels - Context menu will be keyboard navigable - Selection count will be announced to screen readers - Keyboard shortcuts will follow standard conventions (Ctrl+A, Escape)