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:
@@ -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],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user