feat(F.14): Unified Task System — F-TASK-MODEL/API/AGENT/WORK/UI/MIG/GOAL/TEST
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-TASK-MODEL: Extended Task model with polymorphic assignee/entity/creator, subtasks, dependencies, task_type, success_criteria, progress - F-TASK-API: Extended task routes with polymorphic filters, subtasks, dependencies, new lifecycle - F-TASK-AGENT: ai_tools.py (191 lines) — create_task, assign_task, update_task_status, decompose_goal tools - F-TASK-WORK: workstream.py — task_card and goal_card blocks in communication system - F-TASK-UI: TaskBoard.tsx, TaskDetail.tsx, GoalView.tsx frontend components - F-TASK-MIG: Migration 0124 — new columns, data migration for contact_id/assigned_to - F-TASK-GOAL: Progress aggregation, success criteria evaluation, parent status propagation - F-TASK-TEST: test_unified_tasks.py (414 lines) - i18n updates for task system
This commit is contained in:
+119
-10
@@ -1,19 +1,36 @@
|
||||
/**
|
||||
* Tasks API hooks — CRUD, assign, status update.
|
||||
* Tasks API hooks — CRUD, assign, status update, subtasks, dependencies, goals.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
|
||||
|
||||
export type TaskStatus = 'open' | 'in_progress' | 'review' | 'blocked' | 'done' | 'cancelled';
|
||||
export type TaskType = 'todo' | 'approval' | 'follow_up' | 'review' | 'goal' | 'milestone' | 'agent_subtask';
|
||||
export type AssigneeType = 'user' | 'agent' | 'group';
|
||||
export type CreatorType = 'user' | 'agent' | 'workflow' | 'system';
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: 'open' | 'in_progress' | 'done';
|
||||
status: TaskStatus;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
due_date: string | null;
|
||||
assigned_to: string | null;
|
||||
contact_id: string | null;
|
||||
assignee_type: AssigneeType;
|
||||
assignee_id: string | null;
|
||||
entity_type: string | null;
|
||||
entity_id: string | null;
|
||||
creator_type: CreatorType;
|
||||
creator_id: string | null;
|
||||
parent_task_id: string | null;
|
||||
depends_on: string[];
|
||||
task_type: TaskType;
|
||||
success_criteria: Record<string, unknown> | null;
|
||||
target_date: string | null;
|
||||
progress: number;
|
||||
created_by: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -29,28 +46,58 @@ export interface TaskListResponse {
|
||||
export interface TaskCreateInput {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
due_date?: string | null;
|
||||
assigned_to?: string | null;
|
||||
contact_id?: string | null;
|
||||
assignee_type?: AssigneeType;
|
||||
assignee_id?: string | null;
|
||||
entity_type?: string | null;
|
||||
entity_id?: string | null;
|
||||
creator_type?: CreatorType;
|
||||
creator_id?: string | null;
|
||||
parent_task_id?: string | null;
|
||||
depends_on?: string[];
|
||||
task_type?: TaskType;
|
||||
success_criteria?: Record<string, unknown> | null;
|
||||
target_date?: string | null;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export interface TaskUpdateInput {
|
||||
title?: string;
|
||||
description?: string | null;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: 'low' | 'medium' | 'high' | 'urgent';
|
||||
due_date?: string | null;
|
||||
assigned_to?: string | null;
|
||||
contact_id?: string | null;
|
||||
assignee_type?: AssigneeType;
|
||||
assignee_id?: string | null;
|
||||
entity_type?: string | null;
|
||||
entity_id?: string | null;
|
||||
creator_type?: CreatorType;
|
||||
creator_id?: string | null;
|
||||
parent_task_id?: string | null;
|
||||
depends_on?: string[];
|
||||
task_type?: TaskType;
|
||||
success_criteria?: Record<string, unknown> | null;
|
||||
target_date?: string | null;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export interface TaskFilter {
|
||||
status?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: string;
|
||||
assigned_to?: string;
|
||||
contact_id?: string;
|
||||
entity_type?: string;
|
||||
entity_id?: string;
|
||||
assignee_type?: AssigneeType;
|
||||
assignee_id?: string;
|
||||
parent_task_id?: string;
|
||||
task_type?: TaskType;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
@@ -60,6 +107,12 @@ export function useTasks(page = 1, pageSize = 25, filter?: TaskFilter) {
|
||||
if (filter?.priority) params.set('priority', filter.priority);
|
||||
if (filter?.assigned_to) params.set('assigned_to', filter.assigned_to);
|
||||
if (filter?.contact_id) params.set('contact_id', filter.contact_id);
|
||||
if (filter?.entity_type) params.set('entity_type', filter.entity_type);
|
||||
if (filter?.entity_id) params.set('entity_id', filter.entity_id);
|
||||
if (filter?.assignee_type) params.set('assignee_type', filter.assignee_type);
|
||||
if (filter?.assignee_id) params.set('assignee_id', filter.assignee_id);
|
||||
if (filter?.parent_task_id) params.set('parent_task_id', filter.parent_task_id);
|
||||
if (filter?.task_type) params.set('task_type', filter.task_type);
|
||||
if (filter?.search) params.set('search', filter.search);
|
||||
return useQuery({
|
||||
queryKey: ['tasks', page, pageSize, filter],
|
||||
@@ -110,8 +163,8 @@ export function useDeleteTask() {
|
||||
export function useAssignTask() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, assignedTo }: { id: string; assignedTo: string }) =>
|
||||
apiPost<Task>(`/tasks/${id}/assign`, { assigned_to: assignedTo }),
|
||||
mutationFn: ({ id, assigneeType, assigneeId }: { id: string; assigneeType: AssigneeType; assigneeId: string }) =>
|
||||
apiPost<Task>(`/tasks/${id}/assign`, { assignee_type: assigneeType, assignee_id: assigneeId }),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
||||
@@ -122,7 +175,7 @@ export function useAssignTask() {
|
||||
export function useUpdateTaskStatus() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: string }) =>
|
||||
mutationFn: ({ id, status }: { id: string; status: TaskStatus }) =>
|
||||
apiPost<Task>(`/tasks/${id}/status`, { status }),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
@@ -130,3 +183,59 @@ export function useUpdateTaskStatus() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateSubtask() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ parentId, data }: { parentId: string; data: TaskCreateInput }) =>
|
||||
apiPost<Task>(`/tasks/${parentId}/subtasks`, data),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', variables.parentId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useListSubtasks(parentId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['tasks', parentId, 'subtasks'],
|
||||
queryFn: () => apiGet<Task[]>(`/tasks/${parentId}/subtasks`),
|
||||
enabled: !!parentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddDependency() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, dependsOn }: { id: string; dependsOn: string }) =>
|
||||
apiPost<Task>(`/tasks/${id}/dependencies`, { depends_on: dependsOn }),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveDependency() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, dependsOn }: { id: string; dependsOn: string }) =>
|
||||
apiDelete(`/tasks/${id}/dependencies/${dependsOn}`),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDecomposeGoal() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ goalId, subtasks }: { goalId: string; subtasks: TaskCreateInput[] }) =>
|
||||
apiPost<{ goal: Task; subtasks: Task[] }>(`/tasks/${goalId}/decompose`, subtasks),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', variables.goalId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user