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) <noreply@anthropic.com>
This commit is contained in:
parent
957939a165
commit
7eac213c20
@ -710,6 +710,10 @@ export default function ProjectDetail() {
|
||||
<TaskDetailPanel
|
||||
task={selectedTask}
|
||||
projectId={parseInt(id!)}
|
||||
members={acceptedMembers}
|
||||
currentUserId={currentUserId}
|
||||
ownerId={project?.user_id ?? 0}
|
||||
canAssign={isOwner || acceptedMembers.some(m => m.user_id === currentUserId && m.permission === 'create_modify')}
|
||||
onDelete={handleDeleteTask}
|
||||
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
||||
onClose={() => setSelectedTaskId(null)}
|
||||
|
||||
@ -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<string, string> = {
|
||||
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({
|
||||
<User className="h-3 w-3" />
|
||||
Assigned
|
||||
</div>
|
||||
{task.assignments && task.assignments.length > 0 ? (
|
||||
{canAssign ? (
|
||||
<AssignmentPicker
|
||||
currentAssignments={task.assignments ?? []}
|
||||
members={allMembers}
|
||||
currentUserId={currentUserId}
|
||||
ownerId={ownerId}
|
||||
onAssign={(userIds) => assignMutation.mutate(userIds)}
|
||||
onUnassign={(userId) => unassignMutation.mutate(userId)}
|
||||
disabled={assignMutation.isPending || unassignMutation.isPending}
|
||||
/>
|
||||
) : task.assignments && task.assignments.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{task.assignments.map((a) => (
|
||||
<span
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user