diff --git a/apps/http-backend/src/routes/userRoutes/userMiddleware.ts b/apps/http-backend/src/routes/userRoutes/userMiddleware.ts index 1df823c..2333a5b 100644 --- a/apps/http-backend/src/routes/userRoutes/userMiddleware.ts +++ b/apps/http-backend/src/routes/userRoutes/userMiddleware.ts @@ -30,7 +30,7 @@ export async function userMiddleware( }); } - console.log("Decoded User:", payload); + // console.log("Decoded User:", payload); req.user = payload; return next(); @@ -39,4 +39,4 @@ export async function userMiddleware( message: `Invalid token: ${e instanceof Error ? e.message : "Unknown error"}`, }); } -} \ No newline at end of file +} diff --git a/apps/http-backend/src/routes/userRoutes/userRoutes.ts b/apps/http-backend/src/routes/userRoutes/userRoutes.ts index 5790927..5420479 100644 --- a/apps/http-backend/src/routes/userRoutes/userRoutes.ts +++ b/apps/http-backend/src/routes/userRoutes/userRoutes.ts @@ -1,7 +1,7 @@ import dotenv from "dotenv"; dotenv.config(); import { prismaClient } from "@repo/db"; -import { Express, Response, Router } from "express"; +import { Request, Response, Router } from "express"; import { AuthRequest, userMiddleware } from "./userMiddleware.js"; import { AvailableTriggers, @@ -15,29 +15,6 @@ import { } from "@repo/common/zod"; import { GoogleSheetsNodeExecutor } from "@repo/nodes"; const router: Router = Router(); -// router.post("/create", async (req, res) => { -// // const Data = req.body; - -// // const {name , email , password} = req.body -// // console.log(Data) -// try { -// const user = await prismaClient.user.create({ -// data: { -// name: "name", -// email: "email", -// password: "password", -// }, -// }); -// res.json({ -// message: "Signup DOne", -// user, -// }); -// } catch (e) { -// console.log("Detailed Error", e); -// } -// }); - -// ------------------- AVALIABLE TRIGGERS AND NODES CREATION AND FETCHING ROUTES -------------------------- router.post("/createAvaliableNode", async (req: AuthRequest, res: Response) => { try { @@ -70,7 +47,8 @@ router.post("/createAvaliableNode", async (req: AuthRequest, res: Response) => { } }); -router.get("/getAvailableNodes", +router.get( + "/getAvailableNodes", userMiddleware, async (req: AuthRequest, res: Response) => { if (!req.user) { @@ -95,9 +73,10 @@ router.get("/getAvailableNodes", } ); -router.post("/createAvaliableTriggers", - userMiddleware, - async (req: AuthRequest, res: Response) => { +router.post( + "/createAvaliableTriggers", + // userMiddleware, + async (req: Request, res: Response) => { try { const Data = req.body; const ParsedData = AvailableTriggers.safeParse(Data); @@ -129,7 +108,8 @@ router.post("/createAvaliableTriggers", } ); -router.get("/getAvailableTriggers", +router.get( + "/getAvailableTriggers", userMiddleware, async (req: AuthRequest, res: Response) => { try { @@ -179,7 +159,7 @@ router.get("/getAvailableTriggers", // // console.log("response: ",response) // const authUrl = typeof response === 'string' ? response : null // // console.log(authUrl); - + // const credentials = response instanceof Object ? response : null // // console.log(credentials) // if(authUrl){ @@ -204,20 +184,21 @@ router.get("/getAvailableTriggers", //------------------------------ GET CREDENTIALS ----------------------------- -router.get('/getCredentials/:type', +router.get( + "/getCredentials/:type", userMiddleware, async (req: AuthRequest, res) => { try { - console.log("user from getcredentials: ", req.user) + console.log("user from getcredentials: ", req.user); if (!req.user) { return res.status(statusCodes.BAD_REQUEST).json({ - message: "User is not Loggedin" - }) + message: "User is not Loggedin", + }); } const userId = req.user.sub; - const type = req.params.type - console.log(userId, " -userid") - + const type = req.params.type; + console.log(userId, " -userid"); + if (!type || !userId) { return res.status(statusCodes.BAD_REQUEST).json({ message: "Incorrect type Input", @@ -228,15 +209,16 @@ router.get('/getCredentials/:type', const credentials = await prismaClient.credential.findMany({ where: { userId: userId, - type : type - } + type: type, + }, }); if (credentials.length === 0) { // No credentials found - return the correct auth URL - const authUrl = `${process.env.BACKEND_URL || 'http://localhost:3002'}/auth/google/initiate`; + const authUrl = `${process.env.BACKEND_URL || "http://localhost:3002"}/auth/google/initiate`; return res.status(statusCodes.OK).json({ - message: "Credentials not found create credentials using this auth url", + message: + "Credentials not found create credentials using this auth url", Data: authUrl, }); } @@ -246,44 +228,55 @@ router.get('/getCredentials/:type', message: "Credentials Fetched successfully", Data: credentials, }); - } catch (e) { - console.log("Error Fetching the credentials ", e instanceof Error ? e.message : "Unknown reason"); + console.log( + "Error Fetching the credentials ", + e instanceof Error ? e.message : "Unknown reason" + ); return res .status(statusCodes.INTERNAL_SERVER_ERROR) - .json({ message: "Internal server error from fetching the credentials" }); + .json({ + message: "Internal server error from fetching the credentials", + }); } } ); -router.get('/getAllCreds', userMiddleware, async(req: AuthRequest, res:Response) =>{ - try{ - if(!req.user){ - return res.status(statusCodes.BAD_REQUEST).json({ - message: "User is not Loggedin" - }) - } - const userId = req.user.sub; - const creds = await prismaClient.credential.findMany({ - where:{ userId: userId} - }) - if(creds){ - return res.status(statusCodes.OK).json({ - message: "Fetched all credentials of the User!", - data: creds - }) - } +router.get( + "/getAllCreds", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + if (!req.user) { + return res.status(statusCodes.BAD_REQUEST).json({ + message: "User is not Loggedin", + }); } - catch(e){ - console.log("Error Fetching the credentials ", e instanceof Error ? e.message : "Unkown reason"); - return res - .status(statusCodes.INTERNAL_SERVER_ERROR) - .json({ message: "Internal server from fetching the credentials" }); + const userId = req.user.sub; + const creds = await prismaClient.credential.findMany({ + where: { userId: userId }, + }); + if (creds) { + return res.status(statusCodes.OK).json({ + message: "Fetched all credentials of the User!", + data: creds, + }); } -}) + } catch (e) { + console.log( + "Error Fetching the credentials ", + e instanceof Error ? e.message : "Unkown reason" + ); + return res + .status(statusCodes.INTERNAL_SERVER_ERROR) + .json({ message: "Internal server from fetching the credentials" }); + } + } +); // ----------------------------------- CREATE WORKFLOW --------------------------------- -router.post("/create/workflow", +router.post( + "/create/workflow", userMiddleware, async (req: AuthRequest, res) => { try { @@ -315,7 +308,7 @@ router.post("/create/workflow", user: { connect: { id: UserID }, }, - description: "Workflow-generated", + description: ParsedData.data.description || "Workflow-Created", name: ParsedData.data.Name, config: ParsedData.data.Config, }, @@ -335,22 +328,23 @@ router.post("/create/workflow", // ------------------------------------ FETCHING WORKFLOWS ----------------------------------- -router.get("/workflows", - userMiddleware , +router.get( + "/workflows", + userMiddleware, async (req: AuthRequest, res: Response) => { try { if (!req.user) return res .status(statusCodes.UNAUTHORIZED) .json({ message: "User is not logged in /not authorized" }); - const userId = req.user.id ; + const userId = req.user.id; const workflows = await prismaClient.workflow.findMany({ where: { - userId + userId, }, }); - console.log(workflows) + console.log(workflows); return res .status(statusCodes.OK) .json({ message: "Workflows fetched succesfullu", Data: workflows }); @@ -364,36 +358,43 @@ router.get("/workflows", } ); -router.get('/empty/workflow', userMiddleware, async(req:AuthRequest, res: Response)=>{ - try{ - if (!req.user) +router.get( + "/empty/workflow", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + if (!req.user) return res .status(statusCodes.UNAUTHORIZED) .json({ message: "User is not logged in /not authorized" }); const userId = req.user.id; const workflow = await prismaClient.workflow.findFirst({ - where:{ + where: { userId: userId, - isEmpty: true + isEmpty: true, }, orderBy: { - createdAt: 'desc' - } - }) + createdAt: "desc", + }, + }); return res .status(statusCodes.OK) .json({ message: "Workflow fetched succesful", Data: workflow }); + } catch (e) { + console.log( + "The error is from getting wrkflows", + e instanceof Error ? e.message : "UNKNOWN ERROR" + ); - }catch(e){ - console.log("The error is from getting wrkflows", e instanceof Error ? e.message : "UNKNOWN ERROR"); - - return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ - meesage: "Internal Server Error From getting workflows for the user", - }); + return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ + meesage: "Internal Server Error From getting workflows for the user", + }); + } } -}) +); -router.get("/workflow/:workflowId", +router.get( + "/workflow/:workflowId", userMiddleware, async (req: AuthRequest, res: Response) => { try { @@ -409,10 +410,10 @@ router.get("/workflow/:workflowId", id: workflowId, userId: userId, }, - include:{ + include: { Trigger: true, - nodes: { orderBy: {position: 'asc'}} - } + nodes: { orderBy: { position: "asc" } }, + }, }); if (!getWorkflow) { return res.status(statusCodes.UNAUTHORIZED).json({ @@ -432,44 +433,53 @@ router.get("/workflow/:workflowId", } ); + +router.put("/workflow/update" , userMiddleware , (req : AuthRequest , res : Response) => { + +}) // ---------------------------------------- INSERTING DATA INTO NODES/ TRIGGER TABLE----------------------------- -router.post('/create/trigger', userMiddleware, async(req: AuthRequest, res: Response)=>{ - try { +router.post( + "/create/trigger", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { if (!req.user) { return res.status(statusCodes.BAD_REQUEST).json({ message: "User is not logged in ", }); } const data = req.body; - const dataSafe = TriggerSchema.safeParse(data) - if(!dataSafe.success) + const dataSafe = TriggerSchema.safeParse(data); + console.log("The error from creation of trigger is ", dataSafe.error); + + if (!dataSafe.success) return res.status(statusCodes.BAD_REQUEST).json({ - message: "Invalid input" - }) + message: "Invalid input", + }); const createdTrigger = await prismaClient.trigger.create({ - data:{ + data: { name: dataSafe.data.Name, AvailableTriggerID: dataSafe.data.AvailableTriggerID, config: dataSafe.data.Config, workflowId: dataSafe.data.WorkflowId, // trigger type pettla db lo ledu aa column - } - }) + }, + }); await prismaClient.workflow.update({ - where:{ id: dataSafe.data.WorkflowId }, - data:{ - isEmpty: false - } - }) + where: { id: dataSafe.data.WorkflowId }, + data: { + isEmpty: false, + }, + }); - if(createdTrigger){ + if (createdTrigger) { return res.status(statusCodes.CREATED).json({ message: "Trigger created", - data: createdTrigger - }) + data: createdTrigger, + }); } - }catch(e){ + } catch (e) { console.log("This is the error from Trigger creatig", e); return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ message: "Internal server Error from Trigger creation ", @@ -484,135 +494,141 @@ router.post('/create/trigger', userMiddleware, async(req: AuthRequest, res: Resp // "WorkflowId": "d0216fca-ca9b-4f3f-b01c-0a29b4305708", // "TriggerType":"" // } -}) + } +); -router.post('/create/node', userMiddleware, async(req: AuthRequest, res: Response)=>{ - try{ - if(!req.user){ - return res.status(statusCodes.BAD_REQUEST).json({ +router.post( + "/create/node", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + if (!req.user) { + return res.status(statusCodes.BAD_REQUEST).json({ message: "User is not logged in ", }); - } - const data = req.body; - // console.log(data," from http-backeden" ); - - const dataSafe = NodeSchema.safeParse(data) - if(!dataSafe.success) { - return res.status(statusCodes.BAD_REQUEST).json({ - message: "Invalid input" - }) - } - const createdNode = await prismaClient.node.create({ - data:{ - name: dataSafe.data.Name, - workflowId: dataSafe.data.WorkflowId, - AvailableNodeID: dataSafe.data.AvailableNodeId, - // AvailabeNodeID: dataSafe.data.AvailableNodeId, - config: dataSafe.data.Config, - position: dataSafe.data.Position } - }) + const data = req.body; + console.log(" from http-backeden" , data); - if(createdNode) - return res.status(statusCodes.CREATED).json({ - message: "Node created", - data: createdNode - }) - }catch(e){ - console.log("This is the error from Node creating", e); - return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ - message: "Internal server Error from Node creation", - }); + const dataSafe = NodeSchema.safeParse(data); + console.log("The error is ", dataSafe.error); + if (!dataSafe.success) { + return res.status(statusCodes.BAD_REQUEST).json({ + message: "Invalid input", + }); + } + // Fix: Only provide required fields for node creation, exclude credentials/credentialsId + // Use an empty array for credentials (if required) or don't pass it at all + // Config must be valid JSON (not an empty string) + // const stage = dataSafe.data.Position + const createdNode = await prismaClient.node.create({ + data: { + name: dataSafe.data.Name, + workflowId: dataSafe.data.WorkflowId, + AvailableNodeID: dataSafe.data.AvailableNodeId, + config: {}, // Config is an empty object by default + stage: Number(dataSafe.data.stage), + position: {} + }, + }); + + if (createdNode) + return res.status(statusCodes.CREATED).json({ + message: "Node created", + data: createdNode, + }); + } catch (e) { + console.log("This is the error from Node creating", e); + return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Internal server Error from Node creation", + }); + } } -}) +); // ------------------------- UPDATE NODES AND TRIGGES --------------------------- -router.put('/update/node', userMiddleware, async(req: AuthRequest, res: Response)=>{ - try{ - if(!req.user){ - return res.status(statusCodes.BAD_REQUEST).json({ +router.put( + "/update/node", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + if (!req.user) { + return res.status(statusCodes.BAD_REQUEST).json({ message: "User is not logged in ", }); - } - const data = req.body; - const dataSafe = NodeUpdateSchema.safeParse(data) - - if(!dataSafe.success) { - return res.status(statusCodes.BAD_REQUEST).json({ - message: "Invalid input" - }) - } + } + const data = req.body; + const dataSafe = NodeUpdateSchema.safeParse(data); - const updateNode = await prismaClient.node.update({ - where: {id: dataSafe.data.NodeId}, - data:{ - config: dataSafe.data.Config + if (!dataSafe.success) { + return res.status(statusCodes.BAD_REQUEST).json({ + message: "Invalid input", + }); } - }) - if(updateNode) - return res.status(statusCodes.CREATED).json({ - message: "Node updated", - data: updateNode - }) + const updateNode = await prismaClient.node.update({ + where: { id: dataSafe.data.NodeId }, + data: { + position: dataSafe.data.position, + config: dataSafe.data.Config, + }, + }); - }catch(e){ - console.log("This is the error from Node updating", e); - return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ - message: "Internal server Error from Node Updation.", - }); + if (updateNode) + return res.status(statusCodes.CREATED).json({ + message: "Node updated", + data: updateNode, + }); + } catch (e) { + console.log("This is the error from Node updating", e); + return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Internal server Error from Node Updation.", + }); + } } -}) +); -router.put('/update/trigger', userMiddleware, async(req: AuthRequest, res: Response)=>{ - try{ - if(!req.user){ - return res.status(statusCodes.BAD_REQUEST).json({ +router.put( + "/update/trigger", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + if (!req.user) { + return res.status(statusCodes.BAD_REQUEST).json({ message: "User is not logged in ", }); - } - const data = req.body; - const dataSafe = TriggerUpdateSchema.safeParse(data) - - if(!dataSafe.success) - return res.status(statusCodes.BAD_REQUEST).json({ - message: "Invalid input" - }) - - const updatedTrigger = await prismaClient.trigger.update({ - where:{id: dataSafe.data.TriggerId}, - data:{ - config: dataSafe.data.Config } - }) + const data = req.body; + const dataSafe = TriggerUpdateSchema.safeParse(data); - if(updatedTrigger) - return res.status(statusCodes.CREATED).json({ - message: "Trigger updated", - data: updatedTrigger - }) - }catch(e){ - console.log("This is the error from Trigger updating", e); - return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ - message: "Internal server Error from Trigger Updation", - }); - } -}) + if (!dataSafe.success) + return res.status(statusCodes.BAD_REQUEST).json({ + message: "Invalid input", + }); -// ----------------------- GET WORKFLOW DATA(NODES, TRIGGER)--------------------- + const updatedTrigger = await prismaClient.trigger.update({ + where: { id: dataSafe.data.TriggerId }, + data: { + config: dataSafe.data.Config, + }, + }); + + if (updatedTrigger) + return res.status(statusCodes.CREATED).json({ + message: "Trigger updated", + data: updatedTrigger, + }); + } catch (e) { + console.log("This is the error from Trigger updating", e); + return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Internal server Error from Trigger Updation", + }); + } + } +); -// router.get('/getworkflowData', userMiddleware, async(req: AuthRequest, res: Response)=>{ -// try{ -// if(!req.user){ -// return res.status(statusCodes.BAD_REQUEST).json({ -// message: "User is not logged in ", -// }); -// } -// }catch(e){ -// } -// }) router.get("/protected", userMiddleware, (req: AuthRequest, res) => { return res.json({ diff --git a/apps/http-backend/tsconfig.tsbuildinfo b/apps/http-backend/tsconfig.tsbuildinfo index cd93c21..b0eadf1 100644 --- a/apps/http-backend/tsconfig.tsbuildinfo +++ b/apps/http-backend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/index.ts","./src/routes/google_callback.ts","./src/routes/nodes.routes.ts","./src/routes/userroutes/usermiddleware.ts","./src/routes/userroutes/userroutes.ts","./src/scheduler/token-scheduler.ts","./src/services/token-refresh.service.ts"],"version":"5.7.3"} \ No newline at end of file +{"root":["./src/index.ts","./src/routes/google_callback.ts","./src/routes/nodes.routes.ts","./src/routes/userRoutes/userMiddleware.ts","./src/routes/userRoutes/userRoutes.ts","./src/scheduler/token-scheduler.ts","./src/services/token-refresh.service.ts"],"version":"5.7.3"} \ No newline at end of file diff --git a/apps/web/app/components/NODES/BaseNode.tsx b/apps/web/app/components/NODES/BaseNode.tsx new file mode 100644 index 0000000..3eae0a9 --- /dev/null +++ b/apps/web/app/components/NODES/BaseNode.tsx @@ -0,0 +1,164 @@ +import { Handle, Position } from "@xyflow/react"; +interface BaseNodeProps { + id: string; + type: string; + data: { + label: string; + icon?: string; + isPlaceholder?: boolean; + config: any; + nodeType?: "trigger" | "action"; + + status?: "idle" | "running" | "success" | "error"; + onConfigure?: () => void; + onTest?: () => void; + onAddChild?: () => void; + }; +} + +export default function BaseNode({ id, type, data }: BaseNodeProps) { + const { + label, + icon, + isPlaceholder, + config, + onConfigure, + onAddChild, + onTest, + nodeType, + } = data; + + // For a node to be connectable, it must have handles. + // We always want placeholder and configured nodes to be connectable. + // TRIGGER nodes: Only source handle out (right) + // ACTION nodes: Both target handle in (left) and source handle out (right) + + if (isPlaceholder) { + return ( +
+ {/* Icon */} +
+ {icon || "➕"} +
+ {/* Label */} +
+

