14 KiB
Design Document
Overview
This design outlines the refactoring of the TaskBrowser component to extract table rendering logic into a new TasksDataTable component. The refactor improves code maintainability, reusability, and provides a clearer separation of concerns between filtering/orchestration (TaskBrowser) and table rendering/selection (TasksDataTable).
The key architectural change is moving all TanStack Table logic, column definitions, row selection state, and table event handlers into the new TasksDataTable component, while TaskBrowser retains responsibility for data fetching, filtering, toolbar management, and bulk action coordination.
Architecture
Component Hierarchy
TaskBrowser (Parent)
├── TaskTableToolbar (Existing)
├── TasksDataTable (New - Extracted)
│ ├── Table (shadcn-vue)
│ │ ├── TableHeader
│ │ │ └── Checkbox (Select All)
│ │ └── TableBody
│ │ └── TableRow (Multiple)
│ └── Context Menu Trigger Logic
├── TaskDetailPanel (Existing)
└── TaskBulkActionsMenu (Existing)
Responsibility Distribution
TaskBrowser Responsibilities:
- Fetch tasks, episodes, and project members from API
- Apply filters (status, type, episode, assignee, context, search)
- Manage filter state and toolbar interactions
- Coordinate bulk actions (status update, assignment)
- Display task detail panel (desktop and mobile)
- Show context menu and handle bulk action callbacks
- Display selection count and task count
TasksDataTable Responsibilities:
- Render table with TanStack Table
- Manage row selection state (single, multi, range, select-all)
- Handle row click events (single, double, context menu)
- Emit events for parent component actions
- Apply column visibility settings
- Handle sorting state
- Provide visual feedback for selection and hover states
Components and Interfaces
TasksDataTable Component
Props:
interface TasksDataTableProps {
tasks: Task[] // Filtered tasks to display
columnVisibility: VisibilityState // Column visibility state
projectId: number // For context menu positioning
isLoading?: boolean // Loading state for operations
}
Emits:
interface TasksDataTableEmits {
'row-click': (task: Task) => void // Single click on row
'row-double-click': (task: Task) => void // Double click on row
'context-menu': (event: MouseEvent, tasks: Task[]) => void // Right-click with selected tasks
'selection-change': (taskIds: number[]) => void // Selection state changed
'update:column-visibility': (visibility: VisibilityState) => void // Column visibility changed
}
Internal State:
const sorting = ref<SortingState>([{ id: 'created_at', desc: true }])
const rowSelection = ref<RowSelectionState>({}) // { [taskId: string]: boolean }
const lastClickedIndex = ref<number | null>(null) // For shift-click range selection
TaskBrowser Component (Updated)
Responsibilities After Refactor:
- Manage
filteredTaskscomputed property - Handle
@selection-changeevent from TasksDataTable - Store selected task IDs in local state
- Compute
selectedTasksfrom IDs and filtered tasks - Pass selected tasks to bulk action handlers
- Clear selection when filters change
New State:
const selectedTaskIds = ref<Set<number>>(new Set()) // Selected task IDs
Computed:
const selectedTasks = computed(() => {
return filteredTasks.value.filter(task => selectedTaskIds.value.has(task.id))
})
const selectedCount = computed(() => selectedTaskIds.value.size)
Data Models
Task Interface (Existing)
interface Task {
id: number
name: string
description?: string
task_type: string
status: TaskStatus
shot_id?: number
shot_name?: string
asset_id?: number
asset_name?: string
episode_id?: number
episode_name?: string
assigned_user_id?: number
assigned_user_name?: string
deadline?: string
created_at: string
updated_at: string
}
Selection State Model
// TanStack Table's RowSelectionState
type RowSelectionState = Record<string, boolean>
// Example: { "123": true, "456": true, "789": true }
// Keys are task IDs as strings, values indicate selection
Event Payloads
interface ContextMenuEvent {
event: MouseEvent
tasks: Task[] // Currently selected tasks
}
interface SelectionChangeEvent {
taskIds: number[] // Array of selected task IDs
}
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 filtered tasks and selection state, the selected task IDs should only reference tasks that exist in the current filtered task list Validates: Requirements 2.2
Property 2: Click selection exclusivity
For any row click without modifiers, the resulting selection should contain exactly one task ID (the clicked task) Validates: Requirements 3.1
Property 3: Shift-click range selection
For any two row indices A and B where A < B, shift-clicking from A to B should select all tasks with indices in the range [A, B] inclusive Validates: Requirements 3.3
Property 4: Ctrl-click toggle preservation
For any existing selection state and a Ctrl+click on a row, all previously selected rows (except the clicked row if it was selected) should remain selected Validates: Requirements 3.2
Property 5: Select-all completeness
For any filtered task list, clicking the select-all checkbox when unchecked should result in all visible task IDs being selected Validates: Requirements 3.4
Property 6: Context menu selection preservation
For any selected task set, right-clicking on a selected task should not modify the selection state Validates: Requirements 4.1
Property 7: Context menu selection addition
For any selected task set, right-clicking on an unselected task should add that task to the selection without removing existing selections Validates: Requirements 4.2
Property 8: Filter change selection clearing
For any filter change (status, type, episode, assignee, search), the selection state should be empty after the filter is applied Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5
Property 9: Bulk operation selection preservation
For any bulk operation (status update or assignment), the selection state should remain unchanged after the operation completes successfully Validates: Requirements 4.3
Property 10: Double-click selection isolation
For any row double-click event, the selection state should not be modified by the double-click action itself Validates: Requirements 3.5
Error Handling
Selection State Errors
Invalid Task ID in Selection:
- Detection: When computing selected tasks, filter out IDs that don't exist in filtered tasks
- Recovery: Automatically clean up invalid IDs from selection state
- User Impact: None (transparent cleanup)
Selection State Desynchronization:
- Detection: Watch filtered tasks and validate selection state
- Recovery: Remove selections for tasks no longer in filtered list
- User Impact: Selection may shrink when filters are applied
Bulk Operation Errors
Network Failure During Bulk Update:
- Detection: Catch API errors in bulk action handlers
- Recovery: Display error toast, preserve selection for retry
- User Impact: User can retry the operation with same selection
Partial Bulk Operation Success:
- Detection: Check
success_countin API response - Recovery: Display count of successful updates, refresh task list
- User Impact: User sees which tasks were updated successfully
Event Handling Errors
Context Menu Outside Viewport:
- Detection: Check event coordinates against viewport bounds
- Recovery: Adjust context menu position to stay within viewport
- User Impact: Context menu always visible and accessible
Double-Click Race Condition:
- Detection: Check
event.detail === 2in click handler - Recovery: Skip selection logic when double-click is detected
- User Impact: Double-click opens detail panel without selection changes
Testing Strategy
Unit Tests
TasksDataTable Component:
- Test row selection with single click
- Test row selection with Ctrl+click (toggle)
- Test row selection with Shift+click (range)
- Test select-all checkbox functionality
- Test context menu event emission
- Test selection-change event emission
- Test column visibility updates
- Test sorting functionality
TaskBrowser Component:
- Test filtered tasks computation
- Test selected tasks computation from IDs
- Test selection clearing on filter changes
- Test bulk status update handler
- Test bulk assignment handler
- Test context menu positioning
Integration Tests
Selection Flow:
- Select multiple tasks → verify selection state
- Apply filter → verify selection cleared
- Select tasks → right-click → verify context menu shows
- Perform bulk action → verify tasks updated and selection preserved
Bulk Operations Flow:
- Select tasks → update status → verify API called with correct IDs
- Select tasks → assign user → verify API called with correct IDs
- Bulk operation fails → verify selection preserved
- Bulk operation succeeds → verify task list refreshed
Property-Based Tests
Property-based testing will be used to verify the correctness properties defined above. We will use the fast-check library for TypeScript property-based testing.
Test Configuration:
- Minimum 100 iterations per property test
- Generate random task lists (0-100 tasks)
- Generate random selection states
- Generate random click sequences (with modifiers)
Property Test Examples:
-
Selection Consistency Property:
- Generate random filtered task list and selection state
- Verify all selected IDs exist in filtered tasks
-
Click Selection Property:
- Generate random task list and random row index
- Simulate single click
- Verify exactly one task selected
-
Range Selection Property:
- Generate random task list and two random indices
- Simulate shift-click between indices
- Verify all tasks in range are selected
-
Filter Clearing Property:
- Generate random task list and selection
- Apply random filter change
- Verify selection is empty
Implementation Notes
TanStack Table Configuration
The TasksDataTable will use TanStack Table v8 with Vue 3 composition API:
const table = useVueTable({
get data() { return props.tasks },
get columns() { return columns },
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableRowSelection: true,
getRowId: (row) => String(row.id),
// ... state management
})
Selection State Management
Selection will be managed using TanStack Table's built-in rowSelection state:
// Internal state in TasksDataTable
const rowSelection = ref<RowSelectionState>({})
// Emit changes to parent
watch(rowSelection, (newSelection) => {
const selectedIds = Object.keys(newSelection)
.filter(key => newSelection[key])
.map(key => parseInt(key))
emit('selection-change', selectedIds)
}, { deep: true })
Event Handling Pattern
All user interactions will be handled in TasksDataTable and emitted as events:
// Click handler
const handleRowClick = (task: Task, event: MouseEvent) => {
if (event.detail === 2) return // Let double-click handler take over
// Update internal selection state based on modifiers
updateSelection(task, event)
// Emit single click event
emit('row-click', task)
}
// Double-click handler
const handleRowDoubleClick = (task: Task) => {
emit('row-double-click', task)
}
// Context menu handler
const handleContextMenu = (event: MouseEvent, rowIndex: number) => {
event.preventDefault()
// Update selection if needed
const task = props.tasks[rowIndex]
if (!isSelected(task.id)) {
addToSelection(task.id)
}
// Emit with current selected tasks
const selected = getSelectedTasks()
emit('context-menu', event, selected)
}
Column Visibility Persistence
Column visibility will continue to be persisted in sessionStorage, but the logic will be split:
- TasksDataTable: Emits visibility changes
- TaskBrowser: Persists to sessionStorage and passes back to TasksDataTable
Styling and Visual Feedback
Selection and hover states will use Tailwind classes:
// Row classes
const rowClasses = computed(() => [
'cursor-pointer hover:bg-muted/50 select-none',
isSelected ? 'bg-muted/50' : ''
])
The select-none class prevents text selection during shift-click operations.
Migration Strategy
Phase 1: Create TasksDataTable Component
- Create new file:
frontend/src/components/task/TasksDataTable.vue - Copy table rendering logic from TaskBrowser
- Set up props and emits interfaces
- Implement internal selection state management
Phase 2: Update TaskBrowser
- Import TasksDataTable component
- Replace table template with TasksDataTable component
- Update state management to use selectedTaskIds Set
- Wire up event handlers from TasksDataTable
- Update bulk action handlers to use selectedTasks computed
Phase 3: Testing and Validation
- Test all selection scenarios (single, multi, range, select-all)
- Test bulk operations (status update, assignment)
- Test filter changes clear selection
- Test context menu interactions
- Verify no regressions in existing functionality
Phase 4: Cleanup
- Remove unused code from TaskBrowser
- Update any documentation
- Verify TypeScript types are correct
- Run full test suite