LinkDesk/.kiro/specs/task-browser-refactor/design.md

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 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:

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_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:

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

  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