417 lines
14 KiB
Markdown
417 lines
14 KiB
Markdown
# 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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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 `filteredTasks` computed property
|
|
- Handle `@selection-change` event from TasksDataTable
|
|
- Store selected task IDs in local state
|
|
- Compute `selectedTasks` from IDs and filtered tasks
|
|
- Pass selected tasks to bulk action handlers
|
|
- Clear selection when filters change
|
|
|
|
**New State:**
|
|
```typescript
|
|
const selectedTaskIds = ref<Set<number>>(new Set()) // Selected task IDs
|
|
```
|
|
|
|
**Computed:**
|
|
```typescript
|
|
const selectedTasks = computed(() => {
|
|
return filteredTasks.value.filter(task => selectedTaskIds.value.has(task.id))
|
|
})
|
|
|
|
const selectedCount = computed(() => selectedTaskIds.value.size)
|
|
```
|
|
|
|
## Data Models
|
|
|
|
### Task Interface (Existing)
|
|
```typescript
|
|
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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
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_count` in 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 === 2` in 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:**
|
|
|
|
1. **Selection Consistency Property:**
|
|
- Generate random filtered task list and selection state
|
|
- Verify all selected IDs exist in filtered tasks
|
|
|
|
2. **Click Selection Property:**
|
|
- Generate random task list and random row index
|
|
- Simulate single click
|
|
- Verify exactly one task selected
|
|
|
|
3. **Range Selection Property:**
|
|
- Generate random task list and two random indices
|
|
- Simulate shift-click between indices
|
|
- Verify all tasks in range are selected
|
|
|
|
4. **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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
1. Create new file: `frontend/src/components/task/TasksDataTable.vue`
|
|
2. Copy table rendering logic from TaskBrowser
|
|
3. Set up props and emits interfaces
|
|
4. Implement internal selection state management
|
|
|
|
### Phase 2: Update TaskBrowser
|
|
1. Import TasksDataTable component
|
|
2. Replace table template with TasksDataTable component
|
|
3. Update state management to use selectedTaskIds Set
|
|
4. Wire up event handlers from TasksDataTable
|
|
5. Update bulk action handlers to use selectedTasks computed
|
|
|
|
### Phase 3: Testing and Validation
|
|
1. Test all selection scenarios (single, multi, range, select-all)
|
|
2. Test bulk operations (status update, assignment)
|
|
3. Test filter changes clear selection
|
|
4. Test context menu interactions
|
|
5. Verify no regressions in existing functionality
|
|
|
|
### Phase 4: Cleanup
|
|
1. Remove unused code from TaskBrowser
|
|
2. Update any documentation
|
|
3. Verify TypeScript types are correct
|
|
4. Run full test suite
|