Skip to content

feat: integrate redux-persist for workflow state management#73

Merged
Vamsi-o merged 1 commit into
mainfrom
auto-save
Mar 10, 2026
Merged

feat: integrate redux-persist for workflow state management#73
Vamsi-o merged 1 commit into
mainfrom
auto-save

Conversation

@Teja-Budumuru

@Teja-Budumuru Teja-Budumuru commented Mar 9, 2026

Copy link
Copy Markdown
Contributor
  • Added redux-persist to persist workflow state across sessions.
  • Updated workflow slice to include new state structure for managing nodes, edges, and trigger configurations.
  • Refactored WorkflowCanvas component to utilize Redux for state management, including auto-saving and node configuration.
  • Enhanced error handling and loading states during workflow execution and configuration.
  • Adjusted API interactions to align with new state structure and persist changes.
  • Updated Prisma schema to accommodate new fields for triggers and nodes.
  • Modified common validation schemas to include optional position and credential fields.

Summary by CodeRabbit

  • New Features

    • Added automatic workflow saving every 30 seconds
    • Implemented persistent app state across browser sessions
  • Bug Fixes

    • Fixed incorrect error response for unauthenticated requests
  • Refactor

    • Optimized workflow configuration and data synchronization
    • Improved OAuth authentication routing
    • Enhanced internal state management for better data consistency

- Added redux-persist to persist workflow state across sessions.
- Updated workflow slice to include new state structure for managing nodes, edges, and trigger configurations.
- Refactored WorkflowCanvas component to utilize Redux for state management, including auto-saving and node configuration.
- Enhanced error handling and loading states during workflow execution and configuration.
- Adjusted API interactions to align with new state structure and persist changes.
- Updated Prisma schema to accommodate new fields for triggers and nodes.
- Modified common validation schemas to include optional position and credential fields.
Copilot AI review requested due to automatic review settings March 9, 2026 17:00
@Teja-Budumuru Teja-Budumuru requested a review from Vamsi-o as a code owner March 9, 2026 17:00
@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces auto-save functionality for workflows with Redux state persistence. Key changes include restructuring the workflow state model with explicit change tracking, integrating redux-persist for session persistence, refactoring the workflow UI to use Redux dispatch instead of direct API calls, and updating backend routes to support new position and credential fields for triggers and nodes.

Changes

Cohort / File(s) Summary
Backend Route Configuration
apps/http-backend/src/index.ts
OAuth route path changed from /oauth/google to /auth/google.
Backend Workflow API
apps/http-backend/src/routes/userRoutes/userRoutes.ts
Updated node/trigger retrieval ordering by stage; adjusted trigger and node creation/update payloads to support Position and Config fields; changed unauthenticated response from BAD_GATEWAY to UNAUTHORIZED.
Redux Persistence Integration
apps/web/package.json, apps/web/app/components/providers.tsx, apps/web/store/index.ts
Added redux-persist dependency; wrapped providers with PersistGate; configured store with persistedReducer and persistor export with SSR-friendly storage fallback.
Redux Workflow State Restructure
apps/web/store/slices/workflowSlice.ts
Introduced new Position, EdgeItem, and Workflow interfaces; restructured state with nested data object and explicit change tracking (isChanged, changedNodeIds, lastSynced); renamed Trigger/Node id fields to TriggerId/NodeId; added new reducers for position/config updates and syncing.
Auto-Save Workflow Hook
apps/web/app/hooks/useAutoSave.ts
New hook implementing 30-second auto-save interval with change detection from Redux; batches updates to nodes, triggers, and edges; handles beforeunload events and dispatches markSynced on success.
Workflow UI Components
apps/web/app/workflows/[id]/components/ConfigModal.tsx, apps/web/app/workflows/[id]/page.tsx
Removed onSave prop from ConfigModal; added Redux dispatch for config changes; refactored workflow loading to use Redux state; replaced direct API calls with Redux dispatch for node/edge mutations; integrated useAutoSave hook for auto-save status display.
API Type Safety
apps/web/app/lib/api.ts
Updated api.workflows.put and api.nodes.create method signatures to accept typed Zod schemas instead of any.
Database and Schema
packages/db/prisma/schema.prisma, packages/common/src/index.ts
Made Trigger.config optional; added Trigger.Position field; added optional CredentialId to NodeSchema; made TriggerUpdateSchema.Config optional and added CredentialID and Position fields.
Node Trigger Registration
packages/nodes/src/gmail/gmail.node.ts, packages/nodes/src/google-sheets/google-sheets.node.ts
Commented out trigger registration calls in both gmail and google-sheets node register methods.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant React as React Component
    participant Hook as useAutoSave Hook
    participant Redux as Redux Store
    participant API as API Client
    participant Backend as HTTP Backend
    
    User->>React: Modifies workflow (drag node, edit config)
    React->>Redux: dispatch(updateNodePosition/Config)
    Redux->>React: State updated
    React->>React: Marks isChanged flag
    
    Note over Hook: 30-second interval elapses
    Hook->>Hook: batchSave triggered
    Hook->>Redux: Read current workflow state
    Hook->>Redux: Set saveStatus to "saving"
    
    alt Changes detected
        Hook->>API: api.nodes.update (changed nodes)
        Hook->>API: api.triggers.update (if trigger changed)
        Hook->>API: api.workflows.put (if edges changed)
        
        API->>Backend: POST/PUT requests
        Backend->>Backend: Update database
        Backend-->>API: Success response
        
        API-->>Hook: Updates complete
        Hook->>Redux: dispatch(markSynced)
        Redux->>Redux: Clear isChanged flags
        Hook->>React: saveStatus = "saved"
    else No changes
        Hook->>React: saveStatus remains "saved"
    end
    
    User->>React: Trigger beforeunload (page close)
    React->>Hook: useAutoSave cleanup
    Hook->>Hook: batchSave if unsaved changes
    React->>User: Page unloads after save
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Vamsi-o

Poem

🐰 Hops through workflows with boundless cheer,
Redux persists what we hold dear,
Auto-save saves the day with grace,
Position and config find their place,
No more lost work, just smooth delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the pull request—integrating redux-persist for workflow state management—which is a central theme evident across multiple files including store configuration, workflow slice restructuring, and component refactoring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auto-save

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR integrates redux-persist to persist workflow editing state in the web app, refactors workflow state management to Redux (nodes/edges/trigger config + autosave), and aligns backend + Prisma + shared Zod schemas with the updated workflow data shape.

Changes:

  • Add redux-persist and wire PersistGate + persisted reducer to keep the workflow slice across sessions.
  • Redesign the workflow Redux slice to store trigger/nodes/edges plus “dirty” tracking and batched autosave.
  • Update backend routes, Prisma schema, and shared Zod validators to support optional trigger config/position and node credential fields.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds lock entries for redux-persist.
apps/web/package.json Adds redux-persist dependency.
apps/web/store/index.ts Wraps the root reducer with persistReducer and exports a persistor.
apps/web/app/components/providers.tsx Wraps the app in PersistGate to rehydrate persisted Redux state.
apps/web/store/slices/workflowSlice.ts Introduces new normalized workflow state + dirty tracking reducers.
apps/web/app/hooks/useAutoSave.ts Adds interval-based + manual “batch save” to persist changed nodes/trigger/edges.
apps/web/app/workflows/[id]/page.tsx Refactors workflow canvas to use Redux state + autosave and improved pre-execution validation.
apps/web/app/workflows/[id]/components/ConfigModal.tsx Dispatches config updates directly into Redux and loads existing config from Redux.
apps/web/app/lib/api.ts Tightens typings for workflow updates and node creation using shared Zod schemas.
packages/common/src/index.ts Expands Zod schemas for trigger position + node credential id + optional config updates.
packages/db/prisma/schema.prisma Makes trigger config optional and adds a trigger Position JSON field.
apps/http-backend/src/routes/userRoutes/userRoutes.ts Aligns workflow fetch ordering, trigger/node creation & update handling with new fields.
apps/http-backend/src/index.ts Changes Google OAuth router mount path.
packages/nodes/src/google-sheets/google-sheets.node.ts Stops registering this node as an available trigger.
packages/nodes/src/gmail/gmail.node.ts Stops registering this node as an available trigger.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

