Init Repo
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# TaskBrowser Bulk Actions Feature Spec
|
||||
|
||||
## Overview
|
||||
|
||||
This spec defines the multi-selection and bulk action capabilities for the TaskBrowser component, enabling users to select multiple tasks and perform batch operations like status updates and assignments through a context menu.
|
||||
|
||||
## Spec Files
|
||||
|
||||
- **requirements.md** - User stories and acceptance criteria following EARS patterns
|
||||
- **design.md** - Technical design with architecture, components, and correctness properties
|
||||
- **tasks.md** - Implementation task list with 14 main tasks
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Multi-selection with checkboxes** - Select individual tasks or all tasks at once
|
||||
2. **Selection count display** - Shows how many tasks are currently selected
|
||||
3. **Right-click context menu** - Access bulk actions via context menu
|
||||
4. **Bulk status updates** - Change status for multiple tasks simultaneously
|
||||
5. **Bulk task assignment** - Assign multiple tasks to a user at once
|
||||
6. **Keyboard shortcuts** - Ctrl+A, Escape, Ctrl+Click, Shift+Click support
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: Vue 3, TanStack Table (row selection), shadcn-vue (DropdownMenu)
|
||||
- **Backend**: FastAPI with new bulk action endpoints
|
||||
- **Testing**: fast-check for property-based testing (optional tasks)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To begin implementation:
|
||||
|
||||
1. Open `tasks.md` in the Kiro IDE
|
||||
2. Click "Start task" next to Task 1 to begin
|
||||
3. Follow the tasks sequentially for best results
|
||||
|
||||
## Status
|
||||
|
||||
✅ Requirements - Approved
|
||||
✅ Design - Approved
|
||||
✅ Tasks - Approved (with optional tests)
|
||||
⏳ Implementation - Ready to start
|
||||
@@ -0,0 +1,563 @@
|
||||
# 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<string, boolean>)
|
||||
- `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<Record<string, boolean>>({})
|
||||
const contextMenuPosition = ref({ x: 0, y: 0 })
|
||||
const showContextMenu = ref(false)
|
||||
const isProcessingBulkAction = ref(false)
|
||||
const lastSelectedIndex = ref<number | null>(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 }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => 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<BulkActionResult> {
|
||||
const response = await apiClient.put('/tasks/bulk/status', {
|
||||
task_ids: taskIds,
|
||||
status
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
async bulkAssignTasks(taskIds: number[], assignedUserId: number): Promise<BulkActionResult> {
|
||||
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)
|
||||
@@ -0,0 +1,95 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
This specification defines the multi-selection and bulk action capabilities for the TaskBrowser component in the VFX Project Management System. The feature enables users to select multiple tasks simultaneously and perform batch operations such as status updates and assignment changes through a context menu interface.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **TaskBrowser**: The data table component that displays tasks in a tabular format with filtering and sorting capabilities
|
||||
- **Multi-selection**: The ability to select multiple rows (tasks) in the data table simultaneously using checkboxes
|
||||
- **Context Menu**: A right-click dropdown menu that appears when tasks are selected, providing bulk action options
|
||||
- **Bulk Action**: An operation that applies to multiple selected tasks simultaneously
|
||||
- **Task Status**: The current state of a task (e.g., Not Started, In Progress, Complete, On Hold)
|
||||
- **Task Assignment**: The association of a task with a specific user who is responsible for completing it
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1
|
||||
|
||||
**User Story:** As a coordinator, I want to select multiple tasks in the TaskBrowser, so that I can perform bulk operations efficiently without updating tasks one by one.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the TaskBrowser loads THEN the system SHALL display a checkbox column as the first column in the data table
|
||||
2. WHEN a user clicks a row checkbox THEN the system SHALL toggle the selection state for that specific task
|
||||
3. WHEN a user clicks the header checkbox THEN the system SHALL toggle selection for all visible tasks in the current filtered view
|
||||
4. WHEN tasks are selected THEN the system SHALL provide visual feedback by highlighting selected rows
|
||||
5. WHEN the filter or search criteria changes THEN the system SHALL clear all current selections
|
||||
|
||||
### Requirement 2
|
||||
|
||||
**User Story:** As a coordinator, I want to see how many tasks I have selected, so that I can confirm the scope of my bulk action before executing it.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN no tasks are selected THEN the system SHALL display the normal task count information
|
||||
2. WHEN one or more tasks are selected THEN the system SHALL display the count of selected tasks prominently
|
||||
3. WHEN tasks are selected THEN the system SHALL update the selection count immediately upon selection changes
|
||||
|
||||
### Requirement 3
|
||||
|
||||
**User Story:** As a coordinator, I want to right-click on selected tasks to open a context menu, so that I can access bulk action options intuitively.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a user right-clicks on a selected task row THEN the system SHALL display a context menu at the cursor position
|
||||
2. WHEN a user right-clicks on an unselected task row THEN the system SHALL select that task and display the context menu
|
||||
3. WHEN the context menu is open and the user clicks outside THEN the system SHALL close the context menu
|
||||
4. WHEN no tasks are selected and the user right-clicks empty space THEN the system SHALL not display the context menu
|
||||
|
||||
### Requirement 4
|
||||
|
||||
**User Story:** As a coordinator, I want to change the status of multiple tasks at once through the context menu, so that I can efficiently update task progress across the project.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the context menu opens THEN the system SHALL display a "Set Status" option with a submenu of available status values
|
||||
2. WHEN a user selects a status from the submenu THEN the system SHALL update all selected tasks to that status
|
||||
3. WHEN the bulk status update completes successfully THEN the system SHALL display a success notification indicating the number of tasks updated
|
||||
4. WHEN the bulk status update fails THEN the system SHALL display an error notification and maintain the original task states
|
||||
5. WHEN the status update completes THEN the system SHALL refresh the task list to reflect the changes
|
||||
|
||||
### Requirement 5
|
||||
|
||||
**User Story:** As a coordinator, I want to assign multiple tasks to a user through the context menu, so that I can efficiently distribute work across the team.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the context menu opens THEN the system SHALL display an "Assign To" option with a submenu of available users
|
||||
2. WHEN the "Assign To" submenu opens THEN the system SHALL display all project members who can be assigned tasks
|
||||
3. WHEN a user selects an assignee from the submenu THEN the system SHALL update all selected tasks to be assigned to that user
|
||||
4. WHEN the bulk assignment completes successfully THEN the system SHALL display a success notification indicating the number of tasks assigned
|
||||
5. WHEN the bulk assignment fails THEN the system SHALL display an error notification and maintain the original assignments
|
||||
6. WHEN the assignment update completes THEN the system SHALL refresh the task list to reflect the changes
|
||||
|
||||
### Requirement 6
|
||||
|
||||
**User Story:** As a coordinator, I want the context menu to close automatically after I perform an action, so that the interface remains clean and I can see the results of my operation.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a user completes a bulk action from the context menu THEN the system SHALL close the context menu automatically
|
||||
2. WHEN a bulk action is in progress THEN the system SHALL disable the context menu options to prevent duplicate operations
|
||||
3. WHEN a bulk action completes THEN the system SHALL clear the task selections
|
||||
|
||||
### Requirement 7
|
||||
|
||||
**User Story:** As a user, I want keyboard shortcuts for selection operations, so that I can work more efficiently without relying solely on mouse interactions.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a user presses Ctrl+A (or Cmd+A on Mac) while focused on the table THEN the system SHALL select all visible tasks
|
||||
2. WHEN a user presses Escape while tasks are selected THEN the system SHALL clear all selections
|
||||
3. WHEN a user clicks a task while holding Ctrl (or Cmd on Mac) THEN the system SHALL toggle that task's selection without affecting other selections
|
||||
4. WHEN a user clicks a task while holding Shift THEN the system SHALL select all tasks between the last selected task and the clicked task
|
||||
@@ -0,0 +1,179 @@
|
||||
# Implementation Plan
|
||||
|
||||
- [x] 1. Set up backend bulk action endpoints
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Create bulk status update endpoint in `backend/routers/tasks.py`
|
||||
- Create bulk assignment endpoint in `backend/routers/tasks.py`
|
||||
- Implement transaction handling for atomicity
|
||||
- Add request/response schemas in `backend/schemas/task.py`
|
||||
- _Requirements: 4.2, 4.4, 5.3, 5.5_
|
||||
|
||||
- [ ]* 1.1 Write property test for bulk status update atomicity
|
||||
- **Property 4: Bulk status update atomicity**
|
||||
- **Validates: Requirements 4.2, 4.4**
|
||||
|
||||
- [ ]* 1.2 Write property test for bulk assignment atomicity
|
||||
- **Property 5: Bulk assignment atomicity**
|
||||
- **Validates: Requirements 5.3, 5.5**
|
||||
|
||||
- [x] 2. Update task service with bulk action methods
|
||||
|
||||
|
||||
|
||||
|
||||
- Add `bulkUpdateStatus` method to `frontend/src/services/task.ts`
|
||||
- Add `bulkAssignTasks` method to `frontend/src/services/task.ts`
|
||||
- Define TypeScript interfaces for bulk action requests and responses
|
||||
- _Requirements: 4.2, 5.3_
|
||||
|
||||
- [x] 3. Add checkbox selection column to TaskBrowser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Update `frontend/src/components/task/columns.ts` to add select column
|
||||
- Implement header checkbox for select all functionality
|
||||
- Implement row checkboxes for individual selection
|
||||
- Ensure checkbox column is not sortable or hideable
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
|
||||
- [ ]* 3.1 Write property test for selection state consistency
|
||||
- **Property 1: Selection state consistency**
|
||||
- **Validates: Requirements 1.2, 1.3, 2.3**
|
||||
|
||||
- [x] 4. Implement row selection state in TaskBrowser
|
||||
|
||||
|
||||
|
||||
|
||||
- Add `rowSelection` state using TanStack Table's row selection
|
||||
- Configure table with `enableRowSelection: true`
|
||||
- Add computed property for `selectedTasks` array
|
||||
- Add computed property for `selectedCount`
|
||||
- Implement visual feedback for selected rows (background highlight)
|
||||
- _Requirements: 1.2, 1.3, 1.4, 2.1, 2.2, 2.3_
|
||||
|
||||
- [x] 5. Implement selection count display
|
||||
|
||||
|
||||
- Update task count display area to show selection count when tasks are selected
|
||||
- Show format: "X tasks selected" when selection is active
|
||||
- Show normal count when no selection
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
|
||||
- [x] 6. Implement filter-based selection clearing
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Add watchers for filter changes (status, type, episode, assignee, context, search)
|
||||
- Clear `rowSelection` state when any filter changes
|
||||
- _Requirements: 1.5_
|
||||
|
||||
- [ ]* 6.1 Write property test for filter clears selection
|
||||
- **Property 2: Filter clears selection**
|
||||
- **Validates: Requirements 1.5**
|
||||
|
||||
- [x] 7. Create TaskBulkActionsMenu component
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Create new component at `frontend/src/components/task/TaskBulkActionsMenu.vue`
|
||||
- Use DropdownMenu from shadcn-vue for base structure
|
||||
- Implement absolute positioning based on cursor coordinates
|
||||
- Add viewport boundary detection for menu positioning
|
||||
- Create "Set Status" menu item with status submenu
|
||||
- Create "Assign To" menu item with user list submenu
|
||||
- Emit events for status-selected and assignee-selected
|
||||
- _Requirements: 3.1, 3.3, 4.1, 5.1, 5.2_
|
||||
|
||||
- [x] 8. Implement context menu trigger in TaskBrowser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Add `@contextmenu` event handler to table rows
|
||||
- Implement `handleContextMenu` method to position and show menu
|
||||
- Ensure right-clicked unselected row gets selected before menu shows
|
||||
- Add click-outside handler to close context menu
|
||||
- Prevent context menu on empty table areas
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
||||
|
||||
- [ ]* 8.1 Write property test for context menu task inclusion
|
||||
- **Property 3: Context menu task inclusion**
|
||||
- **Validates: Requirements 3.2**
|
||||
|
||||
- [x] 9. Implement bulk status update action
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Create `handleBulkStatusUpdate` method in TaskBrowser
|
||||
- Extract selected task IDs from selection state
|
||||
- Call `taskService.bulkUpdateStatus` with task IDs and new status
|
||||
- Show loading state during operation
|
||||
- Display success toast with count of updated tasks
|
||||
- Handle errors and display error toast
|
||||
- Refresh task list after successful update
|
||||
- Close context menu and clear selection after completion
|
||||
- _Requirements: 4.2, 4.3, 4.4, 4.5, 6.1, 6.3_
|
||||
|
||||
- [x] 10. Implement bulk assignment action
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Create `handleBulkAssignment` method in TaskBrowser
|
||||
- Extract selected task IDs from selection state
|
||||
- Call `taskService.bulkAssignTasks` with task IDs and user ID
|
||||
- Show loading state during operation
|
||||
- Display success toast with count of assigned tasks
|
||||
- Handle errors and display error toast
|
||||
- Refresh task list after successful update
|
||||
- Close context menu and clear selection after completion
|
||||
- _Requirements: 5.3, 5.4, 5.5, 5.6, 6.1, 6.3_
|
||||
|
||||
- [ ] 11. Implement keyboard shortcuts
|
||||
- Add `@keydown` event handler to table container
|
||||
- Implement Ctrl+A (Cmd+A on Mac) to select all visible tasks
|
||||
- Implement Escape to clear selection and close context menu
|
||||
- Implement Ctrl+Click (Cmd+Click on Mac) for toggle selection
|
||||
- Implement Shift+Click for range selection
|
||||
- Track `lastSelectedIndex` for range selection
|
||||
- _Requirements: 7.1, 7.2, 7.3, 7.4_
|
||||
|
||||
- [ ]* 11.1 Write property test for Ctrl+A selection
|
||||
- **Property 6: Keyboard shortcut selection**
|
||||
- **Validates: Requirements 7.1**
|
||||
|
||||
- [ ]* 11.2 Write property test for Shift-click range selection
|
||||
- **Property 7: Shift-click range selection**
|
||||
- **Validates: Requirements 7.4**
|
||||
|
||||
- [ ] 12. Add loading and disabled states
|
||||
- Add `isProcessingBulkAction` state flag
|
||||
- Disable context menu options during bulk operations
|
||||
- Show loading spinner or disabled state in menu
|
||||
- Prevent duplicate operations while processing
|
||||
- _Requirements: 6.2_
|
||||
|
||||
- [ ]* 13. Write unit tests for edge cases
|
||||
- Test empty selection state handling
|
||||
- Test single task selection
|
||||
- Test select all with no tasks
|
||||
- Test context menu positioning at viewport edges
|
||||
- Test keyboard event handling with various modifier keys
|
||||
|
||||
- [ ] 14. Final checkpoint - Ensure all tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
Reference in New Issue
Block a user