feat(5.3): add workflow API frontend module with TypeScript types and React Query hooks

- Create frontend/src/api/workflows.ts with types matching backend workflow model
- Workflow CRUD hooks: useWorkflows, useWorkflow, useCreateWorkflow, useUpdateWorkflow, useDeleteWorkflow
- Instance hooks: useWorkflowInstances, useWorkflowInstance, useCreateWorkflowInstance, useAdvanceWorkflowInstance, useCancelWorkflowInstance
- Step history types included in WorkflowInstanceDetail
- Add comprehensive test suite (13 tests, all passing)
- TSC: 0 new errors
This commit is contained in:
Agent Zero
2026-07-23 20:24:12 +02:00
parent 903d649a0f
commit a9151b1159
2 changed files with 548 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
/**
* Workflow API module — TypeScript types and React Query hooks.
* Matches backend endpoints from /api/v1/workflows.
*
* Backend: app/routes/workflows.py, app/models/workflow.py, app/schemas/workflow.py
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
import type { PaginatedResponse } from './types';
// ── Types ──
export interface WorkflowStep {
name: string;
type: 'action' | 'approval' | 'notification' | 'condition';
config: Record<string, unknown>;
description?: string | null;
}
export interface Workflow {
id: string;
name: string;
description?: string | null;
trigger_event?: string | null;
steps: WorkflowStep[];
is_active: boolean;
created_by?: string | null;
created_at?: string | null;
updated_at?: string | null;
}
export interface WorkflowListResponse extends PaginatedResponse<Workflow> {}
export interface WorkflowCreateInput {
name: string;
description?: string | null;
trigger_event?: string | null;
steps: WorkflowStep[];
is_active?: boolean;
}
export interface WorkflowUpdateInput {
name?: string;
description?: string | null;
trigger_event?: string | null;
steps?: WorkflowStep[];
is_active?: boolean;
}
export type InstanceStatus =
| 'pending'
| 'in_progress'
| 'completed'
| 'rejected'
| 'cancelled';
export interface WorkflowInstance {
id: string;
workflow_id: string;
status: InstanceStatus;
current_step_index: number;
context: Record<string, unknown>;
initiated_by?: string | null;
completed_at?: string | null;
timeout_hours?: number | null;
timeout_at?: string | null;
created_at?: string | null;
updated_at?: string | null;
}
export interface StepHistoryEntry {
id: string;
instance_id: string;
step_index: number;
step_type: string;
action: string;
actor_id?: string | null;
details?: Record<string, unknown> | null;
created_at?: string | null;
}
export interface WorkflowInstanceDetail extends WorkflowInstance {
history: StepHistoryEntry[];
workflow_name?: string | null;
}
export interface InstanceListResponse extends PaginatedResponse<WorkflowInstance> {}
export interface InstanceCreateInput {
context?: Record<string, unknown>;
timeout_hours?: number | null;
}
export interface AdvanceRequest {
decision: 'approve' | 'reject';
comment?: string | null;
}
// ── Workflow Definition Hooks ──
export function useWorkflows(page = 1, pageSize = 20, isActive?: boolean) {
const params = new URLSearchParams({
page: String(page),
page_size: String(pageSize),
});
if (isActive !== undefined) params.set('is_active', String(isActive));
return useQuery({
queryKey: ['workflows', page, pageSize, isActive],
queryFn: () =>
apiGet<WorkflowListResponse>(`/workflows?${params.toString()}`),
});
}
export function useWorkflow(id?: string) {
return useQuery({
queryKey: ['workflows', id],
queryFn: () => apiGet<Workflow>(`/workflows/${id}`),
enabled: !!id,
});
}
export function useCreateWorkflow() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: WorkflowCreateInput) =>
apiPost<Workflow>('/workflows', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] });
},
});
}
export function useUpdateWorkflow() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
data,
}: {
id: string;
data: WorkflowUpdateInput;
}) => apiPatch<Workflow>(`/workflows/${id}`, data),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['workflows'] });
queryClient.invalidateQueries({
queryKey: ['workflows', variables.id],
});
},
});
}
export function useDeleteWorkflow() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiDelete(`/workflows/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] });
},
});
}
// ── Workflow Instance Hooks ──
export function useWorkflowInstances(
page = 1,
pageSize = 20,
statusFilter?: InstanceStatus
) {
const params = new URLSearchParams({
page: String(page),
page_size: String(pageSize),
});
if (statusFilter) params.set('status', statusFilter);
return useQuery({
queryKey: ['workflowInstances', page, pageSize, statusFilter],
queryFn: () =>
apiGet<InstanceListResponse>(
`/workflows/instances?${params.toString()}`
),
});
}
export function useWorkflowInstance(id?: string) {
return useQuery({
queryKey: ['workflowInstances', id],
queryFn: () =>
apiGet<WorkflowInstanceDetail>(`/workflows/instances/${id}`),
enabled: !!id,
});
}
export function useCreateWorkflowInstance() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
workflowId,
data,
}: {
workflowId: string;
data: InstanceCreateInput;
}) =>
apiPost<WorkflowInstance>(
`/workflows/${workflowId}/instances`,
data
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflowInstances'] });
},
});
}
export function useAdvanceWorkflowInstance() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
instanceId,
data,
}: {
instanceId: string;
data: AdvanceRequest;
}) =>
apiPost<WorkflowInstance>(
`/workflows/instances/${instanceId}/advance`,
data
),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['workflowInstances'] });
queryClient.invalidateQueries({
queryKey: ['workflowInstances', variables.instanceId],
});
},
});
}
export function useCancelWorkflowInstance() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (instanceId: string) =>
apiPost<WorkflowInstance>(
`/workflows/instances/${instanceId}/cancel`
),
onSuccess: (_data, instanceId) => {
queryClient.invalidateQueries({ queryKey: ['workflowInstances'] });
queryClient.invalidateQueries({
queryKey: ['workflowInstances', instanceId],
});
},
});
}