Init Repo
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
# Shot Table AJAX Task Status Update Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of AJAX-based task status editing in the shot table view. Previously, task status columns displayed read-only badges. Now they use editable dropdowns that update task status via API calls without page refresh.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Created Shot-Specific EditableTaskStatus Component
|
||||
|
||||
**File**: `frontend/src/components/shot/EditableTaskStatus.vue`
|
||||
|
||||
Created a new component specifically for editing shot task statuses, similar to the existing asset version but adapted for shots:
|
||||
|
||||
**Features**:
|
||||
- Dropdown select with status options
|
||||
- Loading indicator during API calls
|
||||
- Automatic task creation if task doesn't exist
|
||||
- Status update via AJAX
|
||||
- Error handling with status revert
|
||||
- Visual feedback during updates
|
||||
|
||||
**Props**:
|
||||
- `shotId: number` - The shot ID
|
||||
- `taskType: string` - The task type (layout, animation, etc.)
|
||||
- `status: TaskStatus` - Current task status
|
||||
- `taskId?: number | null` - Existing task ID (if task exists)
|
||||
|
||||
**Events**:
|
||||
- `status-updated` - Emitted after successful status update
|
||||
|
||||
**API Calls**:
|
||||
1. `taskService.createShotTask()` - Creates task if it doesn't exist
|
||||
2. `taskService.updateTaskStatus()` - Updates the task status
|
||||
|
||||
### 2. Added createShotTask Method to Task Service
|
||||
|
||||
**File**: `frontend/src/services/task.ts`
|
||||
|
||||
Added new method to create shot tasks:
|
||||
|
||||
```typescript
|
||||
async createShotTask(shotId: number, taskType: string): Promise<TaskStatusInfo> {
|
||||
const response = await apiClient.post(`/shots/${shotId}/tasks?task_type=${taskType}`)
|
||||
return response.data
|
||||
}
|
||||
```
|
||||
|
||||
This mirrors the existing `createAssetTask` method but for shots.
|
||||
|
||||
### 3. Updated Shot Interface
|
||||
|
||||
**File**: `frontend/src/services/shot.ts`
|
||||
|
||||
Added `task_ids` field to Shot interface for easier access to task IDs:
|
||||
|
||||
```typescript
|
||||
export interface Shot {
|
||||
// ... existing fields ...
|
||||
task_ids?: Record<string, number>
|
||||
}
|
||||
```
|
||||
|
||||
This allows the editable component to know if a task already exists and its ID.
|
||||
|
||||
### 4. Updated Shot Table Columns
|
||||
|
||||
**File**: `frontend/src/components/shot/columns.ts`
|
||||
|
||||
**Changes**:
|
||||
1. Imported `EditableTaskStatus` component
|
||||
2. Added `onTaskStatusUpdated` callback to `ShotColumnMeta` interface
|
||||
3. Updated task status column cells to use `EditableTaskStatus` instead of `TaskStatusBadge`
|
||||
|
||||
**Column Cell Implementation**:
|
||||
```typescript
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
const status = shot.task_status?.[taskType] || TaskStatus.NOT_STARTED
|
||||
const taskId = shot.task_ids?.[taskType]
|
||||
|
||||
return h(EditableTaskStatus, {
|
||||
shotId: shot.id,
|
||||
taskType,
|
||||
status,
|
||||
taskId,
|
||||
onStatusUpdated: (shotId: number, taskType: string, newStatus: TaskStatus) => {
|
||||
meta.onTaskStatusUpdated(shotId, taskType, newStatus)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Updated ShotBrowser Component
|
||||
|
||||
**File**: `frontend/src/components/shot/ShotBrowser.vue`
|
||||
|
||||
**Changes**:
|
||||
1. Imported `TaskStatus` enum
|
||||
2. Added `handleTaskStatusUpdated` method
|
||||
3. Passed callback to column meta
|
||||
|
||||
**Handler Implementation**:
|
||||
```typescript
|
||||
const handleTaskStatusUpdated = (shotId: number, taskType: string, newStatus: TaskStatus) => {
|
||||
// Update local state instead of reloading all shots (optimistic update)
|
||||
const shot = shots.value.find(s => s.id === shotId)
|
||||
if (shot) {
|
||||
if (!shot.task_status) {
|
||||
shot.task_status = {}
|
||||
}
|
||||
shot.task_status[taskType] = newStatus
|
||||
}
|
||||
|
||||
// Show success toast
|
||||
toast({
|
||||
title: 'Task status updated',
|
||||
description: `${taskType} status updated successfully`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Key Improvement**: Uses optimistic local state update instead of reloading all shots from the server, matching the asset table behavior for better performance.
|
||||
|
||||
**Column Meta Update**:
|
||||
```typescript
|
||||
const shotColumns = computed(() => {
|
||||
const meta: ShotColumnMeta = {
|
||||
episodes: episodes.value,
|
||||
onEdit: editShot,
|
||||
onDelete: deleteShot,
|
||||
onViewTasks: selectShot,
|
||||
onTaskStatusUpdated: handleTaskStatusUpdated, // Added
|
||||
}
|
||||
return createShotColumns(allTaskTypes.value, meta)
|
||||
})
|
||||
```
|
||||
|
||||
## User Experience Flow
|
||||
|
||||
### Editing Task Status
|
||||
|
||||
1. User clicks on a task status cell in the shot table
|
||||
2. Dropdown opens showing all available statuses
|
||||
3. User selects a new status
|
||||
4. Component shows loading indicator
|
||||
5. If task doesn't exist:
|
||||
- API call creates the task
|
||||
- Task ID is returned
|
||||
6. API call updates the task status
|
||||
7. Success:
|
||||
- Shot list refreshes with new status
|
||||
- Toast notification appears
|
||||
- Loading indicator disappears
|
||||
8. Error:
|
||||
- Status reverts to original
|
||||
- Error is logged to console
|
||||
- Parent component refreshes data
|
||||
|
||||
### Visual Feedback
|
||||
|
||||
- **Loading**: Spinner overlay on the dropdown
|
||||
- **Success**: Toast notification + table refresh
|
||||
- **Error**: Silent revert (status returns to original)
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Added Endpoint: Create Shot Task
|
||||
|
||||
**File**: `backend/routers/shots.py`
|
||||
|
||||
Added new endpoint after `get_shot`:
|
||||
|
||||
```python
|
||||
@router.post("/{shot_id}/tasks", response_model=TaskStatusInfo, status_code=status.HTTP_201_CREATED)
|
||||
async def create_shot_task(
|
||||
shot_id: int,
|
||||
task_type: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_coordinator_or_admin)
|
||||
):
|
||||
"""Create a new task for a shot"""
|
||||
# Validates shot exists
|
||||
# Checks episode access permissions
|
||||
# Returns existing task if already exists (idempotent)
|
||||
# Creates new task with default values
|
||||
# Returns TaskStatusInfo with task_id
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
- Idempotent: Returns existing task if already created
|
||||
- Permission check: Requires coordinator or admin role
|
||||
- Access validation: Checks episode access via shot
|
||||
- Auto-naming: Creates task name as "{shot_name} - {task_type}"
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
### Create Shot Task
|
||||
```
|
||||
POST /shots/{shot_id}/tasks?task_type={task_type}
|
||||
```
|
||||
|
||||
**Response** (201 Created or 200 OK if exists):
|
||||
```json
|
||||
{
|
||||
"task_type": "animation",
|
||||
"status": "not_started",
|
||||
"task_id": 123,
|
||||
"assigned_user_id": null
|
||||
}
|
||||
```
|
||||
|
||||
### Update Task Status
|
||||
```
|
||||
PUT /tasks/{task_id}/status
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"status": "in_progress"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**: Updated task object
|
||||
|
||||
### Get Shots (with task status)
|
||||
```
|
||||
GET /shots/?episode_id={episode_id}
|
||||
```
|
||||
|
||||
**Response**: Array of shots with `task_status` and `task_ids` populated
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **No Page Refresh**: Status updates happen instantly via AJAX
|
||||
2. **Automatic Task Creation**: Tasks are created on-demand when status is first changed
|
||||
3. **Visual Feedback**: Loading indicators and toast notifications
|
||||
4. **Error Handling**: Graceful error handling with status revert
|
||||
5. **Consistent UX**: Matches the asset table behavior
|
||||
6. **Performance**: Only affected shot data is refreshed
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Task Creation Flow
|
||||
|
||||
When a user changes status for a task that doesn't exist:
|
||||
|
||||
1. Component checks if `taskId` prop is provided
|
||||
2. If not, calls `createShotTask(shotId, taskType)`
|
||||
3. Backend creates task with default values
|
||||
4. Returns task ID
|
||||
5. Component then calls `updateTaskStatus(taskId, newStatus)`
|
||||
6. Status is updated
|
||||
7. Parent refreshes to show new data
|
||||
|
||||
### Status Update Flow
|
||||
|
||||
When a user changes status for an existing task:
|
||||
|
||||
1. Component has `taskId` from props
|
||||
2. Directly calls `updateTaskStatus(taskId, newStatus)`
|
||||
3. Status is updated
|
||||
4. Parent refreshes to show new data
|
||||
|
||||
### Data Refresh Strategy
|
||||
|
||||
After status update:
|
||||
- **Optimistic Update**: Local state is updated immediately
|
||||
- Shot's `task_status` object is modified directly
|
||||
- No server reload required
|
||||
- Matches asset table behavior for consistent UX
|
||||
- Much faster than reloading all shots
|
||||
|
||||
## Comparison with Asset Table
|
||||
|
||||
The shot table implementation mirrors the asset table:
|
||||
|
||||
| Feature | Asset Table | Shot Table |
|
||||
|---------|-------------|------------|
|
||||
| Component | `EditableTaskStatus.vue` (asset) | `EditableTaskStatus.vue` (shot) |
|
||||
| Create Method | `createAssetTask()` | `createShotTask()` |
|
||||
| Update Method | `updateTaskStatus()` | `updateTaskStatus()` (same) |
|
||||
| Column Definition | `columns.ts` (asset) | `columns.ts` (shot) |
|
||||
| Parent Handler | `AssetBrowser` | `ShotBrowser` |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
1. **Optimistic Updates**: Update UI immediately, revert on error
|
||||
2. **Batch Updates**: Allow updating multiple tasks at once
|
||||
3. **Undo/Redo**: Add ability to undo status changes
|
||||
4. **Keyboard Shortcuts**: Quick status changes via keyboard
|
||||
5. **Status History**: Track who changed status and when
|
||||
6. **Validation**: Prevent invalid status transitions
|
||||
7. **Permissions**: Check user permissions before allowing edits
|
||||
|
||||
## Testing
|
||||
|
||||
To test the implementation:
|
||||
|
||||
1. Navigate to a project's Shots tab
|
||||
2. Switch to table view
|
||||
3. Find a shot with tasks
|
||||
4. Click on a task status cell
|
||||
5. Select a different status
|
||||
6. Verify:
|
||||
- Loading indicator appears
|
||||
- Status updates in table
|
||||
- Toast notification shows
|
||||
- No page refresh occurs
|
||||
7. Test with a shot that has no tasks:
|
||||
- Change status
|
||||
- Verify task is created
|
||||
- Verify status is set correctly
|
||||
|
||||
## Related Files
|
||||
|
||||
- `frontend/src/components/shot/EditableTaskStatus.vue` - Editable status component
|
||||
- `frontend/src/components/shot/columns.ts` - Column definitions
|
||||
- `frontend/src/components/shot/ShotBrowser.vue` - Parent component
|
||||
- `frontend/src/services/task.ts` - Task service with API methods
|
||||
- `frontend/src/services/shot.ts` - Shot service and types
|
||||
- `frontend/src/components/asset/EditableTaskStatus.vue` - Asset version (reference)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The shot table now supports AJAX-based task status editing, providing a seamless user experience without page refreshes. The implementation follows the same pattern as the asset table, ensuring consistency across the application.
|
||||
Reference in New Issue
Block a user