import { MarkerType, type Node } from "@xyflow/react"; import type { TAppFlowSchema, TAppFlowTriggerSchema } from "flows/AppFlows"; import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { selectAtom } from "jotai/utils"; import { isEqual } from "lodash-es"; import type { ModuleSchemas } from "modules/ModuleManager"; import { createContext, useCallback, useContext, useEffect } from "react"; import { useBknd } from "ui/client/bknd"; export type TFlowNodeData = { label: string; type: string; last?: boolean; start?: boolean; responding?: boolean; }; export type FlowContextType = { name?: string; data?: TAppFlowSchema; schema: ModuleSchemas["flows"]["properties"]["flows"]; actions: { flow: { setName: (name: string) => Promise; }; trigger: { update: (trigger: TAppFlowTriggerSchema) => Promise; }; task: { create: (type: string, defaults?: object) => Promise; update: (name: string, params: any) => Promise; }; }; }; export type TFlowState = { dirty: boolean; name?: string; flow?: TAppFlowSchema; }; export const flowStateAtom = atom({ dirty: false, name: undefined, flow: undefined }); const FlowCanvasContext = createContext(undefined!); const DEFAULT_FLOW = { trigger: {}, tasks: {}, connections: {} }; export function FlowCanvasProvider({ children, name }: { children: any; name?: string }) { //const [dirty, setDirty] = useState(false); const setFlowState = useSetAtom(flowStateAtom); const s = useBknd(); const data = name ? (s.config.flows.flows[name] as TAppFlowSchema) : undefined; const schema = s.schema.flows.properties.flows; useEffect(() => { if (name) { setFlowState({ dirty: false, name, flow: data }); } }, [name]); const actions = { flow: { setName: async (name: string) => { console.log("set name", name); setFlowState((state) => ({ ...state, name, dirty: true })); } }, trigger: { update: async (trigger: TAppFlowTriggerSchema | any) => { console.log("update trigger", trigger); setFlowState((state) => { const flow = state.flow || DEFAULT_FLOW; return { ...state, dirty: true, flow: { ...flow, trigger } }; }); //return s.actions.patch("flows", `flows.flows.${name}`, { trigger }); } }, task: { create: async (name: string, defaults: object = {}) => { console.log("create task", name, defaults); setFlowState((state) => { const flow = state.flow || (DEFAULT_FLOW as any); const tasks = { ...flow.tasks, [name]: defaults }; return { ...state, dirty: true, flow: { ...flow, tasks } }; }); }, update: async (name: string, params: any) => { console.log("update task", name, params); setFlowState((state) => { const flow = state.flow || (DEFAULT_FLOW as any); const task = { ...state.flow?.tasks?.[name], params }; return { ...state, dirty: true, flow: { ...flow, tasks: { ...flow.tasks, [name]: task } } }; }); //return s.actions.patch("flows", `flows.flows.${name}.tasks.${name}`, task); } } }; return ( {children} ); } export function useFlowCanvas() { return useContext(FlowCanvasContext); } export function useFlowCanvasState() { return useAtomValue(flowStateAtom); } export function useFlowSelector( selector: (state: TFlowState) => Reduced, equalityFn: (a: any, b: any) => boolean = isEqual ) { const selected = selectAtom(flowStateAtom, useCallback(selector, []), equalityFn); return useAtom(selected)[0]; } export function flowToNodes(flow: TAppFlowSchema, name: string): Node[] { const nodes: Node[] = [ { id: "trigger", data: { label: name, type: flow.trigger.type }, type: "trigger", dragHandle: ".drag-handle", position: { x: 0, y: 0 } } ]; let i = 1; const count = Object.keys(flow.tasks ?? {}).length; for (const [name, task] of Object.entries(flow.tasks ?? {})) { const last = i === count; const start = i === 1; const responding = last; nodes.push({ id: `task-${name}`, data: { label: name, type: task.type, last, start, responding }, type: "task", dragHandle: ".drag-handle", // @todo: this is currently static position: { x: 450 * i + (i - 1) * 64, y: 0 } }); i++; } /*nodes.push({ id: "select", data: { label: "Select", type: "select" }, type: "select", position: { x: 500 * i, y: 0 } });*/ return nodes; } export function flowToEdges(flow: TAppFlowSchema) { const tasks = Object.entries(flow.tasks ?? {}); if (tasks.length === 0) return []; const edges = tasks.length >= 1 ? [ { id: "trigger-task", source: "trigger", target: `task-${tasks[0]?.[0]}`, //type: "smoothstep", style: { strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, width: 10, height: 10 } } ] : []; for (const [id, connection] of Object.entries(flow.connections ?? {})) { edges.push({ id, source: "task-" + connection.source, target: "task-" + connection.target, style: { strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, width: 10, height: 10 } }); } return edges; }