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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
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 (
+
+ );
+}
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,
+}