+ {label} +

+

Click to configure

+
+ + {/* Handles */} + {nodeType === "action" ? ( + <> + {/* Action placeholders get both handles -- left (input), right (output) */} + + + + ) : ( + // Trigger placeholders only output + + )} +
+ ); + } + + return ( +
+
+ {/* Icon + Label */} +
+ {icon || "📦"} + {label} +
+ {data.onConfigure && ( + + ✓ Configured + + )} + {/* Show config summary if exists */} + {/* {config && ( +
+ {config.summary + ? config.summary + : config.description + ? config.description + : "Configured"} +
+ )} */} + + {/* Buttons */} +
+ {onConfigure && ( + + )} + {onTest && ( + + )} +
+
+ + {/* Add child button */} + {onAddChild && ( +
+ +
+ )} + + {/* Handles */} + {nodeType === "action" ? ( + <> + {/* Action nodes get both handles */} + + + + ) : ( + // Trigger node gets only source handle (output) + + )} +
+ ); +} diff --git a/apps/web/app/components/nodes/TriggerNode.tsx b/apps/web/app/components/nodes/TriggerNode.tsx index 2fe21a7..d4cdb86 100644 --- a/apps/web/app/components/nodes/TriggerNode.tsx +++ b/apps/web/app/components/nodes/TriggerNode.tsx @@ -3,9 +3,9 @@ import { Handle, Position } from "@xyflow/react"; interface TriggerNodeProps { data: { name: string; - icon: string; + icon?: string; type: string; - config: Record; + config?: Record; }; } diff --git a/apps/web/app/components/ui/Design/WorkflowButton.tsx b/apps/web/app/components/ui/Design/WorkflowButton.tsx new file mode 100644 index 0000000..d431ff9 --- /dev/null +++ b/apps/web/app/components/ui/Design/WorkflowButton.tsx @@ -0,0 +1,18 @@ +"use client"; +import { useState } from "react"; +import { Button } from "@workspace/ui/components/button"; +import { CardDemo } from "./WorkflowCard"; + + +export default function ParentComponent() { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + {/* The Modal is conditionally rendered here */} + {isOpen && setIsOpen(false)} />} +
+ ); +} diff --git a/apps/web/app/components/ui/Design/WorkflowCard.tsx b/apps/web/app/components/ui/Design/WorkflowCard.tsx new file mode 100644 index 0000000..3be5e5e --- /dev/null +++ b/apps/web/app/components/ui/Design/WorkflowCard.tsx @@ -0,0 +1,126 @@ +import { useState } from "react"; +import { Button } from "@workspace/ui/components/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@workspace/ui/components/card"; +import { Input } from "@workspace/ui/components/input"; +import { Label } from "@workspace/ui/components/label"; +import { api } from "@/app/lib/api"; +import { useRouter} from "next/navigation" +interface CardDemoProps { + onClose?: () => void; +} + +export function CardDemo({ onClose }: CardDemoProps) { + const router = useRouter(); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + try { + setLoading(true); + // Use name as Name, description as Config (according to api signature) + const create = await api.workflows.create(name, []); + // Optionally: do something with create.data, e.g. inform user or fetch further data + const id = create.data.Data.id; + + router.push(`/workflows/${id}`); + if (onClose) onClose(); + } catch (error: any) { + setLoading(false); + if (error?.response?.data?.message) { + setError(error.response.data.message); + } else if (typeof error === "string") { + setError(error); + } else if (error?.message) { + setError(error.message); + } else { + setError("An unexpected error occurred."); + } + } + }; + + return ( +
+ e.stopPropagation()} + > + + Create Workflow + + + +
+
+ + setName(e.target.value)} + placeholder="Name of workflow" + required + disabled={loading} + /> +
+
+ + setDescription(e.target.value)} + placeholder="Workflow description (optional)" + disabled={loading} + /> +
+ {error &&
{error}
} +
+
+ + + + + + +
+
+ ); +} diff --git a/apps/web/app/lib/api.ts b/apps/web/app/lib/api.ts new file mode 100644 index 0000000..a785d8e --- /dev/null +++ b/apps/web/app/lib/api.ts @@ -0,0 +1,71 @@ +// lib/api.ts +import axios from "axios"; +import { BACKEND_URL, NodeUpdateSchema } from "@repo/common/zod"; +import { TriggerUpdateSchema } from "@repo/common/zod"; +import z from "zod" +// Wrap all API calls in async functions and make sure to set withCredentials: true and add Content-Type header. +export const api = { + workflows: { + create: async (name: string, Config: any) => + await axios.post( + `${BACKEND_URL}/user/create/workflow`, + { Name: name, Config: Config }, + { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + } + ), + get: async (id: string) => + await axios.get(`${BACKEND_URL}/user/workflow/${id}`, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + }, + triggers: { + getAvailable: async () => + await axios.get(`${BACKEND_URL}/user/getAvailableTriggers`, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + create: async (data: any) => + await axios.post(`${BACKEND_URL}/user/create/trigger`, data, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + update: async (data: z.infer) => { + await axios.put(`${BACKEND_URL}/user/update/trigger`, data, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }); + }, + }, + nodes: { + getAvailable: async () => + await axios.get(`${BACKEND_URL}/user/getAvailableNodes`, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + create: async (data: any) => + await axios.post(`${BACKEND_URL}/user/create/node`, data, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + update: async (data: z.infer) => + await axios.put(`${BACKEND_URL}/user/update/node`, data, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + }, + Credentials: { + getCredentials: async (type: string) => + await axios.get(`${BACKEND_URL}/user/getCredentials/${type}`, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + getAllCreds: async () => + await axios.get(`${BACKEND_URL}/user/getAllCreds`, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }), + }, +}; diff --git a/apps/web/app/lib/nodeConfigs/gmail.action.ts b/apps/web/app/lib/nodeConfigs/gmail.action.ts new file mode 100644 index 0000000..bb6df74 --- /dev/null +++ b/apps/web/app/lib/nodeConfigs/gmail.action.ts @@ -0,0 +1,50 @@ +import { NodeConfig } from "../types/node.types"; + +export const gmailActionConfig: NodeConfig = { + id: "gmail", + type: "action", + label: "Gmail", // ✅ Clean name + icon: "📧", // ✅ Email icon + description: "Send emails via Gmail", + credentials: "google", + + fields: [ + { + name: "credentialId", + label: "Google Account", + type: "dropdown", + required: true, + placeholder: "Select your Google account", + description: "Choose which Google account to use" + }, + { + name: "to", // ✅ Lowercase + label: "To", + type: "text", // ✅ Text input (single email) + required: true, + placeholder: "recipient@example.com", + description: "Email address of the receiver", + dependsOn: "credentialId" + }, + { + name: "subject", + label: "Subject", + type: "text", // ✅ Text input (short) + required: true, + placeholder: "Email subject", + description: "Subject line of the email" + // No dependsOn - subject is independent + }, + { + name: "body", // ✅ Lowercase + label: "Body", + type: "textarea", // ✅ Textarea for long content + required: true, + placeholder: "Email content...", + description: "Body content of the email" + } + ], + + summary: "Send emails via Gmail", // ✅ Correct description + helpUrl: "https://docs.example.com/gmail-action" +}; diff --git a/apps/web/app/lib/nodeConfigs/googleSheet.action.ts b/apps/web/app/lib/nodeConfigs/googleSheet.action.ts new file mode 100644 index 0000000..35b049b --- /dev/null +++ b/apps/web/app/lib/nodeConfigs/googleSheet.action.ts @@ -0,0 +1,53 @@ +import { NodeConfig } from "../types/node.types"; + +export const googleSheetActionConfig: NodeConfig = { + id: "google_sheet", + type: "action", + label: "Google Sheets", + icon: "📊", + description: "Read or write data to Google Sheets", + credentials: "google", // Requires Google OAuth + + fields: [ + { + name: "credentialId", + label: "Google Account", + type: "dropdown", + required: true, + placeholder: "Select your Google account", + description: "Choose which Google account to use" + }, + { + name: "spreadsheetId", + label: "Spreadsheet", + type: "dropdown", + required: true, + description: "Select the Google Spreadsheet", + dependsOn: "credentialId" // Only show after credential is selected + }, + { + name: "sheetName", + label: "Sheet Name", + type: "dropdown", + required: true, + description: "Select the specific sheet within the spreadsheet", + dependsOn: "spreadsheetId" // Only show after spreadsheet is selected + }, + { + name: "action", + label: "Action", + type: "dropdown", + options: [ + { label: "Read Rows", value: "read_rows" }, + { label: "Append Row", value: "append_row" }, + { label: "Update Row", value: "update_row" } + ], + required: true, + defaultValue: "read_rows", + description: "What operation to perform on the sheet" + } + ], + + summary: "Interact with Google Sheets spreadsheets", + helpUrl: "https://docs.example.com/google-sheets-action" +}; diff --git a/apps/web/app/lib/nodeConfigs/index.ts b/apps/web/app/lib/nodeConfigs/index.ts new file mode 100644 index 0000000..19afaf8 --- /dev/null +++ b/apps/web/app/lib/nodeConfigs/index.ts @@ -0,0 +1,27 @@ +import { NodeConfig } from "../types/node.types"; +import { gmailActionConfig } from "./gmail.action"; +import { googleSheetActionConfig } from "./googleSheet.action"; +import { webhookTriggerConfig } from "./webhook.trigger"; + +// 1. Create a dictionary of all your nodes +// We map them by their 'label' (what shows on the node) AND their 'id' (internal type) +// so we can find them easily either way. +export const NODE_CONFIG_REGISTRY: Record = { + // Map by Label (matches the `data.label` from your node) + [gmailActionConfig.label]: gmailActionConfig, + [googleSheetActionConfig.label]: googleSheetActionConfig, + [webhookTriggerConfig.label]: webhookTriggerConfig, + + // Map by ID (internal safety fallback) + [gmailActionConfig.id]: gmailActionConfig, // "gmail" + [googleSheetActionConfig.id]: googleSheetActionConfig, // "google_sheet" + [webhookTriggerConfig.id]: webhookTriggerConfig, // "webhook" +}; + +/** + * Helper to get the config object for a given node label or type. + * @param identifier The label (e.g. "Gmail") or id (e.g. "gmail") of the node + */ +export const getNodeConfig = (identifier: string): NodeConfig | null => { + return NODE_CONFIG_REGISTRY[identifier] || null; +}; diff --git a/apps/web/app/lib/nodeConfigs/webhook.trigger.ts b/apps/web/app/lib/nodeConfigs/webhook.trigger.ts new file mode 100644 index 0000000..10017ab --- /dev/null +++ b/apps/web/app/lib/nodeConfigs/webhook.trigger.ts @@ -0,0 +1,49 @@ +// import { NodeConfig } from '../types/node.types'; + +// export const webhookTriggerConfig: NodeConfig = { +// id: "webhook", +// type: "trigger", +// label: "Webhook", +// icon: "📡", +// description: "Trigger workflow on HTTP request", +// fields: [ +// { +// name: "path", +// label: "Webhook Path", +// type: "text", +// required: true, +// placeholder: "/api/webhook/12345", +// description: "The HTTP path where this webhook will listen. Must be unique per workflow." +// }, +// { +// name: "method", +// label: "HTTP Method", +// type: "dropdown", +// required: true, +// options: [ +// { label: "POST", value: "POST" }, +// { label: "GET", value: "GET" } +// ], +// defaultValue: "POST", +// description: "The HTTP method to accept (typically POST)." +// } +// ], +// summary: "Listen for HTTP requests on a unique webhook URL.", +// helpUrl: "https://docs.example.com/webhook-trigger" +// }; + +import { NodeConfig } from "../types/node.types"; + +export const webhookTriggerConfig: NodeConfig = { + id: "webhook", + type: "trigger", + label: "Webhook", + icon: "📡", + description: "Trigger workflow on HTTP request", + + // NO FIELDS! URL is auto-generated + fields: [], + + summary: "Receives HTTP requests to trigger workflow execution", + helpUrl: "https://docs.example.com/webhook-trigger", +}; diff --git a/apps/web/app/lib/types/node.types.ts b/apps/web/app/lib/types/node.types.ts new file mode 100644 index 0000000..f81e7de --- /dev/null +++ b/apps/web/app/lib/types/node.types.ts @@ -0,0 +1,39 @@ +// What information does a node config need? + +export interface NodeConfig { + id: string; // Unique identifier for the node, e.g., "google_sheet" + type: "trigger" | "action"; // Node category + label: string; // Display name, e.g., "Google Sheets" + icon: string; // Node icon, e.g., "📊" + description?: string; // Node description, e.g., "Add or update rows" + credentials?: string; // Reference to required credential set + fields: ConfigField[]; // All user-configurable fields + apiEndpoints?: { + // Optional: Endpoints this node interacts with, if dynamic + [action: string]: string; + }; + summary?: string; // Short summary of the node's configuration (for compact UI) + sampleData?: Record; // Example data the node works with + helpUrl?: string; // Documentation or help link + version?: string; // Node config/API version + tags?: string[]; // Searchable tags + // Any extra raw config data + data?: Record; // Allow extra arbitrary config as needed +} + +export interface ConfigField { + name: string; // Field's internal key, e.g., "sheetId" + label: string; // Human-readable label, e.g., "Sheet ID" + type: "text" | "dropdown" | "textarea" | "number" | "checkbox" | "password"; + required?: boolean; + defaultValue?: string | number | boolean; // Initial value if not set + placeholder?: string; + + options?: Array<{ label: string; value: string | number }>; // For dropdowns + dependsOn?: string; // Name of another field this depends on + description?: string; // Help text for this field + multiline?: boolean; // For textarea: allow specifying multiline + min?: number; // For number fields: min value + max?: number; // For number fields: max value + // You could add validation, inputMask, or other metadata as needed here +} \ No newline at end of file diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 354a5ce..fd1b77a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,5 +1,6 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/utils/auth"; +import ParentComponent from "./components/ui/Design/WorkflowButton"; export default async function Home() { const session = await getServerSession(AuthOptions); @@ -21,7 +22,11 @@ export default async function Home() { > Log out + +
+ + ) : ( <> diff --git a/apps/web/app/types/workflow.types.ts b/apps/web/app/types/workflow.types.ts index 21636db..4dc09a3 100644 --- a/apps/web/app/types/workflow.types.ts +++ b/apps/web/app/types/workflow.types.ts @@ -1,9 +1,9 @@ export interface NodeType { data: { - TYpe: "Trigger" | "Action"; + Type: "Trigger" | "Action"; SelectedType: string; label : string - config : JSON + config? : any }; id: string; @@ -12,7 +12,6 @@ export interface NodeType { export interface Edage { id: string; source: string; - target: string; } export interface AvailableTrigger { diff --git a/apps/web/app/workflows/[id]/components/ConfigModal.tsx b/apps/web/app/workflows/[id]/components/ConfigModal.tsx new file mode 100644 index 0000000..135c582 --- /dev/null +++ b/apps/web/app/workflows/[id]/components/ConfigModal.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { getNodeConfig } from "@/app/lib/nodeConfigs"; +import { useState } from "react"; +import { HOOKS_URL } from "@repo/common/zod"; +import { userAction } from "@/store/slices/userSlice"; +interface ConfigModalProps { + isOpen: boolean; + selectedNode: any | null; + onClose: () => void; + onSave: (config: any, userId: string) => Promise; +} + +export default function ConfigModal({ + isOpen, + selectedNode, + onClose, + onSave, +}: ConfigModalProps) { + const [loading, setLoading] = useState(false); + + if (!isOpen || !selectedNode) return null; + const userId =userAction.setUserId as unknown as string; + console.log("we are getting this userId from ConfigModal" , userId) + const handleSave = async () => { + setLoading(true); + try { + // For now, just save empty config + await onSave({ HOOKS_URL }, userId); + } catch (error) { + console.error("Save failed:", error); + } finally { + setLoading(false); + onClose(); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ Configure {selectedNode.name} +

+ +
+ + {/* Body */} +
+ {/* Show node info */} +
+

+ ID:{" "} + {selectedNode.id} +

+

+ Type:{" "} + {selectedNode.type} +

+
+ + {/* DYNAMIC FORM from registry */} + {(() => { + const nodeConfig = getNodeConfig( + selectedNode.name || selectedNode.actionType + ); + + if (!nodeConfig) { + return ( +

+ No config found for {selectedNode.name} +

+ ); + } + + if (nodeConfig.fields.length === 0) { + return ( +
+
+

+ {nodeConfig.label} +

+

{nodeConfig.description}

+ {nodeConfig.id === "webhook" && ( +
+

+ Webhook URL: +

+ + {`${typeof window !== "undefined" ? window.location.origin : ""}/api/webhooks/${selectedNode.id}`} + +

+ Copy this URL to trigger the workflow +

+
+ )} +
+ ); + } + + // Render fields dynamically (B&W) + return ( +
+ {nodeConfig.fields.map((field) => ( +
+ + {/* Render field based on type - only basic input for now */} + +
+ ))} +
+ ); + })()} +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/apps/web/app/workflows/[id]/components/nodes/PlaceholderNode.tsx b/apps/web/app/workflows/[id]/components/nodes/PlaceholderNode.tsx new file mode 100644 index 0000000..fc174ab --- /dev/null +++ b/apps/web/app/workflows/[id]/components/nodes/PlaceholderNode.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { Handle , Position } from "@xyflow/react"; + + +interface PlaceholderNodeProps { + data: { + onClick?: () => void; + }; +} + +export function PlaceholderNode({ data }: PlaceholderNodeProps) { + return ( +
+ + +
+
+ +
+

Add Action

+
+
+ ); +} diff --git a/apps/web/app/workflows/[id]/page.tsx b/apps/web/app/workflows/[id]/page.tsx new file mode 100644 index 0000000..fe9de91 --- /dev/null +++ b/apps/web/app/workflows/[id]/page.tsx @@ -0,0 +1,460 @@ +"use client"; + +import { useState, useEffect, act } from "react"; +import { useParams } from "next/navigation"; +import { + ReactFlow, + Node, + Edge, + Controls, + Background, + useNodesState, + useEdgesState, + NodeChange, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; + +import BaseNode from "@/app/components/NODES/BaseNode"; +import { TriggerSideBar } from "@/app/components/nodes/TriggerSidebar"; +import ActionSideBar from "@/app/components/Actions/ActionSidebar"; +import { api } from "@/app/lib/api"; +import ConfigModal from "./components/ConfigModal"; + +export default function WorkflowCanvas() { + const params = useParams(); + const workflowId = params.id as string; + + // State + const [nodes, setNodes, onNodesChange] = useNodesState([ + { + id: "trigger-placeholder", + type: "customNode", + position: { x: 250, y: 50 }, + data: { + label: "Add Trigger", + icon: "➕", + isPlaceholder: true, + nodeType: "trigger", + onConfigure: () => setTriggerOpen(true), // Opens sidebar! + }, + }, + ]); + + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [triggerOpen, setTriggerOpen] = useState(false); + const [actionOpen, setActionOpen] = useState(false); + const [configOpen, setConfigOpen] = useState(false); + const [selectedNode, setSelectedNode] = useState(null); + + const [error, setError] = useState("'"); + const nodeTypes = { + customNode: BaseNode, + }; + + const handleNodeConfigure = (node: any) => { + setSelectedNode(node); + setConfigOpen(true); + }; + + const handleNodesChange = (changes: NodeChange[]) => { + onNodesChange(changes); // Update UI first + + changes.forEach((change) => { + if (change.type === "position" && change.position) { + // Find the node in local state to check its type + const changedNode = nodes.find((n) => n.id === change.id); + + if (changedNode?.data?.nodeType === "trigger") { + // If it's a trigger node, update via trigger API + api.triggers.update({ + TriggerId: change.id, + Config: { + ...(typeof changedNode.data.config === "object" && + changedNode.data.config !== null + ? changedNode.data.config + : {}), + position: change.position, + }, + }); + } else { + // Otherwise, update in node table + api.nodes.update({ + NodeId: change.id, + position: change.position, + }); + } + } + }); + }; + // const handleActionSelection = async (action: any) => { + // try { + // // 1. Find trigger node + // const triggerNode = nodes.find( + // (n) => n.data.nodeType === "trigger" && !n.data.isPlaceholder + // ); + + // if (!triggerNode) { + // throw new Error("No trigger found"); + // } + + // const triggerId = triggerNode.id; + + // // 2. Find existing action nodes + // const existingActionNodes = nodes.filter( + // (n) => n.data.nodeType === "action" && !n.data.isPlaceholder + // ); + + // // 3. Calculate position for new action node + // let newPosition; + // if (existingActionNodes.length === 0) { + // // First action: place below trigger + // newPosition = { + // x: triggerNode.position.x, + // y: triggerNode.position.y + 150 // 150px below trigger + // }; + // } else { + // // Subsequent actions: place below last action + // const lastAction = existingActionNodes[existingActionNodes.length - 1]; + // newPosition = { + // x: lastAction!.position.x, + // y: lastAction!.position.y + 150 // 150px below last action + // }; + // } + + // const sourceNodeId = existingActionNodes.length > 0 + // ? existingActionNodes[existingActionNodes.length - 1]!.id + // : triggerId; + + // // 4. Call API + // const result = await api.nodes.create({ + // Name: action.name, + // AvailableNodeId: action.id, + // Config: { + // Position : newPosition + // }, + // WorkflowId: workflowId, + // Position: 1, // ✅ Dynamic position! + + // }); + + // const actionId = result.data.data.id; + + // // 5. Create action node with calculated position + // const newActionNode = { + // id: actionId, + // type: "customNode", + // position: newPosition, // ✅ Use calculated position + // data: { + // label: action.name, + // icon: action.icon, + // isPlaceholder: false, + // nodeType: "action", // ✅ Fixed from earlier + // config: {}, + // onConfigure: () => console.log("Configure", actionId), + // }, + // }; + + // // 6. Create placeholder below new action + // const placeholderPosition = { + // x: newPosition.x, + // y: newPosition.y + 150 // 150px below new action + // }; + + // const newPlaceholder = { + // id: `action-placeholder-${Date.now()}`, + // type: "customNode", + // position: placeholderPosition, + // data: { + // label: "Add Action", + // icon: "➕", + // isPlaceholder: true, + // nodeType: "action", + // config: {}, + // onConfigure: () => setActionOpen(true), + // }, + // }; + + // // Rest of your code stays the same... + // setNodes((prevNodes) => { + // const filtered = prevNodes.filter( + // (n) => !(n.data.isPlaceholder && n.data.nodeType === "action") + // ); + // return [...filtered, newActionNode, newPlaceholder]; + // }); + + // setEdges((prevEdges) => { + // const filtered = prevEdges.filter((e) => { + // const targetNode = nodes.find(n => n.id === e.target); + // return !(targetNode?.data.isPlaceholder && targetNode?.data.nodeType === "action"); + // }); + + // return [ + // ...filtered, + // { + // id: `e-${sourceNodeId}-${actionId}`, + // source: sourceNodeId, + // target: actionId, + // }, + // { + // id: `e-${actionId}-${newPlaceholder.id}`, + // source: actionId, + // target: newPlaceholder.id, + // }, + // ]; + // }); + + // setActionOpen(false); + // } catch (error: any) { + // setError(error); + // } + // }; + + const handleActionSelection = async (action: any) => { + try { + // onSelectAction: (action: { id: string; name: string; type: string; icon?: string }) => void; + + // 1. Call API to create action in DB + console.log("This is node Id before log", action.id); + + const result = await api.nodes.create({ + Name: action.name, + AvailableNodeId: action.id, + Config: { + CredentialsID: "", + }, + WorkflowId: workflowId, + Position: 1, + }); + console.log("This is node Id before log", action.id); + const actionId = result.data.data.id; + + // 2. Create action node + const newNode = { + id: actionId, + type: "customNode", + position: { x: 350, y: 400 }, + data: { + label: action.name, + icon: action.icon, + isPlaceholder: false, + nodeType: "action", + config: {}, + onConfigure: () => + handleNodeConfigure({ + id: actionId, + name: action.name, + type: "action", + actionType: action.id, + }), + + // onConfigure: () => console.log("Configure", actionId), + }, + }; + + // 3. Create NEW placeholder + const actionPlaceholder = { + id: `action-placeholder-${Date.now()}`, + type: "customNode", + position: { x: 550, y: 200 }, + data: { + label: "Add Action", + icon: "➕", + isPlaceholder: true, + nodeType: "action", + config: {}, + onConfigure: () => setActionOpen(true), + }, + }; + + // Find the current trigger node (non-placeholder) + const triggerNode = nodes.find( + (n) => n.data.nodeType === "trigger" && !n.data.isPlaceholder + ); + + if (!triggerNode) { + throw new Error("No trigger found"); + } + + const triggerId = triggerNode.id; + + // Remove old action placeholder and add new action + new placeholder + setNodes((prevNodes) => { + const filtered = prevNodes.filter( + (n) => !(n.data.isPlaceholder && n.data.nodeType === "action") + ); + + return [...filtered, newNode, actionPlaceholder]; + }); + + // Determine the previous latest action node (for correct edge chaining) + setEdges((prevEdges) => { + // Remove edges pointing to the old placeholder + const filtered = prevEdges.filter((e) => { + const targetNode = nodes.find((n) => n.id === e.target); + return !( + targetNode?.data.isPlaceholder && + targetNode?.data.nodeType === "action" + ); + }); + + // Find all non-placeholder action nodes in the CURRENT state (after newNode is added) + const currentActionNodes = [ + ...nodes.filter( + (n) => n.data.nodeType === "action" && !n.data.isPlaceholder + ), + newNode, + ]; + + // Find all existing action nodes (NOT including newNode yet) + const existingActionNodes = nodes.filter( + (n) => n.data.nodeType === "action" && !n.data.isPlaceholder + ); + + // Source is the last existing action, or trigger if this is first action + const sourceNodeId = + existingActionNodes.length > 0 + ? existingActionNodes[existingActionNodes.length - 1]!.id + : triggerId; + + return [ + ...filtered, + // Edge from previous node (trigger or prev action) to the new action + { + id: `e-action-${sourceNodeId}-${actionId}`, + source: sourceNodeId, + target: actionId, + }, + // Edge from new action to new placeholder + { + id: `e-action-${actionId}-placeholder`, + source: actionId, + target: actionPlaceholder.id, + }, + ]; + }); + + setActionOpen(false); + } catch (error: any) { + setError(error); + } + }; + + const handleSelection = async (trigger: any) => { + console.log("THe trigger name is ", trigger.name); + + try { + const result = await api.triggers.create({ + Name: trigger.name, + AvailableTriggerID: trigger.id, + Config: {}, + WorkflowId: workflowId, + TriggerType: trigger.type, + }); + const triggerId = result.data.data.id as string; + console.log("The Trigger Id is : ", triggerId); + + const newNode = { + id: triggerId, + type: "customNode", + position: { x: 250, y: 50 }, + data: { + label: trigger.name, + icon: trigger.icon, + isPlaceholder: false, + nodeType: "trigger", + config: {}, + // onConfigure: () => console.log("Configure", triggerId), + onConfigure: () => + handleNodeConfigure({ + id: triggerId, + name: trigger.name, + type: "trigger", + }), + }, + }; + + const actionPlaceholder = { + id: "action-holder", + type: "customNode", + position: { x: 550, y: 200 }, + data: { + label: "Add Action", + icon: "➕", + isPlaceholder: true, + nodeType: "action", + config: {}, + onConfigure: () => setActionOpen(true), + }, + }; + + setNodes([newNode, actionPlaceholder]); + setEdges([ + { + id: "e1", + source: triggerId, + target: "action-holder", + }, + ]); + setTriggerOpen(false); + } catch (error: any) { + setError(error); + } + }; + return ( +
+ + + + + + {/* ADD THIS MODAL */} + { + setConfigOpen(false); + setSelectedNode(null); + }} + onSave={async (nodeId, config) => { + // const isTrigger = + const isTrigger = + nodes.find((n) => n.id === nodeId)?.data.nodeType === "trigger"; + if (isTrigger) { + await api.triggers.update({ TriggerId: nodeId, Config: config }); + } else { + await api.nodes.update({ NodeId: nodeId, Config: config }); + } + + setNodes((prevNodes) => + prevNodes.map((node) => + node.id === nodeId + ? { + ...node, + data: { ...node.data, config, isConfigured: true }, + } + : node + ) + ); + }} + /> + setTriggerOpen(false)} + onSelectTrigger={handleSelection} + /> + + setActionOpen(false)} + onSelectAction={handleActionSelection} + /> +
+ ); +} diff --git a/apps/worker/src/engine/executor.ts b/apps/worker/src/engine/executor.ts index a12a5c9..631046f 100644 --- a/apps/worker/src/engine/executor.ts +++ b/apps/worker/src/engine/executor.ts @@ -34,13 +34,13 @@ export async function executeWorkflow( status: "InProgress", }, }); - if(!update.error) console.log('updated the workflow execution') + if (!update.error) console.log("updated the workflow execution"); const nodes = data?.workflow.nodes; - console.log(`Total nodes - ${nodes.length}`) + console.log(`Total nodes - ${nodes.length}`); for (const node of nodes) { - console.log(`${node.name}, ${node.position}th - started Execution`) + console.log(`${node.name}, ${node.position}th - started Execution`); const nodeType = node.AvailableNode.type; const context = { // nodeId: node.id, @@ -49,21 +49,21 @@ export async function executeWorkflow( config: node.config as Record, inputData: currentInputData, }; - console.log(`Executing with context: ${context}`) + console.log(`Executing with context: ${context}`); const execute = await register.execute(nodeType, context); - // if (!execute.success) { - // await prismaClient.workflowExecution.update({ - // where: { id: workflowExecutionId }, - // data: { - // status: "Failed", - // error: execute.error, - // completedAt: new Date(), - // }, - // }); - // return; - // } + if (!execute.success) { + await prismaClient.workflowExecution.update({ + where: { id: workflowExecutionId }, + data: { + status: "Failed", + error: execute.error, + completedAt: new Date(), + }, + }); + return; + } currentInputData = execute.output; - console.log("output: ", JSON.stringify(execute)) + console.log("output: ", JSON.stringify(execute)); } const updatedStatus = await prismaClient.workflowExecution.update({ where: { id: workflowExecutionId }, diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 5a7b34f..b3d9b27 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,53 +1,53 @@ import z from "zod"; -import { string } from "zod/v4"; - - -export const BACKEND_URL="http://localhost:3002"; +import { number } from "zod/v4"; +export const BACKEND_URL = "http://localhost:3002"; +export const HOOKS_URL = "http://localhost:3002"; export const AvailableTriggers = z.object({ Name: z.string(), AvailableTriggerID: z.string().optional(), Config: z.any().optional(), - Type : z.string() + Type: z.string(), }); export const AvailableNodes = z.object({ Name: z.string(), AvailableNodeId: z.string().optional(), Config: z.any(), - Type : z.string() + Type: z.string(), }); export const TriggerSchema = z.object({ Name: z.string(), AvailableTriggerID: z.string(), - Config: z.any(), + Config: z.any().optional(), WorkflowId: z.string(), - TriggerType: z.string(), + TriggerType: z.string().optional(), }); export const NodeSchema = z.object({ Name: z.string(), AvailableNodeId: z.string(), - Config: z.any(), - Position: z.number(), + Config: z.any().optional(), + stage: z.number().optional(), WorkflowId: z.string(), }); export const NodeUpdateSchema = z.object({ NodeId: z.string(), - Config: z.any() -}) + Config: z.any().optional(), + position: z.any().optional(), +}); export const TriggerUpdateSchema = z.object({ TriggerId: z.string(), - Config: z.any() -}) + Config: z.any(), +}); export const WorkflowSchema = z.object({ Name: z.string(), Config: z.any(), - + description: z.string().optional(), }); export enum statusCodes { diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 602b1bf..2773c98 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -1,6 +1,5 @@ generator client { provider = "prisma-client-js" - // output = "../node_modules/.prisma/client" } datasource db { @@ -20,13 +19,14 @@ model User { } model Credential { - id String @id @default(uuid()) - userId String - type String - config Json - nodeId String? - node Node? @relation(fields: [nodeId], references: [id]) - user User @relation(fields: [userId], references: [id]) + id String @id @default(uuid()) + userId String + type String + config Json + nodeId String? + node Node? @relation(fields: [nodeId], references: [id]) + user User @relation(fields: [userId], references: [id]) + // Node_Node_CredentialsIDToCredential Node[] @relation("Node_CredentialsIDToCredential") } model Trigger { @@ -59,16 +59,19 @@ model AvailableNode { } model Node { - id String @id @default(uuid()) - name String - config Json - position Int - workflowId String? - AvailableNodeID String - credentials Credential[] - AvailableNode AvailableNode @relation(fields: [AvailableNodeID], references: [id]) - workflow Workflow? @relation(fields: [workflowId], references: [id]) - executions NodeExecution[] + id String @id @default(uuid()) + name String + config Json + position Json + stage Int + workflowId String? + AvailableNodeID String + // CredentialsID String + credentials Credential[] + AvailableNode AvailableNode? @relation(fields: [AvailableNodeID], references: [id]) + // Credential_Node_CredentialsIDToCredential Credential @relation("Node_CredentialsIDToCredential", fields: [CredentialsID], references: [id], onDelete: Cascade) + workflow Workflow? @relation(fields: [workflowId], references: [id]) + executions NodeExecution[] } model Workflow { @@ -103,8 +106,8 @@ model WorkflowExecution { model NodeExecution { id String @id @default(uuid()) nodeId String - startedAt DateTime @default(now()) - completedAt DateTime? + startedAt DateTime @default(dbgenerated("(now() AT TIME ZONE 'utc'::text)")) @db.Timestamptz(6) + completedAt DateTime? @default(dbgenerated("(now() AT TIME ZONE 'utc'::text)")) @db.Timestamptz(6) inputData Json? outputData Json? error String? diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 63ff3d4..83b0e41 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -56,7 +56,8 @@ async function setupTestWorkflow() { create: { id: "test_node_001", name: "Send Test Email", - position: 1, + stage: 1, + position : {x : 250 , y : 200}, workflowId: workflow.id, AvailableNodeID: availableNode.id, config: { diff --git a/packages/db/tsconfig.tsbuildinfo b/packages/db/tsconfig.tsbuildinfo index 6ea85aa..968d9e9 100644 --- a/packages/db/tsconfig.tsbuildinfo +++ b/packages/db/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/index.ts","./src/seed.ts"],"version":"5.7.3"} \ No newline at end of file +{"root":["./src/index.ts","./src/seed.ts"],"version":"5.9.2"} \ No newline at end of file diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index 6530d11..e6d8e49 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@workspace/ui/lib/utils" +import { cn } from "@workspace/ui/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -34,7 +34,7 @@ const buttonVariants = cva( size: "default", }, } -) +); function Button({ className, @@ -44,9 +44,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx new file mode 100644 index 0000000..2e7392a --- /dev/null +++ b/packages/ui/src/components/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@workspace/ui/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}