import React, { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useZino } from "./provider"; import type { FormField } from "./types"; export interface DynamicFormProps { /** Workflow definition ID. */ workflowId: string; /** Activity UID — the form schema is fetched for this activity. */ activityId: string; /** Instance ID — omit for INIT activities (start workflow). */ instanceId?: string; /** Called after successful submission with the result. */ onSuccess?: (result: unknown) => void; /** Called on submission error. */ onError?: (error: unknown) => void; /** Optional CSS class for the form wrapper. */ className?: string; } /** * Renders a dynamic form based on the activity's form schema. * * Fetches the form config from the core-service, renders fields by type, * and submits via performActivity or startWorkflow. * * ```tsx * { closeModal(); }} * /> * ``` */ export function DynamicForm({ workflowId, activityId, instanceId, onSuccess, onError, className, }: DynamicFormProps) { const { workflows } = useZino(); const queryClient = useQueryClient(); const [formData, setFormData] = useState>({}); // Fetch form schema const { data: form, isLoading: formLoading, error: formError } = useQuery({ queryKey: ["form", workflowId, activityId, instanceId], queryFn: () => workflows.getForm(workflowId, activityId, "desktop", instanceId), }); // Submit mutation const submit = useMutation({ mutationFn: async (data: Record) => { if (instanceId) { return workflows.performActivity(workflowId, instanceId, activityId, data); } else { return workflows.startWorkflow(workflowId, activityId, data); } }, onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ["recordview"] }); queryClient.invalidateQueries({ queryKey: ["instance"] }); queryClient.invalidateQueries({ queryKey: ["detailview"] }); queryClient.invalidateQueries({ queryKey: ["audit"] }); onSuccess?.(result); }, onError: (err) => { onError?.(err); }, }); const handleChange = (fieldId: string, value: string) => { setFormData((prev) => ({ ...prev, [fieldId]: value })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Map form field IDs to their mapped_activity_field_id const mapped: Record = {}; for (const field of form?.form_json?.fields ?? []) { const key = field.mapped_activity_field_id || field.id; mapped[key] = formData[field.id] ?? ""; } submit.mutate(mapped); }; if (formLoading) { return
; } if (formError) { return
Failed to load form: {(formError as any)?.message ?? "Unknown error"}
; } const fields: FormField[] = form?.form_json?.fields ?? []; return (
{form?.form_json?.title && (

{form.form_json.title}

)}
{fields.map((field) => (
{renderField(field, formData[field.id] ?? "", (v) => handleChange(field.id, v))}
))}
{submit.error && (
{(submit.error as any)?.message ?? "Submission failed"}
)}
); } function renderField( field: FormField, value: string, onChange: (value: string) => void, ) { const baseClass = "w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"; switch (field.type) { case "paragraph": return (