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
|
<TaskDetailPanel
|
||||||
task={selectedTask}
|
task={selectedTask}
|
||||||
projectId={parseInt(id!)}
|
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}
|
onDelete={handleDeleteTask}
|
||||||
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
onAddSubtask={(parentId) => openTaskForm(null, parentId)}
|
||||||
onClose={() => setSelectedTaskId(null)}
|
onClose={() => setSelectedTaskId(null)}
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import {
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import api, { getErrorMessage } from '@/lib/api';
|
import api, { getErrorMessage } from '@/lib/api';
|
||||||
import { formatUpdatedAt } from '@/components/shared/utils';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@ -46,6 +47,10 @@ const priorityColors: Record<string, string> = {
|
|||||||
interface TaskDetailPanelProps {
|
interface TaskDetailPanelProps {
|
||||||
task: ProjectTask | null;
|
task: ProjectTask | null;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
members?: ProjectMember[];
|
||||||
|
currentUserId?: number;
|
||||||
|
ownerId?: number;
|
||||||
|
canAssign?: boolean;
|
||||||
onDelete: (taskId: number) => void;
|
onDelete: (taskId: number) => void;
|
||||||
onAddSubtask: (parentId: number) => void;
|
onAddSubtask: (parentId: number) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@ -82,6 +87,10 @@ function buildEditState(task: ProjectTask): EditState {
|
|||||||
export default function TaskDetailPanel({
|
export default function TaskDetailPanel({
|
||||||
task,
|
task,
|
||||||
projectId,
|
projectId,
|
||||||
|
members = [],
|
||||||
|
currentUserId = 0,
|
||||||
|
ownerId = 0,
|
||||||
|
canAssign = false,
|
||||||
onDelete,
|
onDelete,
|
||||||
onAddSubtask,
|
onAddSubtask,
|
||||||
onClose,
|
onClose,
|
||||||
@ -94,6 +103,17 @@ export default function TaskDetailPanel({
|
|||||||
task ? buildEditState(task) : { title: '', status: 'pending', priority: 'none', due_date: todayLocal(), person_id: '', description: '' }
|
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 ---
|
// --- 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 ---
|
// --- Handlers ---
|
||||||
|
|
||||||
const handleAddComment = () => {
|
const handleAddComment = () => {
|
||||||
@ -375,7 +419,17 @@ export default function TaskDetailPanel({
|
|||||||
<User className="h-3 w-3" />
|
<User className="h-3 w-3" />
|
||||||
Assigned
|
Assigned
|
||||||
</div>
|
</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">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{task.assignments.map((a) => (
|
{task.assignments.map((a) => (
|
||||||
<span
|
<span
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user