You can also share your feedback on Copilot code review. Take the survey.

import { useAppDispatch, useAppSelector } from "@/app/hooks/redux";
import { useAutoSave } from "@/app/hooks/useAutoSave";
import { workflowActions } from "@/store/slices/workflowSlice";
import { store } from "@/store";

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store is imported here but no longer used (remaining references are commented out). This will trigger ESLint no-unused-vars warnings in the web app lint config; please remove the unused import.

Copilot uses AI. Check for mistakes.
const workflowId = params.id as string;
const dispatch = useAppDispatch()
const reduxWorkflow = useAppSelector(s => s.workflow)
const { saveStatus, batchSave, displayStatus } = useAutoSave(workflowId)

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveStatus is destructured from useAutoSave but not used in this component. Since the repo enables TypeScript-ESLint recommended rules, this will be reported as an unused variable; consider omitting it from the destructuring.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +47
export interface WorkflowSlice {
data: Workflow;
isChanged: {
trigger: boolean;
nodes: boolean;

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkflowSlice state shape has been changed to { data, isChanged, ... }, but there are still app components selecting the old fields (e.g. s.workflow.workflow_id, s.workflow.trigger, s.workflow.nodes). This will break type-checking/runtime state reads unless those callers are updated or you provide a backwards-compatible adapter (e.g. keep deprecated fields or selectors).

Copilot uses AI. Check for mistakes.
app.use("/user" , userRouter)
app.use('/node', sheetRouter)
app.use('/oauth/google', googleAuth)
app.use('/auth/google', googleAuth)

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the mounted OAuth router path to /auth/google appears to break existing OAuth URLs/redirect URIs that still use /oauth/google/... (e.g. apps/http-backend/src/routes/google_callback.ts defaults to http://localhost:3002/oauth/google/callback). Please update the redirect URI defaults and any frontend/back-end links to match the new mount path (or keep the old path as an alias).

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +78
return ()=>{
clearInterval(interval);
window.removeEventListener("beforeunload", handleBeforeUnload)
batchSave()
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

batchSave() is invoked in the effect cleanup; since batchSave calls setSaveStatus, this can trigger React warnings about setting state on an unmounted component. Consider avoiding state updates during unmount (e.g. guard with an isMounted ref, or split the persistence side-effect from the UI status updates).

Copilot uses AI. Check for mistakes.
Comment on lines 187 to +191
const updatedConfig = ({ ...config, [fieldName]: value })
console.log(fieldName, " ", value, " ", nodeConfig)
console.log(config, "from handle field function - 1")
setConfig((prev) => ({ ...prev, [fieldName]: value }));
dispatchConfig(updatedConfig);

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleFieldChange builds updatedConfig from the stale config closure, but then updates state via the functional setConfig(prev => ...). This can cause Redux to receive a different config than the local state when changes happen quickly. Consider deriving a single nextConfig from prev and using that for both setConfig and dispatchConfig (and for dependent-field option fetching).

Copilot uses AI. Check for mistakes.
Comment on lines 216 to +220
useEffect(() => {
setConfig({});
// We no longer set local credentials here; handled by useCredentials!
if (!selectedNode) {
setConfig({});
return;
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When selectedNode changes, the effect loads loadedConfig but never resets dynamicOptions. This can leave dropdown options from the previous node in state and show incorrect options for the newly selected node. Consider clearing dynamicOptions (and any other per-node derived state like activeField/testResult) at the start of this effect.

Copilot uses AI. Check for mistakes.
Comment on lines +584 to +588

...(dataSafe.data.position !== undefined ? { position: dataSafe.data.position } : {}),
...(dataSafe.data.Config !== undefined ? { config: dataSafe.data.Config } : {}),
...(dataSafe.data.Config?.credentialId ? { CredentialsID: dataSafe.data.Config.credentialId } : {})
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated node update logic only sets CredentialsID when Config.credentialId is truthy, but never clears it when credentials are removed/emptied in the config. This can leave a stale credential relationship in the DB. Consider explicitly setting CredentialsID to null when Config is provided and credentialId is missing/empty (or accept a top-level credential field for updates).

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/http-backend/src/routes/userRoutes/userRoutes.ts (2)

622-635: ⚠️ Potential issue | 🟡 Minor

Incorrect status code for update operation.

Line 632 returns CREATED (201) for an update operation. This should return OK (200) since no new resource is being created.

Proposed fix
       if (updatedTrigger)
-        return res.status(statusCodes.CREATED).json({
+        return res.status(statusCodes.OK).json({
           message: "Trigger updated",
           data: updatedTrigger,
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/http-backend/src/routes/userRoutes/userRoutes.ts` around lines 622 -
635, The handler currently returns statusCodes.CREATED after updating an
existing Trigger; locate the prismaClient.trigger.update call and the following
if (updatedTrigger) branch and change the response status from
statusCodes.CREATED to statusCodes.OK so an update returns HTTP 200 (OK) instead
of 201 (Created) when sending the res.status(...).json({ message: "Trigger
updated", data: updatedTrigger }).

581-595: ⚠️ Potential issue | 🟡 Minor

Two issues: duplicate credential storage and incorrect status code.

  1. Credential duplication: Line 587 extracts credentialId from inside Config and stores it in CredentialsID. However, if Config is also stored (line 586), the credentialId will exist in both the config JSON field and the CredentialsID column. Consider stripping credentialId from Config before saving, as the commented code on line 580 originally intended.

  2. Status code: Line 592 returns CREATED (201) for an update operation. Updates should return OK (200) instead.

Proposed fix
       const updateNode = await prismaClient.node.update({
         where: { id: dataSafe.data.NodeId },
         data: {
-
         ...(dataSafe.data.position !== undefined ? { position: dataSafe.data.position } : {}),
-        ...(dataSafe.data.Config !== undefined ? { config: dataSafe.data.Config } : {}),
-        ...(dataSafe.data.Config?.credentialId ? { CredentialsID: dataSafe.data.Config.credentialId } : {})
+        ...(dataSafe.data.Config !== undefined ? {
+          config: (() => {
+            const { credentialId, ...restConfig } = dataSafe.data.Config || {};
+            return restConfig;
+          })()
+        } : {}),
+        ...(dataSafe.data.Config?.credentialId ? { CredentialsID: dataSafe.data.Config.credentialId } : {}),
+        ...(dataSafe.data.CredentialId !== undefined ? { CredentialsID: dataSafe.data.CredentialId } : {})
         }
       });
 
       if (updateNode)
-        return res.status(statusCodes.CREATED).json({
+        return res.status(statusCodes.OK).json({
           message: "Node updated",
           data: updateNode,
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/http-backend/src/routes/userRoutes/userRoutes.ts` around lines 581 -
595, The update block currently writes credentialId both into the config JSON
and into CredentialsID and returns a 201; change prismaClient.node.update usage
so you remove/omit credentialId from dataSafe.data.Config before including it in
the update (e.g., build a cleanedConfig that copies dataSafe.data.Config without
credentialId and use that for the config field) while still setting
CredentialsID to the extracted credentialId when present (referencing the
existing updateNode and prismaClient.node.update usage and
dataSafe.data.Config/CredentialsID symbols), and change the HTTP response to
return statusCodes.OK instead of statusCodes.CREATED for the update response.
apps/web/app/workflows/[id]/components/ConfigModal.tsx (1)

347-347: ⚠️ Potential issue | 🟡 Minor

Remove console.log in JSX render.

Line 347 has a console.log(options) inside the JSX return, which will log on every render. This should be removed for production code.

🧹 Remove debug log
-            {console.log(options)}
             <option value="">Select {field.label.toLowerCase()}</option>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx at line 347, Remove
the debug console.log by deleting the console.log(options) expression that was
placed inside the JSX return of the ConfigModal component (the render for
ConfigModal in ConfigModal.tsx). If you need to keep the log for local
debugging, move it out of the JSX into a useEffect or temporary development-only
block (e.g., useEffect(() => { console.log(options) }, [options])) so it doesn't
execute on every render or ship to production.
🧹 Nitpick comments (13)
apps/http-backend/src/routes/userRoutes/userRoutes.ts (2)

591-595: Missing fallback response if update returns falsy value.

If updateNode is falsy, no response is sent and the request will hang. While Prisma's update() typically throws on failure (e.g., record not found), defensive coding suggests either removing the conditional check or adding an else branch.

Proposed fix - remove unnecessary check
-      if (updateNode)
-        return res.status(statusCodes.CREATED).json({
-          message: "Node updated",
-          data: updateNode,
-        });
+      return res.status(statusCodes.OK).json({
+        message: "Node updated",
+        data: updateNode,
+      });

The same pattern applies to the trigger update route (lines 631-635).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/http-backend/src/routes/userRoutes/userRoutes.ts` around lines 591 -
595, The handler currently only returns a response when updateNode is truthy,
causing the request to hang if it's falsy; update the code in the route handler
that calls updateNode (and the analogous handler calling updateTrigger) to
always send a response: either remove the unnecessary conditional and always
return the created/updated payload, or add an else branch that returns a proper
fallback (e.g., res.status(404).json({ message: "Node not found" }) or
res.status(500) for unexpected failures). Ensure the change is applied to the
block around the updateNode response and the similar block around updateTrigger
so every execution path ends with a res.* call.

465-474: Consider allowing null instead of defaulting to empty object.

The Prisma schema defines Position Json? as nullable. Defaulting to {} when Position is not provided stores an empty object instead of null, which may have different semantics when querying or checking for "no position set."

Consider this alternative if null semantics are preferred
-          Position: dataSafe.data.Position || {}
+          Position: dataSafe.data.Position ?? null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/http-backend/src/routes/userRoutes/userRoutes.ts` around lines 465 -
474, The code currently forces Position to an empty object when missing, but the
Prisma field Position is nullable (Json?), so change the create payload in
prismaClient.trigger.create to pass null (or omit the Position key) when
dataSafe.data.Position is undefined to preserve null semantics; locate the
prismaClient.trigger.create call and replace the unconditional Position:
dataSafe.data.Position || {} with logic that sets Position to
dataSafe.data.Position ?? null (or conditionally exclude Position from the data
object) so missing positions are stored as null instead of {}.
packages/common/src/index.ts (1)

2-2: Unused import.

The number import from zod/v4 is not used anywhere in this file.

🧹 Proposed fix
-import { number } from "zod/v4";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/index.ts` at line 2, Remove the unused named import
"number" from "zod/v4" in this module (the import statement that reads import {
number } from "zod/v4"); simply delete the unused import so the file no longer
imports the unused symbol "number" and run lint/tsc to ensure no other
references remain.
apps/web/app/workflows/[id]/components/ConfigModal.tsx (2)

65-73: Consider debouncing Redux dispatches on config changes.

dispatchConfig is called on every keystroke in text fields (Lines 370-374, 395-399). This causes frequent Redux state updates and potential performance issues, especially with redux-persist writing to localStorage.

⚡ Proposed optimization with debounce
+import { useMemo } from "react";
+import { debounce } from "lodash"; // or implement a simple debounce

 const dispatchConfig = (newConfig: Record<string, any>) => {
     if (!selectedNode) return;
     const isTrigger = reduxWorkflow.trigger?.TriggerId === selectedNode.id;
     if (isTrigger) {
         dispatch(workflowActions.updateTriggerConfig({ config: newConfig }));
     } else {
         dispatch(workflowActions.updateNodeConfig({ nodeId: selectedNode.id, config: newConfig }));
     }
 };

+// Debounced version for text input changes
+const debouncedDispatchConfig = useMemo(
+  () => debounce(dispatchConfig, 300),
+  [selectedNode?.id, reduxWorkflow.trigger?.TriggerId]
+);

Then use debouncedDispatchConfig for text/input fields and immediate dispatchConfig for dropdowns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx around lines 65 - 73,
dispatchConfig currently dispatches on every keystroke causing excessive Redux
updates (see dispatchConfig, selectedNode, reduxWorkflow.trigger?.TriggerId,
workflowActions.updateTriggerConfig and workflowActions.updateNodeConfig);
implement a debounced wrapper (e.g., debouncedDispatchConfig) using a stable
debounce utility (lodash.debounce or a custom hook like useDebouncedCallback)
and use debouncedDispatchConfig for text/input change handlers while keeping the
immediate dispatchConfig for dropdown/select changes so dropdowns remain
responsive; ensure the debounced function is memoized/created once per component
(useRef or useCallback) and cancelled on unmount to avoid stale updates.

26-26: Remove commented-out code.

Multiple sections of code are commented out rather than removed (Lines 26, 35, 244-255, 598-606). This adds noise and makes the code harder to maintain. Since the onSave callback flow is being intentionally removed in favor of Redux-based persistence, these comments should be deleted.

🧹 Clean up commented code

Remove all the commented-out sections:

  • Line 26: // onSave: (selectedNode: string, config: any, userId: string) => Promise<void>;
  • Line 35: // onSave,
  • Lines 244-255: The entire commented handleSave function
  • Lines 598-606: The commented Cancel button

This keeps the codebase clean and the git history already preserves the removed code if needed.

Also applies to: 35-35, 244-255, 598-606

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx at line 26, Remove
all commented-out onSave-related code and UI comments to clean up the component:
delete the commented prop declaration "// onSave: (selectedNode: string, config:
any, userId: string) => Promise<void>;", the commented prop usage "// onSave,",
the entire commented-out handleSave function block, and the commented Cancel
button markup so only the active Redux-based persistence flow remains; locate
these by looking for the identifiers "onSave", "handleSave", and the Cancel
button JSX comment inside ConfigModal.tsx and remove those commented sections
entirely.
apps/web/app/components/providers.tsx (1)

38-51: Consider adding a loading indicator for PersistGate.

Using loading={null} means children render immediately without waiting for rehydration. This could cause a brief flash where the persisted workflow data isn't available yet. For a smoother UX, consider adding a minimal loading state.

💡 Optional: Add loading indicator
-      <PersistGate loading={null} persistor={persistor}>
+      <PersistGate 
+        loading={<div className="min-h-screen" />} 
+        persistor={persistor}
+      >

Or for a more visible loading state:

loading={<div className="flex items-center justify-center min-h-screen">Loading...</div>}

The current approach works but may cause a brief content flash on slow devices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/components/providers.tsx` around lines 38 - 51, Replace the
PersistGate's loading={null} so the app shows a minimal loading indicator while
Redux rehydrates; update the PersistGate component (PersistGate, persistor) to
pass a small fallback UI (e.g., a centered spinner or simple loading div) via
the loading prop so children don't render until rehydration completes and avoid
content flash.
apps/web/store/index.ts (1)

14-19: Dynamic require for storage is correct but consider async import.

The dynamic require works for SSR compatibility, but using require in an ES module context (the package.json shows "type": "module") can be fragile. Consider using dynamic import() for better ES module compatibility.

♻️ Alternative using dynamic import (optional)
// If issues arise with require in ESM context, consider:
let browserStorage: any = null;

const createStorage = () => {
    if (typeof window !== 'undefined') {
        // Synchronous require works here because this code only runs client-side
        // after the module is already loaded
        return require('redux-persist/lib/storage').default;
    }
    return createNoopStorage();
}

The current implementation should work, but monitor for any bundler warnings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/store/index.ts` around lines 14 - 19, The createStorage function
uses a CommonJS require which can be fragile in ESM builds; change createStorage
to load the browser storage via dynamic import (await
import('redux-persist/lib/storage')) and return the imported default when typeof
window !== 'undefined', otherwise return createNoopStorage(); make createStorage
async (or provide an async getStorage wrapper) and update any callers to await
it, or cache the imported module locally to avoid repeated imports; reference
createStorage and createNoopStorage when making the change.
apps/web/store/slices/workflowSlice.ts (2)

89-94: Unnecessary optional chaining on changedNodeIds.

changedNodeIds is initialized as [] in initialState (line 71) and never set to undefined/null, so the optional chaining (state.changedNodeIds?.includes, state.changedNodeIds?.push) is unnecessary. The same pattern appears in updateNodePosition (lines 103-104) and updateNodeConfig (lines 114-115).

Suggested cleanup
         addWorkflowNode(state, action: PayloadAction<NodeItem>) {
             state.data.nodes.push(action.payload)
             state.isChanged.nodes = true
-            if(!state.changedNodeIds?.includes(action.payload.NodeId))
-                state.changedNodeIds?.push(action.payload.NodeId)
+            if(!state.changedNodeIds.includes(action.payload.NodeId))
+                state.changedNodeIds.push(action.payload.NodeId)
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/store/slices/workflowSlice.ts` around lines 89 - 94, The optional
chaining on state.changedNodeIds in addWorkflowNode, updateNodePosition, and
updateNodeConfig is unnecessary because initialState initializes changedNodeIds
as an empty array; replace uses of state.changedNodeIds?.includes(...) and
state.changedNodeIds?.push(...) with direct calls
state.changedNodeIds.includes(...) and state.changedNodeIds.push(...) in those
functions (addWorkflowNode, updateNodePosition, updateNodeConfig) to simplify
the code and rely on the guaranteed array initialization.

7-7: Consider typing Config more strictly than any.

Using any for Config (lines 7 and 20) bypasses TypeScript's type checking. If the config structure varies by trigger/node type, consider using a discriminated union, Record<string, unknown>, or a generic parameter for better type safety.

Also applies to: 20-20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/store/slices/workflowSlice.ts` at line 7, The Config field is
currently typed as any (symbol: Config) which disables type safety; replace it
with a stricter type by introducing a Config type alias or generic for the slice
(e.g., a discriminated-union for different trigger/node shapes, or at minimum
Record<string, unknown>), update the declarations that reference Config (the
occurrences shown at lines with "Config: any;") to use the new alias or generic
parameter, and adjust any places that read/write Config to use narrow type
guards or casts so the compiler enforces the expected properties.
apps/web/app/workflows/[id]/page.tsx (3)

159-253: Significant code duplication between Redux and API branches.

The two branches in loadWorkflows (lines 159-253 for Redux data and lines 257-406 for API fetch) share nearly identical logic for transforming triggers, nodes, and edges into React Flow format. Consider extracting a shared helper function to reduce duplication and maintenance burden.

Example extraction
function buildFlowNodes(
  trigger: { TriggerId: string; name: string; Config: any; position?: any },
  nodes: Array<{ NodeId: string; name: string; Config: any; position?: any; AvailableNodeID: string }>,
  handleNodeConfigure: (node: any) => void,
  setActionOpen: (open: boolean) => void,
  checkIsConfigure: (name: string, config: any) => boolean
) {
  const triggerPosition = ensurePosition(trigger.position, DEFAULT_TRIGGER_POSITION);
  
  const triggerNode = {
    id: trigger.TriggerId,
    type: "customNode",
    position: triggerPosition,
    data: {
      label: trigger.name || "Trigger",
      icon: "⚡",
      nodeType: "trigger",
      isConfigured: checkIsConfigure(trigger.name, trigger.Config),
      onConfigure: () => handleNodeConfigure({ id: trigger.TriggerId, name: trigger.name }),
    },
  };
  
  // ... rest of transformation logic
  return { triggerNode, transformedNodes, actionPlaceholder, edges };
}

Also applies to: 257-406

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/page.tsx around lines 159 - 253, The Redux and
API branches in loadWorkflows duplicate the flow construction logic
(trigger/node/placeholder/edges); extract a helper (e.g., buildFlowNodes) that
accepts trigger, nodes, edges plus utilities (ensurePosition,
DEFAULT_TRIGGER_POSITION, handleNodeConfigure, checkIsConfigure, setActionOpen)
and returns { finalNodes, newEdges, errorState } or similar; replace both
branches to call buildFlowNodes and then setNodes(finalNodes),
setEdges(newEdges), setError(null) (or setError from returned errorState) to
remove duplication while preserving existing IDs/placeholder logic and the
e-action-... edge creation.

429-459: Remove commented-out code.

The commented-out API calls in nodeChangeDb (and similar blocks throughout the file at lines 497-502, 573-579, 677-691, 779-822) should be removed. If this code is needed for reference, preserve it in version control history or a separate documentation file rather than leaving it inline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/page.tsx around lines 429 - 459, Remove the
commented-out API calls inside nodeChangeDb and the other similar blocks
referenced (the commented await api.triggers.update and await api.nodes.update
calls) so the function only contains active logic: the try/catch with dispatch
calls to workflowActions.updateTriggerPosition and
workflowActions.updateNodePosition and the setError error handling; do the same
for the other blocks at the mentioned locations (where commented await api.*
calls appear) so only live code remains and history is preserved in VCS rather
than inline comments.

79-79: Remove debug console.log statements before merging.

Multiple console.log calls (lines 79, 128, 264, 494) should be removed or converted to proper logging for production code.

Also applies to: 128-128, 264-264, 494-494

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/page.tsx at line 79, Remove the debug
console.log calls in this component: delete the specific console.log("This is
from the Execute Button", data) and the other ad-hoc console.log statements in
the same file, and if persistent tracing is needed replace them with a proper
logging utility (e.g., useLogger or processLogger) at appropriate levels; search
for remaining console.log occurrences in the page component and either remove
them or convert them to structured logger calls (keeping sensitive data out of
logs).
apps/web/app/hooks/useAutoSave.ts (1)

13-13: Misleading variable name.

hasUnChanged implies "has no changes" but actually means "has unsaved changes". Rename to hasUnsavedChanges for clarity.

Suggested rename
-    const hasUnChanged = isChangedState.trigger || isChangedState.edges || isChangedState.nodes;
+    const hasUnsavedChanges = isChangedState.trigger || isChangedState.edges || isChangedState.nodes;

Then update line 17:

-                            hasUnChanged ? 'Unsaved Changes' : 'Saved'
+                            hasUnsavedChanges ? 'Unsaved Changes' : 'Saved'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/hooks/useAutoSave.ts` at line 13, Rename the misleading variable
hasUnChanged to hasUnsavedChanges inside useAutoSave (where it is defined from
isChangedState) and update all local references to the new name (e.g., the usage
at the boolean expression using isChangedState.trigger || isChangedState.edges
|| isChangedState.nodes and any subsequent reads/writes) so the identifier
accurately reflects that it means "has unsaved changes" rather than "unchanged".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/http-backend/src/index.ts`:
- Line 37: The Google OAuth redirect URI is mismatched because the route was
changed to use app.use('/auth/google', googleAuth) but the environment value
used in google_callback.ts (GOOGLE_REDIRECT_URI) still points to
http://localhost:3002/oauth/google/callback; update the GOOGLE_REDIRECT_URI to
http://localhost:3002/auth/google/callback (and update any .env or config
loading code that populates GOOGLE_REDIRECT_URI) and confirm the same redirect
URI is registered in the Google Cloud Console for the OAuth client so the
callback handled by the googleAuth route (and its callback handler in
google_callback.ts) receives the OAuth response.

In `@apps/web/app/hooks/useAutoSave.ts`:
- Around line 74-79: The cleanup effect currently calls batchSave() without
awaiting it and omits batchSave from the deps, risking aborted in-flight saves
and stale closures; modify the hook so batchSave is memoized with useCallback
(including workflowId and any other captured state) and track in-flight requests
with a ref (e.g., isSavingRef) so the cleanup handler (that clears interval and
removes handleBeforeUnload) can either await completion of the in-flight save
before returning or explicitly document that the final save is best-effort;
update the effect dependency array to include the memoized batchSave and ensure
handleBeforeUnload/interval teardown only proceeds after checking the in-flight
ref or awaiting the promise when feasible.
- Around line 64-69: The beforeunload handler (handleBeforeUnload) currently
calls the async batchSave() which won't complete before the page unload; change
the handler to avoid awaiting async work: detect unsaved changes via
store.getState().workflow.isChanged and either (A) send a synchronous
fire-and-forget save using navigator.sendBeacon with the minimal payload instead
of batchSave(), or (B) remove the async call and simply set
e.preventDefault()/e.returnValue to show a confirmation dialog and let the user
choose to stay and trigger batchSave elsewhere; update handleBeforeUnload to use
navigator.sendBeacon when available and fall back to the confirmation-only
behavior so data isn't lost.

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx:
- Around line 216-240: The effect uses reduxWorkflow and fetchOptionsMap but
only depends on selectedNode; to fix, derive the minimal pieces from
reduxWorkflow (e.g., const triggerId = reduxWorkflow.trigger?.TriggerId and
const nodesSnapshot = reduxWorkflow.nodes) using useMemo or local variables and
include those derived values in the useEffect dependency array instead of the
whole reduxWorkflow, and ensure fetchOptionsMap is stable (wrap it in a ref or
memoize it) and reference fetchOptionsMapRef.current in the effect; update the
useEffect dependency array to [selectedNode, triggerId, nodesSnapshot,
fetchOptionsMapRef.current] and keep the existing logic that calls
getNodeConfig, fetchFn and setDynamicOptions.

In `@apps/web/app/workflows/`[id]/page.tsx:
- Around line 138-154: The useEffect updating nodes uses setNodes and the
render-scoped checkIsConfigure but doesn't include them in the dependency array;
wrap checkIsConfigure in useCallback (or move it outside the component) so it is
stable, then add checkIsConfigure and setNodes (if not stable) to the useEffect
deps, or explicitly disable the eslint rule with a comment and rationale; update
references to the function name checkIsConfigure in the effect and ensure the
new useCallback signature captures only safe dependencies (or has an empty deps
array) so the linter is satisfied.

In `@packages/common/src/index.ts`:
- Line 29: Update the schema field names to use camelCase to match
backend/database expectations: in TriggerSchema and TriggerUpdateSchema replace
the Position field with position (e.g., Position: z.object({...}).optional() ->
position: z.object({...}).optional()) and normalize credential fields to
credentialId (replace any CredentialId or CredentialID occurrences) so they
align with the database mapping (CredentialsID); ensure any parsing/validation
keys and exported types referencing TriggerSchema or TriggerUpdateSchema are
updated accordingly to avoid serialization mismatches.

In `@packages/db/prisma/schema.prisma`:
- Around line 36-39: The Prisma schema changes made the field config optional
and added the Position field on the Trigger model (with workflowId and
AvailableTriggerID present); generate and apply a migration to update the DB by
running: npx prisma migrate dev --name add_trigger_position_and_optional_config,
verify the generated SQL in packages/db/prisma/migrations/*/migration.sql and
commit that migration file, then run prisma db push or the migrate apply flow to
ensure the database reflects the changes; optionally plan a follow-up to
standardize the Position identifier (e.g., rename to position) for consistency
with Node.position.

In `@packages/nodes/src/gmail/gmail.node.ts`:
- Around line 33-36: The static async register method currently only calls
NodeRegistry.register(this.definition) which prevents the gmail trigger from
being persisted; restore the trigger registration by calling and awaiting
NodeRegistry.registerTrigger(this.definition) in static async register (i.e.,
re-enable the commented line so both NodeRegistry.register(this.definition) and
await NodeRegistry.registerTrigger(this.definition) are invoked using
this.definition).

In `@packages/nodes/src/google-sheets/google-sheets.node.ts`:
- Around line 34-37: The trigger registration for the Google Sheets node was
commented out so its trigger ("google_sheet") isn't upserted into
availableTrigger; restore or relocate that call so triggers are registered
during startup. Re-enable the call to NodeRegistry.registerTrigger in the static
async register method of the GoogleSheets node (or move equivalent logic to the
global startup path where NodeRegistry.registerAll runs) so that
NodeRegistry.registerTrigger(this.definition) executes and the
/getAvailableTriggers endpoint can read the google_sheet entry from the
availableTrigger table.

---

Outside diff comments:
In `@apps/http-backend/src/routes/userRoutes/userRoutes.ts`:
- Around line 622-635: The handler currently returns statusCodes.CREATED after
updating an existing Trigger; locate the prismaClient.trigger.update call and
the following if (updatedTrigger) branch and change the response status from
statusCodes.CREATED to statusCodes.OK so an update returns HTTP 200 (OK) instead
of 201 (Created) when sending the res.status(...).json({ message: "Trigger
updated", data: updatedTrigger }).
- Around line 581-595: The update block currently writes credentialId both into
the config JSON and into CredentialsID and returns a 201; change
prismaClient.node.update usage so you remove/omit credentialId from
dataSafe.data.Config before including it in the update (e.g., build a
cleanedConfig that copies dataSafe.data.Config without credentialId and use that
for the config field) while still setting CredentialsID to the extracted
credentialId when present (referencing the existing updateNode and
prismaClient.node.update usage and dataSafe.data.Config/CredentialsID symbols),
and change the HTTP response to return statusCodes.OK instead of
statusCodes.CREATED for the update response.

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx:
- Line 347: Remove the debug console.log by deleting the console.log(options)
expression that was placed inside the JSX return of the ConfigModal component
(the render for ConfigModal in ConfigModal.tsx). If you need to keep the log for
local debugging, move it out of the JSX into a useEffect or temporary
development-only block (e.g., useEffect(() => { console.log(options) },
[options])) so it doesn't execute on every render or ship to production.

---

Nitpick comments:
In `@apps/http-backend/src/routes/userRoutes/userRoutes.ts`:
- Around line 591-595: The handler currently only returns a response when
updateNode is truthy, causing the request to hang if it's falsy; update the code
in the route handler that calls updateNode (and the analogous handler calling
updateTrigger) to always send a response: either remove the unnecessary
conditional and always return the created/updated payload, or add an else branch
that returns a proper fallback (e.g., res.status(404).json({ message: "Node not
found" }) or res.status(500) for unexpected failures). Ensure the change is
applied to the block around the updateNode response and the similar block around
updateTrigger so every execution path ends with a res.* call.
- Around line 465-474: The code currently forces Position to an empty object
when missing, but the Prisma field Position is nullable (Json?), so change the
create payload in prismaClient.trigger.create to pass null (or omit the Position
key) when dataSafe.data.Position is undefined to preserve null semantics; locate
the prismaClient.trigger.create call and replace the unconditional Position:
dataSafe.data.Position || {} with logic that sets Position to
dataSafe.data.Position ?? null (or conditionally exclude Position from the data
object) so missing positions are stored as null instead of {}.

In `@apps/web/app/components/providers.tsx`:
- Around line 38-51: Replace the PersistGate's loading={null} so the app shows a
minimal loading indicator while Redux rehydrates; update the PersistGate
component (PersistGate, persistor) to pass a small fallback UI (e.g., a centered
spinner or simple loading div) via the loading prop so children don't render
until rehydration completes and avoid content flash.

In `@apps/web/app/hooks/useAutoSave.ts`:
- Line 13: Rename the misleading variable hasUnChanged to hasUnsavedChanges
inside useAutoSave (where it is defined from isChangedState) and update all
local references to the new name (e.g., the usage at the boolean expression
using isChangedState.trigger || isChangedState.edges || isChangedState.nodes and
any subsequent reads/writes) so the identifier accurately reflects that it means
"has unsaved changes" rather than "unchanged".

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx:
- Around line 65-73: dispatchConfig currently dispatches on every keystroke
causing excessive Redux updates (see dispatchConfig, selectedNode,
reduxWorkflow.trigger?.TriggerId, workflowActions.updateTriggerConfig and
workflowActions.updateNodeConfig); implement a debounced wrapper (e.g.,
debouncedDispatchConfig) using a stable debounce utility (lodash.debounce or a
custom hook like useDebouncedCallback) and use debouncedDispatchConfig for
text/input change handlers while keeping the immediate dispatchConfig for
dropdown/select changes so dropdowns remain responsive; ensure the debounced
function is memoized/created once per component (useRef or useCallback) and
cancelled on unmount to avoid stale updates.
- Line 26: Remove all commented-out onSave-related code and UI comments to clean
up the component: delete the commented prop declaration "// onSave:
(selectedNode: string, config: any, userId: string) => Promise<void>;", the
commented prop usage "// onSave,", the entire commented-out handleSave function
block, and the commented Cancel button markup so only the active Redux-based
persistence flow remains; locate these by looking for the identifiers "onSave",
"handleSave", and the Cancel button JSX comment inside ConfigModal.tsx and
remove those commented sections entirely.

In `@apps/web/app/workflows/`[id]/page.tsx:
- Around line 159-253: The Redux and API branches in loadWorkflows duplicate the
flow construction logic (trigger/node/placeholder/edges); extract a helper
(e.g., buildFlowNodes) that accepts trigger, nodes, edges plus utilities
(ensurePosition, DEFAULT_TRIGGER_POSITION, handleNodeConfigure,
checkIsConfigure, setActionOpen) and returns { finalNodes, newEdges, errorState
} or similar; replace both branches to call buildFlowNodes and then
setNodes(finalNodes), setEdges(newEdges), setError(null) (or setError from
returned errorState) to remove duplication while preserving existing
IDs/placeholder logic and the e-action-... edge creation.
- Around line 429-459: Remove the commented-out API calls inside nodeChangeDb
and the other similar blocks referenced (the commented await api.triggers.update
and await api.nodes.update calls) so the function only contains active logic:
the try/catch with dispatch calls to workflowActions.updateTriggerPosition and
workflowActions.updateNodePosition and the setError error handling; do the same
for the other blocks at the mentioned locations (where commented await api.*
calls appear) so only live code remains and history is preserved in VCS rather
than inline comments.
- Line 79: Remove the debug console.log calls in this component: delete the
specific console.log("This is from the Execute Button", data) and the other
ad-hoc console.log statements in the same file, and if persistent tracing is
needed replace them with a proper logging utility (e.g., useLogger or
processLogger) at appropriate levels; search for remaining console.log
occurrences in the page component and either remove them or convert them to
structured logger calls (keeping sensitive data out of logs).

In `@apps/web/store/index.ts`:
- Around line 14-19: The createStorage function uses a CommonJS require which
can be fragile in ESM builds; change createStorage to load the browser storage
via dynamic import (await import('redux-persist/lib/storage')) and return the
imported default when typeof window !== 'undefined', otherwise return
createNoopStorage(); make createStorage async (or provide an async getStorage
wrapper) and update any callers to await it, or cache the imported module
locally to avoid repeated imports; reference createStorage and createNoopStorage
when making the change.

In `@apps/web/store/slices/workflowSlice.ts`:
- Around line 89-94: The optional chaining on state.changedNodeIds in
addWorkflowNode, updateNodePosition, and updateNodeConfig is unnecessary because
initialState initializes changedNodeIds as an empty array; replace uses of
state.changedNodeIds?.includes(...) and state.changedNodeIds?.push(...) with
direct calls state.changedNodeIds.includes(...) and
state.changedNodeIds.push(...) in those functions (addWorkflowNode,
updateNodePosition, updateNodeConfig) to simplify the code and rely on the
guaranteed array initialization.
- Line 7: The Config field is currently typed as any (symbol: Config) which
disables type safety; replace it with a stricter type by introducing a Config
type alias or generic for the slice (e.g., a discriminated-union for different
trigger/node shapes, or at minimum Record<string, unknown>), update the
declarations that reference Config (the occurrences shown at lines with "Config:
any;") to use the new alias or generic parameter, and adjust any places that
read/write Config to use narrow type guards or casts so the compiler enforces
the expected properties.

In `@packages/common/src/index.ts`:
- Line 2: Remove the unused named import "number" from "zod/v4" in this module
(the import statement that reads import { number } from "zod/v4"); simply delete
the unused import so the file no longer imports the unused symbol "number" and
run lint/tsc to ensure no other references remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32d813fb-b941-4b9c-baa1-0a6469c67435

📥 Commits

Reviewing files that changed from the base of the PR and between db6c941 and c687de1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/http-backend/src/index.ts
  • apps/http-backend/src/routes/userRoutes/userRoutes.ts
  • apps/web/app/components/providers.tsx
  • apps/web/app/hooks/useAutoSave.ts
  • apps/web/app/lib/api.ts
  • apps/web/app/workflows/[id]/components/ConfigModal.tsx
  • apps/web/app/workflows/[id]/page.tsx
  • apps/web/package.json
  • apps/web/store/index.ts
  • apps/web/store/slices/workflowSlice.ts
  • packages/common/src/index.ts
  • packages/db/prisma/schema.prisma
  • packages/nodes/src/gmail/gmail.node.ts
  • packages/nodes/src/google-sheets/google-sheets.node.ts

app.use("/user" , userRouter)
app.use('/node', sheetRouter)
app.use('/oauth/google', googleAuth)
app.use('/auth/google', googleAuth)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for old route references in the codebase
rg -n 'oauth/google' --type ts --type tsx --type js
# Search for new route references
rg -n 'auth/google' --type ts --type tsx --type js

Repository: Dev-Pross/BuildFlow

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Search for old route references in the codebase (broader search)
echo "=== Searching for 'oauth/google' references ==="
rg -n 'oauth/google' -t ts -t js

echo -e "\n=== Searching for 'auth/google' references ==="
rg -n 'auth/google' -t ts -t js

echo -e "\n=== Checking the index.ts file context ==="
head -50 apps/http-backend/src/index.ts

Repository: Dev-Pross/BuildFlow

Length of output: 3051


🏁 Script executed:

#!/bin/bash
# Check the googleAuth handler to see what routes it exposes
echo "=== Content of google_callback.ts ==="
cat -n apps/http-backend/src/routes/google_callback.ts

echo -e "\n=== Checking for any callback endpoint definitions ==="
rg -n 'callback|router\.' apps/http-backend/src/routes/google_callback.ts

Repository: Dev-Pross/BuildFlow

Length of output: 11672


Critical: Route change breaks OAuth callback flow—update GOOGLE_REDIRECT_URI environment variable.

The Google OAuth route was moved from /oauth/google to /auth/google, but the default GOOGLE_REDIRECT_URI in google_callback.ts (line 22) still points to http://localhost:3002/oauth/google/callback. This causes a mismatch: Google will attempt to redirect to /oauth/google/callback, but the server callback endpoint is now at /auth/google/callback.

Update the GOOGLE_REDIRECT_URI environment variable to http://localhost:3002/auth/google/callback and ensure it matches the redirect URI registered in your Google Cloud Console OAuth application settings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/http-backend/src/index.ts` at line 37, The Google OAuth redirect URI is
mismatched because the route was changed to use app.use('/auth/google',
googleAuth) but the environment value used in google_callback.ts
(GOOGLE_REDIRECT_URI) still points to
http://localhost:3002/oauth/google/callback; update the GOOGLE_REDIRECT_URI to
http://localhost:3002/auth/google/callback (and update any .env or config
loading code that populates GOOGLE_REDIRECT_URI) and confirm the same redirect
URI is registered in the Google Cloud Console for the OAuth client so the
callback handled by the googleAuth route (and its callback handler in
google_callback.ts) receives the OAuth response.

Comment on lines +64 to +69
const handleBeforeUnload = (e: BeforeUnloadEvent) =>{
const { isChanged } = store.getState().workflow
if(isChanged.edges || isChanged.nodes || isChanged.trigger){
e.preventDefault()
batchSave();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Async batchSave() in beforeunload won't complete before page unload.

beforeunload fires synchronously, and calling an async function here doesn't block the browser from navigating away. While e.preventDefault() shows a confirmation dialog, the batchSave() promise won't resolve in time, potentially losing user data.

Consider using navigator.sendBeacon for fire-and-forget persistence, or warn users about unsaved changes instead of attempting a save.

Alternative: warn only, don't attempt async save
        const handleBeforeUnload = (e: BeforeUnloadEvent) =>{
            const { isChanged } = store.getState().workflow
            if(isChanged.edges || isChanged.nodes || isChanged.trigger){
                e.preventDefault()
-                batchSave();
+                // Browser will show "unsaved changes" dialog
+                // Async save won't complete before unload; rely on interval/manual save
            }
        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleBeforeUnload = (e: BeforeUnloadEvent) =>{
const { isChanged } = store.getState().workflow
if(isChanged.edges || isChanged.nodes || isChanged.trigger){
e.preventDefault()
batchSave();
}
const handleBeforeUnload = (e: BeforeUnloadEvent) =>{
const { isChanged } = store.getState().workflow
if(isChanged.edges || isChanged.nodes || isChanged.trigger){
e.preventDefault()
// Browser will show "unsaved changes" dialog
// Async save won't complete before unload; rely on interval/manual save
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/hooks/useAutoSave.ts` around lines 64 - 69, The beforeunload
handler (handleBeforeUnload) currently calls the async batchSave() which won't
complete before the page unload; change the handler to avoid awaiting async
work: detect unsaved changes via store.getState().workflow.isChanged and either
(A) send a synchronous fire-and-forget save using navigator.sendBeacon with the
minimal payload instead of batchSave(), or (B) remove the async call and simply
set e.preventDefault()/e.returnValue to show a confirmation dialog and let the
user choose to stay and trigger batchSave elsewhere; update handleBeforeUnload
to use navigator.sendBeacon when available and fall back to the
confirmation-only behavior so data isn't lost.

Comment on lines +74 to +79
return ()=>{
clearInterval(interval);
window.removeEventListener("beforeunload", handleBeforeUnload)
batchSave()
}
}, [workflowId])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cleanup effect calls async batchSave() which won't complete.

When the component unmounts, React won't wait for the batchSave() promise to resolve. This means the final save attempt may be aborted mid-flight. Consider tracking in-flight requests with a ref and only cleaning up after completion, or accept that cleanup saves are best-effort.

Additionally, batchSave is not in the dependency array. Since batchSave is defined inside the component and captures workflowId in its closure, if workflowId changed without re-running the effect, stale closures could occur. However, since workflowId is in the deps, the effect re-runs correctly. Consider wrapping batchSave in useCallback with proper deps for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/hooks/useAutoSave.ts` around lines 74 - 79, The cleanup effect
currently calls batchSave() without awaiting it and omits batchSave from the
deps, risking aborted in-flight saves and stale closures; modify the hook so
batchSave is memoized with useCallback (including workflowId and any other
captured state) and track in-flight requests with a ref (e.g., isSavingRef) so
the cleanup handler (that clears interval and removes handleBeforeUnload) can
either await completion of the in-flight save before returning or explicitly
document that the final save is best-effort; update the effect dependency array
to include the memoized batchSave and ensure handleBeforeUnload/interval
teardown only proceeds after checking the in-flight ref or awaiting the promise
when feasible.

Comment on lines 216 to 240
useEffect(() => {
setConfig({});
// We no longer set local credentials here; handled by useCredentials!
if (!selectedNode) {
setConfig({});
return;
}
// Load existing saved config from Redux instead of starting empty
const isTrigger = reduxWorkflow.trigger?.TriggerId === selectedNode.id;
const loadedConfig = isTrigger ? (reduxWorkflow.trigger?.Config || {}) : (reduxWorkflow.nodes.find(n => n.NodeId === selectedNode.id)?.Config || {})

setConfig(loadedConfig)

const nodeConfig = getNodeConfig(selectedNode.name || selectedNode.actionType);
if(nodeConfig?.fields){
for(const field of nodeConfig.fields){
if(field.fetchOptions && field.dependsOn && loadedConfig[field.dependsOn]){
const fetchFn = fetchOptionsMap[field.fetchOptions];
if(fetchFn){
fetchFn(loadedConfig)
.then((option: any[])=> setDynamicOptions(prev => ({...prev, [field.name]: option})))
.catch(()=> {})
}
}
}
}
}, [selectedNode]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing dependencies in useEffect.

The useEffect hook references reduxWorkflow and fetchOptionsMap but they're not included in the dependency array. This could lead to stale closures where the effect uses outdated values.

However, adding reduxWorkflow would cause the effect to run on every Redux state change, which isn't desired. Consider using a ref or restructuring the logic.

🔧 Proposed fix using selective dependency
   useEffect(() => {
     if (!selectedNode) {
       setConfig({});
       return;
     }
-    // Load existing saved config from Redux instead of starting empty
-    const isTrigger = reduxWorkflow.trigger?.TriggerId === selectedNode.id;
-    const loadedConfig = isTrigger ? (reduxWorkflow.trigger?.Config || {}) : (reduxWorkflow.nodes.find(n => n.NodeId === selectedNode.id)?.Config || {})
+    // Extract config once when selectedNode changes
+    const trigger = reduxWorkflow.trigger;
+    const nodes = reduxWorkflow.nodes;
+    const isTrigger = trigger?.TriggerId === selectedNode.id;
+    const loadedConfig = isTrigger 
+      ? (trigger?.Config || {}) 
+      : (nodes.find(n => n.NodeId === selectedNode.id)?.Config || {});

     setConfig(loadedConfig)
     // ... rest of effect
-  }, [selectedNode]);
+  }, [selectedNode, reduxWorkflow.trigger?.TriggerId, reduxWorkflow.nodes]);

Alternatively, memoize the config lookup outside the effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/components/ConfigModal.tsx around lines 216 -
240, The effect uses reduxWorkflow and fetchOptionsMap but only depends on
selectedNode; to fix, derive the minimal pieces from reduxWorkflow (e.g., const
triggerId = reduxWorkflow.trigger?.TriggerId and const nodesSnapshot =
reduxWorkflow.nodes) using useMemo or local variables and include those derived
values in the useEffect dependency array instead of the whole reduxWorkflow, and
ensure fetchOptionsMap is stable (wrap it in a ref or memoize it) and reference
fetchOptionsMapRef.current in the effect; update the useEffect dependency array
to [selectedNode, triggerId, nodesSnapshot, fetchOptionsMapRef.current] and keep
the existing logic that calls getNodeConfig, fetchFn and setDynamicOptions.

Comment on lines +138 to +154
useEffect(()=>{
setNodes(prev => prev.map(node => {
if(node.data?.isPlaceholder) return node;
if(node.data?.nodeType === 'trigger'){
const reduxConfig = reduxWorkflow.data.trigger?.Config;
const name = reduxWorkflow.data.trigger?.name || "";
return { ...node, data: { ...node.data, isConfigured: checkIsConfigure(name, reduxConfig)}}
}

if(node.data.nodeType === 'action'){
const reduxNode = reduxWorkflow.data.nodes.find(n=> n.NodeId === node.id);
if(!reduxNode) return node;
return { ...node, data: { ...node.data, isConfigured: checkIsConfigure(reduxNode.name, reduxNode.Config)}};
}
return node;
}));
}, [reduxWorkflow.data.nodes, reduxWorkflow.data.trigger])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing dependencies in useEffect.

This effect calls setNodes and checkIsConfigure but neither is included in the dependency array. While setNodes from useNodesState is stable and checkIsConfigure is defined in render scope, the linter rule exists to catch subtle bugs. Wrap checkIsConfigure in useCallback and include both in deps, or disable the lint rule with an explanation.

Suggested fix
-  }, [reduxWorkflow.data.nodes, reduxWorkflow.data.trigger])
+  }, [reduxWorkflow.data.nodes, reduxWorkflow.data.trigger, setNodes])

And move checkIsConfigure outside the component or wrap in useCallback:

const checkIsConfigure = useCallback((nodeName: string, config: any): boolean => {
  const nodeConfig = getNodeConfig(nodeName);
  if(!nodeConfig || !nodeConfig.fields) return true;
  const requiredFields = nodeConfig.fields.filter((f: any) => f.required);
  if(requiredFields.length === 0) return true;
  return requiredFields.every((f: any) => config?.[f.name] !== undefined && config?.[f.name] !== '');
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/workflows/`[id]/page.tsx around lines 138 - 154, The useEffect
updating nodes uses setNodes and the render-scoped checkIsConfigure but doesn't
include them in the dependency array; wrap checkIsConfigure in useCallback (or
move it outside the component) so it is stable, then add checkIsConfigure and
setNodes (if not stable) to the useEffect deps, or explicitly disable the eslint
rule with a comment and rationale; update references to the function name
checkIsConfigure in the effect and ensure the new useCallback signature captures
only safe dependencies (or has an empty deps array) so the linter is satisfied.

Config: z.any().optional(),
WorkflowId: z.string(),
TriggerType: z.string().optional(),
Position: z.object({x: z.number(), y: z.number()}).optional()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how Position/position and CredentialId/CredentialID are used in backend routes
rg -n -C3 'Position|position|CredentialId|CredentialID' --type ts apps/http-backend/

Repository: Dev-Pross/BuildFlow

Length of output: 4399


Standardize schema field naming to camelCase to prevent serialization bugs.

Frontend schemas have inconsistent casing that causes silent failures when backend routes map data to database operations:

  1. Position field: TriggerSchema and TriggerUpdateSchema use Position (PascalCase), but backend node creation expects position (camelCase) when persisting to the database.
  2. CredentialId field: Inconsistent as CredentialId vs CredentialID across schemas, while backend always maps to CredentialsID in the database.

Change both fields to camelCase to match database column names and ensure consistent data flow.

🔧 Proposed fix
 export const TriggerSchema = z.object({
   Name: z.string(),
   AvailableTriggerID: z.string(),
   Config: z.any().optional(),
   WorkflowId: z.string(),
   TriggerType: z.string().optional(),
-  Position: z.object({x: z.number(), y: z.number()}).optional()
+  position: z.object({x: z.number(), y: z.number()}).optional()
 });

 export const TriggerUpdateSchema = z.object({
   TriggerId: z.string(),
   Config: z.any().optional(),
-  Position: z.object({ x: z.number(), y: z.number()}).optional(),
-  CredentialID: z.string().optional()
+  position: z.object({ x: z.number(), y: z.number()}).optional(),
+  CredentialId: z.string().optional()
 });

Also applies to: Lines 38-43, 59-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/index.ts` at line 29, Update the schema field names to
use camelCase to match backend/database expectations: in TriggerSchema and
TriggerUpdateSchema replace the Position field with position (e.g., Position:
z.object({...}).optional() -> position: z.object({...}).optional()) and
normalize credential fields to credentialId (replace any CredentialId or
CredentialID occurrences) so they align with the database mapping
(CredentialsID); ensure any parsing/validation keys and exported types
referencing TriggerSchema or TriggerUpdateSchema are updated accordingly to
avoid serialization mismatches.

Comment on lines +36 to +39
config Json?
workflowId String? @unique
AvailableTriggerID String
Position Json?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for recent migration files
fd -e sql . packages/db/prisma/migrations --changed-within 7d
# Check migration status
cat packages/db/prisma/migrations/*/migration.sql 2>/dev/null | tail -50

Repository: Dev-Pross/BuildFlow

Length of output: 45


🏁 Script executed:

# Check if migrations directory exists and list its contents
find packages/db/prisma/migrations -type f -name "*.sql" 2>/dev/null | head -20

# If no migrations, check the directory structure
ls -la packages/db/prisma/ 2>/dev/null

# Check git status for any migration files
git ls-files packages/db/prisma/migrations/ 2>/dev/null | head -20

Repository: Dev-Pross/BuildFlow

Length of output: 346


🏁 Script executed:

# Get the schema file to verify the actual changes mentioned in the review
cat -n packages/db/prisma/schema.prisma | sed -n '30,80p'

Repository: Dev-Pross/BuildFlow

Length of output: 2329


🏁 Script executed:

# Check git diff or git log to see what actually changed in this commit
git diff HEAD~1 packages/db/prisma/schema.prisma 2>/dev/null || git show HEAD:packages/db/prisma/schema.prisma 2>/dev/null | sed -n '30,80p'

Repository: Dev-Pross/BuildFlow

Length of output: 1972


Generate and apply a Prisma migration for these schema changes.

No migration files were found in packages/db/prisma/migrations/. The schema changes (making config optional and adding the Position field) require a migration to update the database. Generate and commit the migration before deployment:

npx prisma migrate dev --name add_trigger_position_and_optional_config

Then verify the generated migration file in packages/db/prisma/migrations/*/migration.sql is committed.

Note: The naming inconsistency remains (Trigger.Position in PascalCase vs Node.position in camelCase). Consider standardizing to lowercase for consistency in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/prisma/schema.prisma` around lines 36 - 39, The Prisma schema
changes made the field config optional and added the Position field on the
Trigger model (with workflowId and AvailableTriggerID present); generate and
apply a migration to update the DB by running: npx prisma migrate dev --name
add_trigger_position_and_optional_config, verify the generated SQL in
packages/db/prisma/migrations/*/migration.sql and commit that migration file,
then run prisma db push or the migrate apply flow to ensure the database
reflects the changes; optionally plan a follow-up to standardize the Position
identifier (e.g., rename to position) for consistency with Node.position.

Comment on lines 33 to +36
static async register(){
await NodeRegistry.register(this.definition)
// console.log(`✅ Registered node: ${this.definition.name}`);
await NodeRegistry.registerTrigger(this.definition)
// await NodeRegistry.registerTrigger(this.definition)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This drops Gmail from the available trigger catalog.

Without NodeRegistry.registerTrigger(this.definition), the gmail trigger is never persisted during startup. The backend later serves available triggers from availableTrigger, so users will no longer be able to create Gmail-triggered workflows unless another registration path was added.

Suggested fix
 static async register(){
     await NodeRegistry.register(this.definition)
-    // await NodeRegistry.registerTrigger(this.definition)
+    await NodeRegistry.registerTrigger(this.definition)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nodes/src/gmail/gmail.node.ts` around lines 33 - 36, The static
async register method currently only calls
NodeRegistry.register(this.definition) which prevents the gmail trigger from
being persisted; restore the trigger registration by calling and awaiting
NodeRegistry.registerTrigger(this.definition) in static async register (i.e.,
re-enable the commented line so both NodeRegistry.register(this.definition) and
await NodeRegistry.registerTrigger(this.definition) are invoked using
this.definition).

Comment on lines 34 to +37
static async register(){
await NodeRegistry.register(this.definition)
// console.log(`✅ Registered node: ${this.definition.name}`);
await NodeRegistry.registerTrigger(this.definition)
// await NodeRegistry.registerTrigger(this.definition)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Re-enable trigger registration or move it to another startup path.

Commenting this out stops google_sheet from being upserted into availableTrigger. NodeRegistry.registerAll() only registers webhook explicitly, and /getAvailableTriggers reads from that table, so this trigger will disappear from the frontend after server start.

Suggested fix
 static async register(){
     await NodeRegistry.register(this.definition)
-    // await NodeRegistry.registerTrigger(this.definition)
+    await NodeRegistry.registerTrigger(this.definition)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static async register(){
await NodeRegistry.register(this.definition)
// console.log(`✅ Registered node: ${this.definition.name}`);
await NodeRegistry.registerTrigger(this.definition)
// await NodeRegistry.registerTrigger(this.definition)
static async register(){
await NodeRegistry.register(this.definition)
// console.log(`✅ Registered node: ${this.definition.name}`);
await NodeRegistry.registerTrigger(this.definition)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nodes/src/google-sheets/google-sheets.node.ts` around lines 34 - 37,
The trigger registration for the Google Sheets node was commented out so its
trigger ("google_sheet") isn't upserted into availableTrigger; restore or
relocate that call so triggers are registered during startup. Re-enable the call
to NodeRegistry.registerTrigger in the static async register method of the
GoogleSheets node (or move equivalent logic to the global startup path where
NodeRegistry.registerAll runs) so that
NodeRegistry.registerTrigger(this.definition) executes and the
/getAvailableTriggers endpoint can read the google_sheet entry from the
availableTrigger table.

@Vamsi-o Vamsi-o left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Route change breaks OAuth callback flow—update GOOGLE_REDIRECT_URI environment variable.

The Google OAuth route was moved from /oauth/google to /auth/google, but the default GOOGLE_REDIRECT_URI in google_callback.ts (line 22) still points to http://localhost:3002/oauth/google/callback. This causes a mismatch: Google will attempt to redirect to /oauth/google/callback, but the server callback endpoint is now at /auth/google/callback.

Update the GOOGLE_REDIRECT_URI environment variable to http://localhost:3002/auth/google/callback and ensure it matches the redirect URI registered in your Google Cloud Console OAuth application settings.

@Vamsi-o Vamsi-o merged commit c07be29 into main Mar 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants