From 7eac213c20e3a4048a317764450e22412ceab1b3 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Tue, 17 Mar 2026 04:31:39 +0800 Subject: [PATCH] Wire AssignmentPicker into TaskDetailPanel for task assignment TaskDetailPanel now shows an interactive AssignmentPicker (click to open dropdown, select members, remove with X) when the user has create_modify permission or is the owner. Read-only users see static chips. Owner is included as a synthetic entry in the picker so they can self-assign. Both assign and unassign mutations invalidate the project query for immediate UI refresh. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/projects/ProjectDetail.tsx | 4 ++ .../components/projects/TaskDetailPanel.tsx | 58 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/projects/ProjectDetail.tsx b/frontend/src/components/projects/ProjectDetail.tsx index bf37fa7..5908930 100644 --- a/frontend/src/components/projects/ProjectDetail.tsx +++ b/frontend/src/components/projects/ProjectDetail.tsx @@ -710,6 +710,10 @@ export default function ProjectDetail() { m.user_id === currentUserId && m.permission === 'create_modify')} onDelete={handleDeleteTask} onAddSubtask={(parentId) => openTaskForm(null, parentId)} onClose={() => setSelectedTaskId(null)} diff --git a/frontend/src/components/projects/TaskDetailPanel.tsx b/frontend/src/components/projects/TaskDetailPanel.tsx index bb0109b..535b40e 100644 --- a/frontend/src/components/projects/TaskDetailPanel.tsx +++ b/frontend/src/components/projects/TaskDetailPanel.tsx @@ -9,7 +9,8 @@ import { import axios from 'axios'; import api, { getErrorMessage } from '@/lib/api'; import { formatUpdatedAt } from '@/components/shared/utils'; -import type { ProjectTask, TaskComment } from '@/types'; +import type { ProjectTask, TaskComment, ProjectMember } from '@/types'; +import { AssignmentPicker } from './AssignmentPicker'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; @@ -46,6 +47,10 @@ const priorityColors: Record = { interface TaskDetailPanelProps { task: ProjectTask | null; projectId: number; + members?: ProjectMember[]; + currentUserId?: number; + ownerId?: number; + canAssign?: boolean; onDelete: (taskId: number) => void; onAddSubtask: (parentId: number) => void; onClose?: () => void; @@ -82,6 +87,10 @@ function buildEditState(task: ProjectTask): EditState { export default function TaskDetailPanel({ task, projectId, + members = [], + currentUserId = 0, + ownerId = 0, + canAssign = false, onDelete, onAddSubtask, onClose, @@ -94,6 +103,17 @@ export default function TaskDetailPanel({ task ? buildEditState(task) : { title: '', status: 'pending', priority: 'none', due_date: todayLocal(), person_id: '', description: '' } ); + // Build a combined members list that includes the owner for the AssignmentPicker + const allMembers: ProjectMember[] = [ + // Synthetic owner entry so they appear in the picker + ...(ownerId ? [{ + id: 0, project_id: projectId, user_id: ownerId, invited_by: ownerId, + permission: 'create_modify' as const, status: 'accepted' as const, + source: 'invited' as const, user_name: currentUserId === ownerId ? 'Me (Owner)' : 'Owner', + inviter_name: null, created_at: '', updated_at: '', accepted_at: null, + }] : []), + ...members.filter(m => m.status === 'accepted'), + ]; // --- Mutations --- @@ -168,6 +188,30 @@ export default function TaskDetailPanel({ }, }); + const assignMutation = useMutation({ + mutationFn: async (userIds: number[]) => { + await api.post(`/projects/${projectId}/tasks/${task!.id}/assignments`, { user_ids: userIds }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); + }, + onError: (error) => { + toast.error(getErrorMessage(error, 'Failed to assign')); + }, + }); + + const unassignMutation = useMutation({ + mutationFn: async (userId: number) => { + await api.delete(`/projects/${projectId}/tasks/${task!.id}/assignments/${userId}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); + }, + onError: (error) => { + toast.error(getErrorMessage(error, 'Failed to unassign')); + }, + }); + // --- Handlers --- const handleAddComment = () => { @@ -375,7 +419,17 @@ export default function TaskDetailPanel({ Assigned - {task.assignments && task.assignments.length > 0 ? ( + {canAssign ? ( + assignMutation.mutate(userIds)} + onUnassign={(userId) => unassignMutation.mutate(userId)} + disabled={assignMutation.isPending || unassignMutation.isPending} + /> + ) : task.assignments && task.assignments.length > 0 ? (
{task.assignments.map((a) => (