From c687de190ca750afae826cb81e9fce4efe3cbdbf Mon Sep 17 00:00:00 2001 From: BUDUMURU SRINIVAS SAI SARAN TEJA Date: Mon, 9 Mar 2026 22:29:04 +0530 Subject: [PATCH] feat: integrate redux-persist for workflow state management - Added redux-persist to persist workflow state across sessions. - Updated workflow slice to include new state structure for managing nodes, edges, and trigger configurations. - Refactored WorkflowCanvas component to utilize Redux for state management, including auto-saving and node configuration. - Enhanced error handling and loading states during workflow execution and configuration. - Adjusted API interactions to align with new state structure and persist changes. - Updated Prisma schema to accommodate new fields for triggers and nodes. - Modified common validation schemas to include optional position and credential fields. --- apps/http-backend/src/index.ts | 2 +- .../src/routes/userRoutes/userRoutes.ts | 35 +- apps/web/app/components/providers.tsx | 29 +- apps/web/app/hooks/useAutoSave.ts | 83 +++ apps/web/app/lib/api.ts | 6 +- .../workflows/[id]/components/ConfigModal.tsx | 106 ++- apps/web/app/workflows/[id]/page.tsx | 654 ++++++++++++------ apps/web/package.json | 1 + apps/web/store/index.ts | 40 +- apps/web/store/slices/workflowSlice.ts | 135 +++- packages/common/src/index.ts | 8 +- packages/db/prisma/schema.prisma | 3 +- packages/nodes/src/gmail/gmail.node.ts | 2 +- .../src/google-sheets/google-sheets.node.ts | 2 +- pnpm-lock.yaml | 23 +- 15 files changed, 800 insertions(+), 329 deletions(-) create mode 100644 apps/web/app/hooks/useAutoSave.ts diff --git a/apps/http-backend/src/index.ts b/apps/http-backend/src/index.ts index 1a6ab49..13e5fe7 100644 --- a/apps/http-backend/src/index.ts +++ b/apps/http-backend/src/index.ts @@ -34,7 +34,7 @@ app.use(cookieParser()); app.use("/user" , userRouter) app.use('/node', sheetRouter) -app.use('/oauth/google', googleAuth) +app.use('/auth/google', googleAuth) app.use('/execute', execRouter) const PORT= 3002 diff --git a/apps/http-backend/src/routes/userRoutes/userRoutes.ts b/apps/http-backend/src/routes/userRoutes/userRoutes.ts index f0220ef..cbc4df0 100644 --- a/apps/http-backend/src/routes/userRoutes/userRoutes.ts +++ b/apps/http-backend/src/routes/userRoutes/userRoutes.ts @@ -361,7 +361,7 @@ router.get( try { if (!req.user) return res - .status(statusCodes.BAD_GATEWAY) + .status(statusCodes.UNAUTHORIZED) .json({ message: "User isnot logged in /not authorized" }); const userId = req.user.sub; @@ -373,7 +373,7 @@ router.get( }, include: { Trigger: true, - nodes: { orderBy: { position: "asc" } }, + nodes: { orderBy: { stage: "asc" } }, }, }); if (!getWorkflow) { @@ -441,6 +441,9 @@ router.put("/workflow/update", userMiddleware, async (req: AuthRequest, res: Res } }) +//------------------------------------------ TRIGGER AND NODE CREATION ------------------------------ + +//TRIGGER CREATION router.post( "/create/trigger", userMiddleware, @@ -465,6 +468,7 @@ router.post( AvailableTriggerID: dataSafe.data.AvailableTriggerID, config: dataSafe.data.Config, workflowId: dataSafe.data.WorkflowId, + Position: dataSafe.data.Position || {} // trigger type pettla db lo ledu aa column }, }); @@ -499,6 +503,7 @@ router.post( } ); +//NODE CREATION router.post( "/create/node", userMiddleware, @@ -510,7 +515,7 @@ router.post( }); } const data = req.body; - console.log(" from http-backeden", data); + // console.log(" from http-backeden", data); const dataSafe = NodeSchema.safeParse(data); console.log("The error is ", dataSafe.error); @@ -524,19 +529,18 @@ router.post( // Config must be valid JSON (not an empty string) // const stage = dataSafe.data.Position console.log("This is from the backend log of positions", dataSafe.data.position) - const { credentialId, ...restConfig } = dataSafe.data.Config; const createdNode = await prismaClient.node.create({ data: { name: dataSafe.data.Name, workflowId: dataSafe.data.WorkflowId, - config: restConfig || {}, + config: dataSafe.data.Config || {}, stage: Number(dataSafe.data.stage ?? 0), position: { x: dataSafe.data.position.x, y: dataSafe.data.position.y }, AvailableNodeID: dataSafe.data.AvailableNodeId, - CredentialsID: credentialId + CredentialsID: dataSafe.data.CredentialId }, }); @@ -556,8 +560,7 @@ router.post( // ------------------------- UPDATE NODES AND TRIGGES --------------------------- -router.put( - "/update/node", +router.put("/update/node", userMiddleware, async (req: AuthRequest, res: Response) => { try { @@ -578,10 +581,11 @@ router.put( const updateNode = await prismaClient.node.update({ where: { id: dataSafe.data.NodeId }, data: { - position: dataSafe.data.position, - config: dataSafe.data.Config , - CredentialsID: dataSafe.data.Config?.credentialId || null - }, + + ...(dataSafe.data.position !== undefined ? { position: dataSafe.data.position } : {}), + ...(dataSafe.data.Config !== undefined ? { config: dataSafe.data.Config } : {}), + ...(dataSafe.data.Config?.credentialId ? { CredentialsID: dataSafe.data.Config.credentialId } : {}) + } }); if (updateNode) @@ -598,8 +602,7 @@ router.put( } ); -router.put( - "/update/trigger", +router.put("/update/trigger", userMiddleware, async (req: AuthRequest, res: Response) => { try { @@ -619,7 +622,9 @@ router.put( const updatedTrigger = await prismaClient.trigger.update({ where: { id: dataSafe.data.TriggerId }, data: { - config: dataSafe.data.Config, + ...(dataSafe.data.Config !== undefined ? { config: dataSafe.data.Config} : {}) , + ...(dataSafe.data.CredentialID !== undefined ? { CredentialsID: dataSafe.data.CredentialID} : {}), + ...(dataSafe.data.Position !== undefined ? {Position: dataSafe.data.Position} : {}) }, }); diff --git a/apps/web/app/components/providers.tsx b/apps/web/app/components/providers.tsx index 0d5f560..7275f86 100644 --- a/apps/web/app/components/providers.tsx +++ b/apps/web/app/components/providers.tsx @@ -3,10 +3,11 @@ import * as React from "react" import { ThemeProvider as NextThemesProvider } from "next-themes" import { Provider as ReduxProvider } from "react-redux" -import { store } from "@/store" +import { persistor, store } from "@/store" import { SessionProvider, useSession } from "next-auth/react" import { useAppDispatch } from "../hooks/redux" import { userAction } from "@/store/slices/userSlice" +import { PersistGate } from "redux-persist/lib/integration/react" function SessionSync(){ const { status, data } = useSession(); @@ -34,18 +35,20 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( - - - - {children} - - + + + + + {children} + + + ) } diff --git a/apps/web/app/hooks/useAutoSave.ts b/apps/web/app/hooks/useAutoSave.ts new file mode 100644 index 0000000..9af0360 --- /dev/null +++ b/apps/web/app/hooks/useAutoSave.ts @@ -0,0 +1,83 @@ +"use client" +import { useEffect, useState } from "react"; +import { useAppDispatch, useAppSelector } from "./redux"; +import { api } from "../lib/api"; +import { store } from "@/store"; +import { workflowActions } from "@/store/slices/workflowSlice"; + +type Status = "saved" | "error" | "saving" +export function useAutoSave(workflowId: string){ + const [saveStatus, setSaveStatus] = useState("saved") + const dispatch = useAppDispatch() + const isChangedState = useAppSelector(s=> s.workflow.isChanged) + const hasUnChanged = isChangedState.trigger || isChangedState.edges || isChangedState.nodes; + + const displayStatus = saveStatus === 'saving' ? 'Saving...' : + saveStatus === 'error' ? 'Save Error' : + hasUnChanged ? 'Unsaved Changes' : 'Saved' + + const batchSave = async()=>{ + const { data, isChanged, changedNodeIds } = store.getState().workflow + + const anyChanges = isChanged.edges || isChanged.nodes || isChanged.trigger; + + if(!anyChanges || !data.workflowId) return; + + setSaveStatus("saving") + + try{ + if(isChanged.nodes && data.nodes){ + const changedNodes = data.nodes.filter(n => changedNodeIds.includes(n.NodeId)) + await Promise.all( + changedNodes.map(node => api.nodes.update({ + NodeId: node.NodeId, + Config: node.Config, + position: node.position + })) + ) + } + if(isChanged.trigger){ + const trigger = data.trigger; + if(trigger) + await api.triggers.update({ + TriggerId: trigger.TriggerId, + Config: trigger.Config, + Position: trigger.position + }) + } + if(isChanged.edges){ + const edges = data.edges; + if(edges) + await api.workflows.put({workflowId: workflowId,edges}) + } + setSaveStatus("saved") + dispatch(workflowActions.markSynced()) + }catch(e){ + setSaveStatus("error") + console.error("error while auto saving: ", e instanceof Error ? e.message : "") + } + } + + useEffect(()=>{ + const interval = setInterval(batchSave,30000); + + const handleBeforeUnload = (e: BeforeUnloadEvent) =>{ + const { isChanged } = store.getState().workflow + if(isChanged.edges || isChanged.nodes || isChanged.trigger){ + e.preventDefault() + batchSave(); + } + } + + window.addEventListener("beforeunload", handleBeforeUnload) + + return ()=>{ + clearInterval(interval); + window.removeEventListener("beforeunload", handleBeforeUnload) + batchSave() + } + }, [workflowId]) + + return { saveStatus, batchSave, displayStatus} + +} \ No newline at end of file diff --git a/apps/web/app/lib/api.ts b/apps/web/app/lib/api.ts index a66c212..1a48687 100644 --- a/apps/web/app/lib/api.ts +++ b/apps/web/app/lib/api.ts @@ -1,6 +1,6 @@ // lib/api.ts import axios from "axios"; -import { BACKEND_URL, NodeUpdateSchema } from "@repo/common/zod"; +import { BACKEND_URL, NodeSchema, NodeUpdateSchema, workflowUpdateSchema } from "@repo/common/zod"; import { TriggerUpdateSchema } from "@repo/common/zod"; import z from "zod"; import { getCredentials } from "../workflow/lib/config"; @@ -31,7 +31,7 @@ export const api = { headers: { "Content-Type": "application/json" }, }); }, - put: async (data: any) => { + put: async (data: z.infer) => { return await axios.put(`${BACKEND_URL}/user/workflow/update`, data, { @@ -70,7 +70,7 @@ export const api = { withCredentials: true, headers: { "Content-Type": "application/json" }, }), - create: async (data: any) => + create: async (data: z.infer) => await axios.post(`${BACKEND_URL}/user/create/node`, data, { withCredentials: true, headers: { "Content-Type": "application/json" }, diff --git a/apps/web/app/workflows/[id]/components/ConfigModal.tsx b/apps/web/app/workflows/[id]/components/ConfigModal.tsx index acd7558..c19418b 100644 --- a/apps/web/app/workflows/[id]/components/ConfigModal.tsx +++ b/apps/web/app/workflows/[id]/components/ConfigModal.tsx @@ -17,12 +17,13 @@ import { selectAllOutputs, NodeTestOutput } from "@/store/slices/nodeOutputSlice"; +import { workflowActions } from "@/store/slices/workflowSlice"; interface ConfigModalProps { isOpen: boolean; selectedNode: any | null; onClose: () => void; - onSave: (selectedNode: string, config: any, userId: string) => Promise; + // onSave: (selectedNode: string, config: any, userId: string) => Promise; workflowId?: string; previousNodes: PreviousNodeOutput[]; } @@ -31,9 +32,9 @@ export default function ConfigModal({ isOpen, selectedNode, onClose, - onSave, + // onSave, workflowId, - previousNodes + previousNodes, }: ConfigModalProps) { const [config, setConfig] = useState>({}); const [dynamicOptions, setDynamicOptions] = useState>({}); @@ -43,6 +44,7 @@ export default function ConfigModal({ const dispatch = useAppDispatch(); const userId = useAppSelector((state) => state.user.userId) as string; + const reduxWorkflow = useAppSelector((state) => state.workflow.data); // Get all tested outputs from Redux (for variable resolution) const allTestedOutputs = useAppSelector(selectAllOutputs); @@ -59,6 +61,16 @@ export default function ConfigModal({ "google.getDocuments" : ({credentialId}) => api.google.getDocuments(credentialId), "google.getSheets" : ({spreadsheetId, credentialId}) => api.google.getSheets(spreadsheetId, credentialId) } + + const dispatchConfig = (newConfig: Record) => { + if (!selectedNode) return; + const isTrigger = reduxWorkflow.trigger?.TriggerId === selectedNode.id; + if (isTrigger) { + dispatch(workflowActions.updateTriggerConfig({ config: newConfig })); + } else { + dispatch(workflowActions.updateNodeConfig({ nodeId: selectedNode.id, config: newConfig })); + } +}; // Build interpolation context from all previously tested nodes const buildTestContext = (): InterpolationContext => { @@ -165,7 +177,9 @@ export default function ConfigModal({ if(!activeField) return; const currentValue = config[activeField] || ""; - setConfig({...config, [activeField]: currentValue + variableSyntax}) + const newConfig = {...config, [activeField]: currentValue + variableSyntax}; + setConfig(newConfig) + dispatchConfig(newConfig) } const handleFieldChange = async (fieldName: string, value: string, nodeConfig: any) => { @@ -174,6 +188,7 @@ export default function ConfigModal({ console.log(fieldName, " ", value, " ", nodeConfig) console.log(config, "from handle field function - 1") setConfig((prev) => ({ ...prev, [fieldName]: value })); + dispatchConfig(updatedConfig); console.log(config, "from handle field fun - 2") console.log({ ...config, [fieldName]: value }, "what we're setting") // Find fields that depend on this field @@ -199,24 +214,45 @@ export default function ConfigModal({ const { cred: credentials = [], authUrl } = useCredentials(credType ?? "", workflowId); useEffect(() => { - setConfig({}); - // We no longer set local credentials here; handled by useCredentials! + if (!selectedNode) { + setConfig({}); + return; + } + // Load existing saved config from Redux instead of starting empty + const isTrigger = reduxWorkflow.trigger?.TriggerId === selectedNode.id; + const loadedConfig = isTrigger ? (reduxWorkflow.trigger?.Config || {}) : (reduxWorkflow.nodes.find(n => n.NodeId === selectedNode.id)?.Config || {}) + + setConfig(loadedConfig) + + const nodeConfig = getNodeConfig(selectedNode.name || selectedNode.actionType); + if(nodeConfig?.fields){ + for(const field of nodeConfig.fields){ + if(field.fetchOptions && field.dependsOn && loadedConfig[field.dependsOn]){ + const fetchFn = fetchOptionsMap[field.fetchOptions]; + if(fetchFn){ + fetchFn(loadedConfig) + .then((option: any[])=> setDynamicOptions(prev => ({...prev, [field.name]: option}))) + .catch(()=> {}) + } + } + } + } }, [selectedNode]); if (!isOpen || !selectedNode) return null; - const handleSave = async () => { - setLoading(true); - try { - await onSave(selectedNode.id, config, userId); - toast.success("Configured Successfully"); - } catch { - toast.error("Failed to save config"); - } finally { - setLoading(false); - onClose(); - } - }; + // const handleSave = async () => { + // setLoading(true); + // try { + // await onSave(selectedNode.id, config, userId); + // toast.success("Configured Successfully"); + // } catch { + // toast.error("Failed to save config"); + // } finally { + // setLoading(false); + // onClose(); + // } + // }; const renderField = (field: any, nodeConfig: any) => { const fieldValue = config[field.name] || ""; @@ -331,11 +367,11 @@ export default function ConfigModal({ value={fieldValue} placeholder={field.placeholder} onFocus={()=> setActiveField(field.name)} - onChange={(e) => - setConfig({ - ...config, - [field.name]: e.target.value, - }) + onChange={(e) => { + const newConfig = { ...config, [field.name]: e.target.value }; + setConfig(newConfig) + dispatchConfig(newConfig) + } } className="w-full p-3 border border-gray-900 bg-black text-white rounded-md focus:ring-2 focus:ring-white focus:border-white placeholder-gray-400" required={field.required} @@ -356,11 +392,11 @@ export default function ConfigModal({ value={fieldValue} onFocus={()=> setActiveField(field.name)} placeholder={field.placeholder} - onChange={(e) => - setConfig({ - ...config, - [field.name]: e.target.value, - }) + onChange={(e) => { + const newConfig = { ...config, [field.name]: e.target.value }; + setConfig(newConfig) + dispatchConfig(newConfig) + } } className="w-full p-3 border border-gray-900 bg-black text-white rounded-md focus:ring-2 focus:ring-white focus:border-white placeholder-gray-400" required={field.required} @@ -559,7 +595,7 @@ export default function ConfigModal({
- + */}
diff --git a/apps/web/app/workflows/[id]/page.tsx b/apps/web/app/workflows/[id]/page.tsx index ead8bb9..fabea2c 100644 --- a/apps/web/app/workflows/[id]/page.tsx +++ b/apps/web/app/workflows/[id]/page.tsx @@ -21,10 +21,17 @@ import { api } from "@/app/lib/api"; import ConfigModal from "./components/ConfigModal"; import { toast } from "sonner"; import { getNodeConfig } from "@/app/lib/nodeConfigs"; +import { useAppDispatch, useAppSelector } from "@/app/hooks/redux"; +import { useAutoSave } from "@/app/hooks/useAutoSave"; +import { workflowActions } from "@/store/slices/workflowSlice"; +import { store } from "@/store"; export default function WorkflowCanvas() { const params = useParams(); const workflowId = params.id as string; + const dispatch = useAppDispatch() + const reduxWorkflow = useAppSelector(s => s.workflow) + const { saveStatus, batchSave, displayStatus } = useAutoSave(workflowId) const getPreviousNodes = ( selectedNodeId: string, @@ -56,7 +63,17 @@ export default function WorkflowCanvas() { }; // State const handleExecute = async () => { + + const unConfigured = nodes.filter( + n=> !n.data.isPlaceholder && !n.data.isConfigured + ); + + if(unConfigured.length > 0){ + setError(`Configure these nodes first: ${unConfigured.map(n => n.data.label).join(', ')}`); + return + } setLoading(true); + try { const data = await api.workflows.execute({workflowId}) console.log("This is from the Execute Button", data) @@ -91,7 +108,7 @@ export default function WorkflowCanvas() { const [configOpen, setConfigOpen] = useState(false); const [selectedNode, setSelectedNode] = useState(null); const [error, setError] = useState(null); - const [loading, setLoading] = useState("") + const [loading, setLoading] = useState(false) const nodeTypes = { customNode: BaseNode, }; @@ -109,131 +126,284 @@ export default function WorkflowCanvas() { return pos; } console.log("The Detaisl of Selected Node is ", selectedNode) + + function checkIsConfigure(nodeName: string, config: any): boolean{ + const nodeConfig = getNodeConfig(nodeName); + if(!nodeConfig || !nodeConfig.fields) return true; + const requiredFields = nodeConfig.fields.filter((f: any)=> f.required); + if(requiredFields.length === 0) return true; + return requiredFields.every((f:any)=> config?.[f.name] !== undefined && config?.[f.name] !== '' ); + } + + useEffect(()=>{ + setNodes(prev => prev.map(node => { + if(node.data?.isPlaceholder) return node; + if(node.data?.nodeType === 'trigger'){ + const reduxConfig = reduxWorkflow.data.trigger?.Config; + const name = reduxWorkflow.data.trigger?.name || ""; + return { ...node, data: { ...node.data, isConfigured: checkIsConfigure(name, reduxConfig)}} + } + + if(node.data.nodeType === 'action'){ + const reduxNode = reduxWorkflow.data.nodes.find(n=> n.NodeId === node.id); + if(!reduxNode) return node; + return { ...node, data: { ...node.data, isConfigured: checkIsConfigure(reduxNode.name, reduxNode.Config)}}; + } + return node; + })); + }, [reduxWorkflow.data.nodes, reduxWorkflow.data.trigger]) + useEffect(() => { const loadWorkflows = async () => { try { - const workflows = await api.workflows.get(workflowId); + if(reduxWorkflow.data.workflowId === workflowId){ + const { trigger, nodes: reduxNodes, edges: reduxEdges } = reduxWorkflow.data - // Defensive: Default to empty arrays if not present - const dbNodes = Array.isArray(workflows?.data?.Data?.nodes) - ? workflows.data.Data.nodes - : []; - console.log("the node data is", dbNodes) - const dbEdges = Array.isArray(workflows?.data?.Data?.Edges) - ? workflows.data.Data.Edges - : []; - const Trigger = workflows?.data?.Data?.Trigger; - - if (!Trigger) { - setError("No trigger found in workflow data, so start with selecting the trigger for the workflow"); - return; + if (!trigger) { + setError("No trigger found in workflow data, so start with selecting the trigger for the workflow"); + return; + } + + const triggerPosition = ensurePosition(trigger.position, DEFAULT_TRIGGER_POSITION ) + const triggerNode = { + id: trigger.TriggerId, + type: "customNode", + position: triggerPosition, + data: { + label: trigger.name || "Trigger", + icon: "⚡", // add icon field in redux and db + nodeType: "trigger", + isConfigured: checkIsConfigure(trigger.name, trigger.Config), + onConfigure: () => + handleNodeConfigure({ + id: trigger.TriggerId, + name: trigger.name + }), + }, + }; + + const transformedNodes = reduxNodes.map((node) =>({ + id: node.NodeId, + type: "customNode", + position: ensurePosition(node.position, { + x: triggerPosition.x + 350, + y: triggerPosition.y + 150, + }), + data: { + label: node.name || "Unknown", + icon: "⚙️", // icon add to db and redux + nodeType: "action", + isConfigured: checkIsConfigure(node.name, node.Config), + onConfigure: () => + handleNodeConfigure({ + id: node.NodeId, + name: node.name, + type: "action", + actionType: node.AvailableNodeID, + }), + } + })) + + const lastNode = + transformedNodes.length > 0 + ? transformedNodes[transformedNodes.length - 1] + : triggerNode; + + const lastPosition = ensurePosition( + lastNode?.position, + transformedNodes.length > 0 + ? { x: triggerPosition.x + 350, y: triggerPosition.y + 150 } + : triggerPosition + ); + + const placeholderPosition = { + x: lastPosition.x + 550, + y: lastPosition.y, + }; + + const actionPlaceholder = { + id: `action-placeholder-${Date.now()}`, + type: "customNode", + position: placeholderPosition, + data: { + label: "Add Action", + icon: "➕", + isPlaceholder: true, + nodeType: "action", + onConfigure: () => setActionOpen(true), + }, + }; + + // 5. Combine nodes + const finalNodes = [triggerNode, ...transformedNodes, actionPlaceholder]; + + const lastActionNode = reduxNodes.length > 0 ? reduxNodes[reduxNodes.length - 1] : null; + const sourceNodeId = lastActionNode ? lastActionNode.NodeId : trigger.TriggerId + + const cleanReduxEdges = reduxEdges.filter(e => !e.target.startsWith('action-placeholder-')) + + const newEdges = [ + ...cleanReduxEdges, + { id: `e-action-${sourceNodeId}-placeholder`, source: sourceNodeId, target: actionPlaceholder.id }, + ] + setNodes(finalNodes) + setEdges(newEdges) + setError(null) + + return } - // Ensure trigger position - const triggerPosition = ensurePosition( - Trigger?.config?.position, - DEFAULT_TRIGGER_POSITION - ); - const triggerNode = { - id: Trigger.id, - type: "customNode", - position: triggerPosition, - data: { - label: Trigger.name || Trigger.data?.label || "Trigger", - icon: Trigger.data?.icon || "⚡", - nodeType: "trigger", - isConfigured: true, - onConfigure: () => - handleNodeConfigure({ - id: Trigger.id, - name: Trigger.name - }), - }, - }; + else{ + const workflows = await api.workflows.get(workflowId); + + // Defensive: Default to empty arrays if not present + const dbNodes = Array.isArray(workflows?.data?.Data?.nodes) + ? workflows.data.Data.nodes + : []; + console.log("the node data is", dbNodes) + const dbEdges = Array.isArray(workflows?.data?.Data?.Edges) + ? workflows.data.Data.Edges + : []; + const Trigger = workflows?.data?.Data?.Trigger; + + + if (!Trigger) { + setError("No trigger found in workflow data, so start with selecting the trigger for the workflow"); + return; + } + + // store updating + dispatch(workflowActions.setWorkflow({ + workflowId, + name: workflows.data.Data.name, + description: workflows.data.Data.description, + trigger: Trigger ? { + TriggerId: Trigger.id, + name: Trigger.name, + type: Trigger.type, + Config: Trigger.config || {}, + position: Trigger.Position || DEFAULT_TRIGGER_POSITION, + AvailableTriggerID: Trigger.AvailableTriggerID + } : null, + nodes: dbNodes.map((n: any) => ({ + NodeId: n.id, + name: n.name, + type: n.type, + Config: n.config || {}, + position: n.position || { x: 0, y: 0 }, + stage: n.stage, + AvailableNodeID: n.AvailableNodeId + })), + edges: dbEdges + })) + // Ensure trigger position + const triggerPosition = ensurePosition( + Trigger?.Position, + DEFAULT_TRIGGER_POSITION + ); - // 3. Transform action nodes, ensuring position property is always valid - const transformedNodes = dbNodes.map((node: any) => ({ - id: node.id, - type: "customNode", - position: ensurePosition(node?.position, { - x: triggerPosition.x + 350, - y: triggerPosition.y + 150, - }), - data: { - label: node.data?.label || node.name || "Unknown", - icon: node.data?.icon || "⚙️", - nodeType: "action", - onConfigure: () => - handleNodeConfigure({ - id: node.id, - name: node.data?.label || node.name, - type: "action", - actionType: node.AvailableNodeId, - }), - }, - })); - - // 4. Calculate placeholder position: use last action node, fallback to trigger - const lastNode = - transformedNodes.length > 0 - ? transformedNodes[transformedNodes.length - 1] - : triggerNode; - - const lastPosition = ensurePosition( - lastNode?.position, - transformedNodes.length > 0 - ? { x: triggerPosition.x + 350, y: triggerPosition.y + 150 } - : triggerPosition - ); + const triggerNode = { + id: Trigger.id, + type: "customNode", + position: triggerPosition, + data: { + label: Trigger.name || Trigger.data?.label || "Trigger", + icon: Trigger.data?.icon || "⚡", + nodeType: "trigger", + isConfigured: checkIsConfigure(Trigger.name, Trigger.config || {}), + onConfigure: () => + handleNodeConfigure({ + id: Trigger.id, + name: Trigger.name + }), + }, + }; - const placeholderPosition = { - x: lastPosition.x + 550, - y: lastPosition.y, - }; + // 3. Transform action nodes, ensuring position property is always valid + const transformedNodes = dbNodes.map((node: any) => ({ + id: node.id, + type: "customNode", + position: ensurePosition(node?.position, { + x: triggerPosition.x + 350, + y: triggerPosition.y + 150, + }), + data: { + label: node.data?.label || node.name || "Unknown", + icon: node.data?.icon || "⚙️", + nodeType: "action", + isConfigured: checkIsConfigure(node.name || node.data?.label, node.config || {}), + onConfigure: () => + handleNodeConfigure({ + id: node.id, + name: node.data?.label || node.name, + type: "action", + actionType: node.AvailableNodeId, + }), + }, + })); + + // 4. Calculate placeholder position: use last action node, fallback to trigger + const lastNode = + transformedNodes.length > 0 + ? transformedNodes[transformedNodes.length - 1] + : triggerNode; + + const lastPosition = ensurePosition( + lastNode?.position, + transformedNodes.length > 0 + ? { x: triggerPosition.x + 350, y: triggerPosition.y + 150 } + : triggerPosition + ); - const actionPlaceholder = { - id: `action-placeholder-${Date.now()}`, - type: "customNode", - position: placeholderPosition, - data: { - label: "Add Action", - icon: "➕", - isPlaceholder: true, - nodeType: "action", - onConfigure: () => setActionOpen(true), - }, - }; + const placeholderPosition = { + x: lastPosition.x + 550, + y: lastPosition.y, + }; - // 5. Combine nodes - const finalNodes = [triggerNode, ...transformedNodes, actionPlaceholder]; + const actionPlaceholder = { + id: `action-placeholder-${Date.now()}`, + type: "customNode", + position: placeholderPosition, + data: { + label: "Add Action", + icon: "➕", + isPlaceholder: true, + nodeType: "action", + onConfigure: () => setActionOpen(true), + }, + }; - // 6. Manage edges - let finalEdges = Array.isArray(dbEdges) ? [...dbEdges] : []; + // 5. Combine nodes + const finalNodes = [triggerNode, ...transformedNodes, actionPlaceholder]; - const placeholderEdge = { - id: `e-${lastNode.id}-${actionPlaceholder.id}`, - source: lastNode.id, - target: actionPlaceholder.id, - type: "default", - animated: true, - }; + // 6. Manage edges + let finalEdges = Array.isArray(dbEdges) ? [...dbEdges] : []; - if (dbEdges.length === 0 && transformedNodes.length > 0) { - const triggerEdge = { - id: `e-${triggerNode.id}-${transformedNodes[0].id}`, - source: triggerNode.id, - target: transformedNodes[0].id, + const placeholderEdge = { + id: `e-${lastNode.id}-${actionPlaceholder.id}`, + source: lastNode.id, + target: actionPlaceholder.id, type: "default", + animated: true, }; - finalEdges.push(triggerEdge); - } - finalEdges.push(placeholderEdge); + if (dbEdges.length === 0 && transformedNodes.length > 0) { + const triggerEdge = { + id: `e-${triggerNode.id}-${transformedNodes[0].id}`, + source: triggerNode.id, + target: transformedNodes[0].id, + type: "default", + }; + finalEdges.push(triggerEdge); + } + + finalEdges.push(placeholderEdge); - setNodes(finalNodes); - setEdges(finalEdges); - setError(null); + setNodes(finalNodes); + setEdges(finalEdges); + setError(null); + } } catch (err: any) { console.error("Failed to load workflow:", err); setError( @@ -254,38 +424,39 @@ export default function WorkflowCanvas() { const handleNodesChange = (changes: NodeChange[]) => { onNodesChange(changes); + }; + + const nodeChangeDb = async (event: React.MouseEvent, node: Node)=>{ try { - changes.forEach((change) => { - if (change.type === "position" && change.position) { - const changedNode = nodes.find((n) => n.id === change.id); - - if (changedNode?.data?.nodeType === "trigger") { - api.triggers.update({ - TriggerId: change.id, - Config: { - ...(typeof changedNode.data.config === "object" && - changedNode.data.config !== null - ? changedNode.data.config - : {}), - position: change.position, - }, - }); - } else { - api.nodes.update({ - NodeId: change.id, - position: change.position, - }); - } - } - }); - setError(null); + if (node.data?.nodeType === "trigger") { + // await api.triggers.update({ + // TriggerId: node.id, + // Config: { + // ...(typeof node.data.config === "object" && node.data.config !== null + // ? node.data.config + // : {}), + // position: node.position, + // }, + // }); + dispatch(workflowActions.updateTriggerPosition(node.position)) + } + else { + // await api.nodes.update({ + // NodeId: node.id, + // position: node.position, + // }); + dispatch(workflowActions.updateNodePosition({ + nodeId:node.id, + position: node.position + })) + } } catch (err: any) { setError( err?.message ?? "Failed to update node position. Please try again." ); } - }; + } const handleActionSelection = async (action: any) => { // Defensive: Ensure at least one trigger present @@ -316,9 +487,6 @@ export default function WorkflowCanvas() { const result = await api.nodes.create({ Name: action.name, AvailableNodeId: action.id, - Config: { - CredentialsID: "", - }, WorkflowId: workflowId, position: newNodePosition, stage: nextIndex, @@ -326,6 +494,31 @@ export default function WorkflowCanvas() { console.log("The data of Node Positions from 201", newNodePosition) const actionId = result.data.data.id; + // const reduxState = store.getState().workflow.data + // const existingReduxNodes = reduxState.nodes + // const sourceNodeId = existingReduxNodes.length > 0 + // ? existingReduxNodes[existingReduxNodes.length - 1]!.NodeId + // : triggerNode.id + // const filterEdges = reduxState.edges.filter(e => !e.target.startsWith('action-placeholder-')) + + const sourceNodeId = currentActionNodes.length > 0 ? currentActionNodes[currentActionNodes.length - 1]!.id : triggerNode.id + + const cleanReduxEdges = edges.filter( + e => !e.target.startsWith('action-placeholder-') && + e.target !== 'action-holder' + ) + // store updating + + dispatch(workflowActions.addWorkflowNode({ + NodeId: actionId, + name: action.name, + type: action.type, + Config: {}, + position: newNodePosition, + stage: nextIndex, + AvailableNodeID: action.id + })) + const newNode = { id: actionId, type: "customNode", @@ -367,7 +560,7 @@ export default function WorkflowCanvas() { }, }; - const triggerId = triggerNode.id; + // const triggerId = triggerNode.id; setNodes((prevNodes) => { // Remove any existing action placeholder nodes @@ -377,40 +570,23 @@ export default function WorkflowCanvas() { return [...filtered, newNode, actionPlaceholder]; }); - setEdges((prevEdges) => { - // Remove placeholder-targeting edges - const filtered = prevEdges.filter((e) => { - const targetNode = nodes.find((n) => n.id === e.target); - return !( - targetNode?.data.isPlaceholder && - targetNode?.data.nodeType === "action" - ); - }); - - const existingActionNodes = nodes.filter( - (n) => n.data.nodeType === "action" && !n.data.isPlaceholder - ); - - const sourceNodeId = - existingActionNodes.length > 0 - ? existingActionNodes[existingActionNodes.length - 1]!.id - : triggerId; - - return [ - ...filtered, - { - id: `e-action-${sourceNodeId}-${actionId}`, - source: sourceNodeId, - target: actionId, - }, - { - id: `e-action-${actionId}-placeholder`, - source: actionId, - target: actionPlaceholder.id, - }, - ]; - }); - + // const reduxState = store.getState().workflow.data + // const existingReduxNodes = reduxState.nodes + // const sourceNodeId = existingReduxNodes.length > 0 + // ? existingReduxNodes[existingReduxNodes.length - 1]!.NodeId + // : triggerId + // // Remove placeholder-targeting edges + // const filterEdges = reduxState.edges.filter(e => !e.target.startsWith('action-placeholder-')) + + const newEdges = [ + ...cleanReduxEdges, + {id: `e-action-${sourceNodeId}-${actionId}`, source: sourceNodeId, target: actionId }, + { id: `e-action-${actionId}-placeholder`, source: actionId, target: actionPlaceholder.id }, + ] + + setEdges(newEdges); + const reduxEdges = newEdges.filter(e => !e.target.startsWith('action-placeholder-') && e.target !== 'action-holder') + dispatch(workflowActions.setEdge(reduxEdges)) setActionOpen(false); setError(null); } catch (err: any) { @@ -432,6 +608,8 @@ export default function WorkflowCanvas() { }); const triggerId = result.data.data.id as string; + + const newNode = { id: triggerId, type: "customNode", @@ -441,7 +619,7 @@ export default function WorkflowCanvas() { icon: trigger.icon, isPlaceholder: false, nodeType: "trigger", - isConfigured: false, + isConfigured: checkIsConfigure(trigger.name, {}), config: {}, onConfigure: () => handleNodeConfigure({ @@ -467,13 +645,25 @@ export default function WorkflowCanvas() { }; setNodes([newNode, actionPlaceholder]); - setEdges([ + const newEdge = [ { id: "e1", source: triggerId, target: "action-holder", }, - ]); + ] + setEdges(newEdge); + const reduxEdge = newEdge.filter(e => e.target !== 'action-holder') + + dispatch(workflowActions.setWorkflowTrigger({ + TriggerId: triggerId, + name: trigger.name, + type: trigger.type, + Config: {}, + position: DEFAULT_TRIGGER_POSITION, + AvailableTriggerID: trigger.id + })) + dispatch(workflowActions.setEdge(reduxEdge)) setTriggerOpen(false); setError(null); } catch (err: any) { @@ -484,21 +674,21 @@ export default function WorkflowCanvas() { } }; - const handleSave = async () => { - try { - await api.workflows.put({ - workflowId: workflowId, - edges: edges, - }); - setError(null); - } catch (err: any) { - setError( - err?.message ?? - "Failed to save workflow. Please try again." - ); - } - }; - console.log("THis log from page.tsx about the nodeConfig", selectedNode) + // const handleSave = async () => { + // try { + // await api.workflows.put({ + // workflowId: workflowId, + // edges: edges, + // }); + // setError(null); + // } catch (err: any) { + // setError( + // err?.message ?? + // "Failed to save workflow. Please try again." + // ); + // } + // }; + // console.log("THis log from page.tsx about the nodeConfig", selectedNode) return (
@@ -549,6 +739,7 @@ export default function WorkflowCanvas() { onNodesChange={handleNodesChange} onEdgesChange={onEdgesChange} nodeTypes={nodeTypes} + onNodeDragStop={nodeChangeDb} fitView > @@ -557,10 +748,10 @@ export default function WorkflowCanvas() {