Skip to content

feat: implemente node configuration registry, execution engine, and d… - #83

Merged
Vamsi-o merged 1 commit into
mainfrom
creating-filter-node
Aug 3, 2026
Merged

feat: implemente node configuration registry, execution engine, and d…#83
Vamsi-o merged 1 commit into
mainfrom
creating-filter-node

Conversation

@Teja-Budumuru

@Teja-Budumuru Teja-Budumuru commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

…ynamic config modal for Gmail, Google Sheets, and FILTER nodes

Summary by CodeRabbit

  • New Features

    • Added dataset filtering with deduplication and comparison against new or existing records.
    • Added rich variable inputs with color-coded node references, suggestions, placeholders, and dynamic nested field selection.
    • Added whole-table selection when inserting spreadsheet data.
    • Improved nested object and array result displays with expandable table formatting.
  • Bug Fixes

    • Preserved non-string values when configuration consists of a single variable reference.
  • Documentation

    • Clarified Gmail and Google Sheets operation descriptions and usage requirements.

…ynamic config modal for Gmail, Google Sheets, and FILTER nodes
Copilot AI review requested due to automatic review settings August 2, 2026 18:33
@Teja-Budumuru
Teja-Budumuru requested a review from Vamsi-o as a code owner August 2, 2026 18:33
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Filter node

Layer / File(s) Summary
Filter configuration and input contract
apps/web/app/lib/nodeConfigs/filter.action.ts, apps/web/app/lib/nodeConfigs/index.ts, apps/web/app/lib/types/node.types.ts, packages/common/src/index.ts
Adds filter operations, dynamic key fields, output schemas, and the FilterNodeInput validation contract.
Filter execution and result handling
packages/nodes/src/filter/filter.executor.ts, packages/nodes/package.json
Normalizes object and tabular data, applies filtering operations, and returns structured results or errors.
Filter node registration
packages/nodes/src/filter/filter.node.ts, packages/nodes/src/registry/*
Registers the filter node and executor.

Variable-driven workflow UI

Layer / File(s) Summary
Rich variable input and configuration integration
apps/web/app/components/ui/RichVariableInput.tsx, apps/web/app/workflows/[id]/components/ConfigModal.tsx
Adds pill-based variable editing and integrates tested node suggestions into configuration fields.
Dynamic schema selection
apps/web/app/workflows/[id]/components/ConfigModal.tsx
Extracts nested keys from resolved array data and displays them in dynamic dropdowns.
Node-ID variable paths
apps/web/app/components/ui/variable-panel.tsx
Uses node IDs for variable references and adds whole-table .rows insertion.
Nested test output rendering
apps/web/app/components/ui/TestPanel.tsx
Renders nested objects and object arrays as embedded tables.
Typed interpolation results
packages/common/src/interpolation.ts
Preserves non-string values for expressions that contain only one interpolation.

Node metadata and presentation

Layer / File(s) Summary
Node descriptions and icons
apps/web/app/components/nodes/BaseNode.tsx, packages/nodes/src/gmail/gmail.node.ts, packages/nodes/src/google-sheets/google-sheets.node.ts
Updates node icon handling, Gmail documentation, and Google Sheets documentation and icons.
Shared schema formatting
packages/common/src/index.ts
Normalizes formatting in existing schema declarations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowUser
  participant ConfigModal
  participant RichVariableInput
  participant FilterExecutor
  participant NodeRegistry
  WorkflowUser->>ConfigModal: configure filter node
  ConfigModal->>RichVariableInput: edit variable-backed fields
  ConfigModal->>NodeRegistry: resolve registered filter node
  NodeRegistry->>FilterExecutor: create filter executor
  FilterExecutor-->>ConfigModal: return filtered and discarded data
Loading

Possibly related PRs

Suggested reviewers: vamsi-o, copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the node configuration registry, execution engine, and dynamic configuration changes implemented by the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch creating-filter-node

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/common/src/index.ts

Oops! Something went wrong! :(

ESLint: 9.32.0

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './library.js' is not defined by "exports" in /node_modules/@workspace/eslint-config/package.json
Referenced from: /.eslintrc.js
at exportsNotFound (node:internal/modules/esm/resolve:310:10)
at packageExportsResolve (node:internal/modules/esm/resolve:658:9)
at resolveExports (node:internal/modules/cjs/loader:685:36)
at Module._findPath (node:internal/modules/cjs/loader:752:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1461:27)
at wrapResolveFilename (node:internal/modules/cjs/loader:1049:27)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1094:12)
at require.resolve (node:internal/modules/helpers:171:31)
at Object.resolve (/node_modules/.pnpm/@eslint+eslintrc@3.3.1/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2447:46)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/.pnpm/@eslint+eslintrc@3.3.1/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3383:33)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

packages/common/src/interpolation.ts

Oops! Something went wrong! :(

ESLint: 9.32.0

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './library.js' is not defined by "exports" in /node_modules/@workspace/eslint-config/package.json
Referenced from: /.eslintrc.js
at exportsNotFound (node:internal/modules/esm/resolve:310:10)
at packageExportsResolve (node:internal/modules/esm/resolve:658:9)
at resolveExports (node:internal/modules/cjs/loader:685:36)
at Module._findPath (node:internal/modules/cjs/loader:752:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1461:27)
at wrapResolveFilename (node:internal/modules/cjs/loader:1049:27)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1094:12)
at require.resolve (node:internal/modules/helpers:171:31)
at Object.resolve (/node_modules/.pnpm/@eslint+eslintrc@3.3.1/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2447:46)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/.pnpm/@eslint+eslintrc@3.3.1/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3383:33)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

packages/nodes/package.json

Oops! Something went wrong! :(

ESLint: 9.32.0

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './library.js' is not defined by "exports" in /node_modules/@workspace/eslint-config/package.json
Referenced from: /.eslintrc.js
at exportsNotFound (node:internal/modules/esm/resolve:310:10)
at packageExportsResolve (node:internal/modules/esm/resolve:658:9)
at resolveExports (node:internal/modules/cjs/loader:685:36)
at Module._findPath (node:internal/modules/cjs/loader:752:31)
at Module._resolveFilename (node:internal/modules/cjs/loader:1461:27)
at wrapResolveFilename (node:internal/modules/cjs/loader:1049:27)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1094:12)
at require.resolve (node:internal/modules/helpers:171:31)
at Object.resolve (/node_modules/.pnpm/@eslint+eslintrc@3.3.1/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2447:46)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/.pnpm/@eslint+eslintrc@3.3.1/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3383:33)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

  • 6 others

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.

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 expands BuildFlow’s node ecosystem by introducing a new Filter node (schema + executor + UI config), and improves the workflow configuration UX with richer variable insertion and more dynamic config behavior (including schema-aware dropdowns).

Changes:

  • Register and execute a new Filter action node (backend registry + execution registry + Zod input schema).
  • Improve interpolation/config resolution so exact {{...}} values can resolve to non-strings (arrays/objects) for downstream nodes.
  • Update the web config modal to support richer variable inputs and dynamic schema-driven dropdown options; add node icons/SVG assets.

Reviewed changes

Copilot reviewed 17 out of 24 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/nodes/src/registry/node-registry.ts Registers Filter node alongside existing nodes.
packages/nodes/src/registry/execution.registory.ts Registers Filter executor for runtime execution.
packages/nodes/src/google-sheets/google-sheets.node.ts Updates node description and adds icon path.
packages/nodes/src/gmail/gmail.node.ts Updates node description and adds icon path.
packages/nodes/src/filter/filter.node.ts Adds Filter node definition + registration.
packages/nodes/src/filter/filter.executor.ts Implements Filter node execution logic + outputs.
packages/nodes/package.json Adds workspace dependency on @repo/common.
packages/common/src/interpolation.ts Improves config variable resolution for exact-match variables.
packages/common/src/index.ts Adds FilterNodeInput Zod schema/type export.
apps/web/public/webhook.svg Adds webhook icon asset.
apps/web/public/google_sheet.svg Adds Google Sheets icon asset.
apps/web/public/gmail.svg Adds Gmail icon asset.
apps/web/app/workflows/[id]/components/ConfigModal.tsx Adds RichVariableInput + dynamic schema dropdown field rendering.
apps/web/app/lib/types/node.types.ts Adds new config field type: dynamic_schema_dropdown.
apps/web/app/lib/nodeConfigs/index.ts Registers Filter node config in UI registry.
apps/web/app/lib/nodeConfigs/filter.action.ts Adds UI config schema for Filter node.
apps/web/app/components/ui/variable-panel.tsx Switches variable insertion syntax to use nodeId-based keys.
apps/web/app/components/ui/TestPanel.tsx Improves rendering for nested arrays/objects.
apps/web/app/components/ui/RichVariableInput.tsx Adds contentEditable-based rich variable input component.
apps/web/app/components/nodes/BaseNode.tsx Adjusts node icon rendering/styling.

Comment on lines +183 to +187
metadata: {
operation_used: operation,
items_kept: filteredData.length,
items_discard: discardedData.length
}
Comment on lines +65 to 66
const formattedNodeName = node.nodeId

Comment on lines +1 to +4
import { config } from "dotenv";
import NodeRegistry from "../registry/node-registry.js";
import { FilterExecutor } from "./filter.executor.js";

Comment on lines +97 to +99
{icon ?
<img src={icon ? icon : "⚡"} className="w-16 h-16 object-obtain"
/> : ("⚡")}
Comment on lines +36 to +46
export function parseValueToHtml(rawValue: string, availableNodes: AvailableNode[]): string {
if (!rawValue) return "";

return rawValue.replace(/\{\{([^.]+)\.([^}]+)\}\}/g, (match, nodeId, path) => {
const node = availableNodes.find(n => n.id === nodeId);
const displayName = node ? node.name : "Unknown Node";
const colorClass = getNodeColorClass(nodeId);

return `<span contenteditable="false" class="inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
});
}

@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

🧹 Nitpick comments (2)
packages/nodes/src/filter/filter.executor.ts (1)

136-176: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap case declarations in blocks.

Biome's noSwitchDeclarations flags the const declarations at Line 138 (unique_results), Line 152 (newResult), and Line 168 (existing_data). These declarations are not block-scoped to their case, so they leak into the shared switch scope. The current break/return statements prevent a live bug today, but this pattern is fragile: a future edit that adds a case referencing one of these names before its own case runs would hit a temporal-dead-zone error. Wrap each case body in { }.

🔧 Proposed fix
             switch (operation) {
-                case 'unique_rows':
+                case 'unique_rows': {
                     const unique_results = this.handleUniqueRowsFromList(normalizedSource, sourceKey);
                     filteredData = unique_results.filteredData;
                     discardedData = unique_results.discardedData;
                     break;
+                }

-                case 'new_data_only':
+                case 'new_data_only': {
                     if (!sourceKey || !referenceKey) return {
                         success: false,
                         error: "sourceKey and referenceKey are required to compare datasets"
                     }
                     if (normalizedRef === undefined) return {
                         success: false,
                         error: "reference data is required to compare datasets"
                     }
                     const newResult = this.compareDataSet(normalizedSource, normalizedRef, sourceKey, referenceKey);

                     filteredData = newResult.uniqueData;
                     discardedData = [...newResult.existingData, ...newResult.discardedData]
                     break;
+                }

-                case 'existing_data_only':
+                case 'existing_data_only': {
                     if (!sourceKey || !referenceKey) return {
                         success: false,
                         error: "sourceKey and referenceKey are required to compare datasets"
                     }
                     if (normalizedRef === undefined) return {
                         success: false,
                         error: "reference data is required to compare datasets"
                     }

                     const existing_data = this.compareDataSet(normalizedSource, normalizedRef, sourceKey, referenceKey);

                     filteredData = existing_data.existingData;
                     discardedData = [...existing_data.discardedData, ...existing_data.uniqueData];
                     break;
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nodes/src/filter/filter.executor.ts` around lines 136 - 176, Wrap
each switch case body declaring a const in its own block: unique_rows around
unique_results, new_data_only around newResult, and existing_data_only around
existing_data. Preserve the existing validation, assignments, breaks, and return
behavior while ensuring each declaration is scoped only to its case.

Source: Linters/SAST tools

packages/common/src/index.ts (1)

196-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider enforcing sourceKey/referenceKey co-requirement in the schema.

FilterNodeInput leaves sourceKey and referenceKey independently optional. FilterExecutor.execute in packages/nodes/src/filter/filter.executor.ts (Lines 144-147, 159-162) duplicates the same if (!sourceKey || !referenceKey) check in two switch cases. Move this rule into the schema with .superRefine() so the constraint lives in one place and both callers benefit automatically.

🔧 Proposed refactor
 export const FilterNodeInput = z.object({
   operation: z.enum([
     "unique_rows",
     "new_data_only",
     "existing_data_only"
   ]),
   sourceData: z.array(z.any()),
   referenceData: z.array(z.any()).optional(),
   sourceKey: z.string().optional(),
   referenceKey: z.string().optional()
-})
+}).superRefine((data, ctx) => {
+  if (data.operation !== "unique_rows" && (!data.sourceKey || !data.referenceKey || !data.referenceData)) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      message: "sourceKey, referenceKey, and referenceData are required for comparison operations",
+    });
+  }
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/index.ts` around lines 196 - 210, Update the
FilterNodeInput schema with superRefine so sourceKey and referenceKey must
either both be provided or both be absent, and report a validation issue when
only one is present. Then remove the duplicated co-requirement checks from
FilterExecutor.execute’s affected switch cases, relying on schema validation
while preserving existing behavior for valid inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/components/nodes/BaseNode.tsx`:
- Line 98: Update the img element in BaseNode to replace the invalid
object-obtain Tailwind class with object-contain, preserving the existing sizing
and other classes.

In `@apps/web/app/components/ui/RichVariableInput.tsx`:
- Around line 39-45: Update the RichVariableInput rendering logic around the
rawValue replacement so rawValue, nodeId, path, and displayName are never
interpolated into innerHTML. Build the variable pill using DOM APIs or React
elements, assigning untrusted values through textContent or equivalent escaped
rendering while preserving the existing styling, data attributes, and display
behavior.
- Around line 44-45: Add the "pill" class to the serialized variable span
generated in the handleInput serialization logic, preserving all existing
classes and attributes so handleInput can recognize it and restore the
{{nodeId.path}} representation.
- Around line 75-84: Update the serialization logic in the RichVariableInput
element-processing branch to append "\n" for br elements and block-element
boundaries instead of relying solely on textContent. Preserve pill serialization
and text extraction for inline elements, ensuring multiline values retain their
line breaks when edited.

In `@apps/web/app/components/ui/TestPanel.tsx`:
- Around line 28-39: In the nested rendering logic, define and reuse a predicate
that identifies only non-null, non-array objects, replacing the current typeof
value[0] checks for both the padding class and renderArrayOfObjectsTable
selection. Keep arrays such as nested matrices in the inline branch, and
serialize object elements there so mixed arrays display their contents instead
of “[object Object]”.

In `@apps/web/app/components/ui/variable-panel.tsx`:
- Around line 155-164: The spreadsheet insertion logic in renderSpreadsheetTable
must handle direct 2D-array data when data.rows is absent: use the node path
itself for the full table and one-based row indexing with zero-based column
indexing for individual cells. Preserve the existing .rows-based paths when rows
is available, and update the “Select Entire Table” handler in the surrounding
spreadsheet output UI consistently.

In `@apps/web/app/lib/nodeConfigs/filter.action.ts`:
- Around line 32-57: Update the `referenceData` and `referenceKey` field
definitions in the filter action configuration to use `required: true` instead
of `required: false`. Keep their existing `showForOperation` conditions
unchanged so they remain required only when displayed for `new_data_only` and
`existing_data_only`, without affecting `unique_rows`.

In `@packages/common/src/interpolation.ts`:
- Around line 86-90: Update the bare-key branch in resolveVariable to return the
original interpolation expression when context[trimmed] is undefined, while
preserving all existing native values including 0, false, null, objects, and
arrays. Add regression tests covering missing bare variables and each of those
native value types, including the exact-match path used by ConfigModal.

In `@packages/nodes/src/filter/filter.executor.ts`:
- Around line 183-187: Update the metadata object returned by
FilterExecutor.execute so the items_discard key is renamed to items_discarded,
matching the declared output schema. Keep the existing discardedData.length
value and all other metadata fields unchanged.

---

Nitpick comments:
In `@packages/common/src/index.ts`:
- Around line 196-210: Update the FilterNodeInput schema with superRefine so
sourceKey and referenceKey must either both be provided or both be absent, and
report a validation issue when only one is present. Then remove the duplicated
co-requirement checks from FilterExecutor.execute’s affected switch cases,
relying on schema validation while preserving existing behavior for valid
inputs.

In `@packages/nodes/src/filter/filter.executor.ts`:
- Around line 136-176: Wrap each switch case body declaring a const in its own
block: unique_rows around unique_results, new_data_only around newResult, and
existing_data_only around existing_data. Preserve the existing validation,
assignments, breaks, and return behavior while ensuring each declaration is
scoped only to its case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df09b8f4-dbdd-4537-b089-21888b181cdc

📥 Commits

Reviewing files that changed from the base of the PR and between 52caf7c and f302c7a.

⛔ Files ignored due to path filters (7)
  • apps/web/public/filtering.png is excluded by !**/*.png
  • apps/web/public/gmail.png is excluded by !**/*.png
  • apps/web/public/gmail.svg is excluded by !**/*.svg
  • apps/web/public/google_sheet.png is excluded by !**/*.png
  • apps/web/public/google_sheet.svg is excluded by !**/*.svg
  • apps/web/public/webhook.png is excluded by !**/*.png
  • apps/web/public/webhook.svg is excluded by !**/*.svg
📒 Files selected for processing (17)
  • apps/web/app/components/nodes/BaseNode.tsx
  • apps/web/app/components/ui/RichVariableInput.tsx
  • apps/web/app/components/ui/TestPanel.tsx
  • apps/web/app/components/ui/variable-panel.tsx
  • apps/web/app/lib/nodeConfigs/filter.action.ts
  • apps/web/app/lib/nodeConfigs/index.ts
  • apps/web/app/lib/types/node.types.ts
  • apps/web/app/workflows/[id]/components/ConfigModal.tsx
  • packages/common/src/index.ts
  • packages/common/src/interpolation.ts
  • packages/nodes/package.json
  • packages/nodes/src/filter/filter.executor.ts
  • packages/nodes/src/filter/filter.node.ts
  • packages/nodes/src/gmail/gmail.node.ts
  • packages/nodes/src/google-sheets/google-sheets.node.ts
  • packages/nodes/src/registry/execution.registory.ts
  • packages/nodes/src/registry/node-registry.ts

<img src={icon ? icon : "⚡"} className="w-16 h-16 object-cover"
/> : ("⚡")}
{icon ?
<img src={icon ? icon : "⚡"} className="w-16 h-16 object-obtain"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n --fixed-strings 'object-obtain' apps/web/app/components/nodes/BaseNode.tsx && exit 1
rg -n --fixed-strings 'object-contain' apps/web/app/components/nodes/BaseNode.tsx

Repository: Dev-Pross/BuildFlow

Length of output: 239


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== relevant file excerpt =="
sed -n '85,105p' apps/web/app/components/nodes/BaseNode.tsx

echo
echo "== package/dependency references =="
rg -n '"tailwindcss"|tailwindcss|w-16|h-16|object-contain|object-obtain|object-fill' package.json node_modules apps web -g '!node_modules' -g '!dist' -g '!build' 2>/dev/null | head -120

echo
echo "== tailwind css if vendored =="
fd -a 'tailwindcss' node_modules . 2>/dev/null | head -5

echo
echo "== deterministic CSS behavior probe for the exact class token =="
python3 - <<'PY'
import re, pathlib
src = pathlib.Path('apps/web/app/components/nodes/BaseNode.tsx').read_text()
class_val = re.search(r'className="([^"]*)"', src).group(1)
print("className", class_val)
print("contains object-obtain", "object-obtain" in class_val)
print("contains object-contain", "object-contain" in class_val)
PY

Repository: Dev-Pross/BuildFlow

Length of output: 1511


🌐 Web query:

Tailwind CSS object-center utility object-fit class names

💡 Result:

In Tailwind CSS, object-position and object-fit are separate sets of utility classes used to control replaced elements like images and videos [1][2]. Object Position Utilities These classes control the object-position CSS property, which determines how an element is positioned within its container [1][3]. Class | CSS Property --- | --- object-bottom | object-position: bottom; [1] object-bottom-left | object-position: bottom left; [1] object-bottom-right | object-position: bottom right; [1] object-center | object-position: center; [1] object-left | object-position: left; [1] object-right | object-position: right; [1] object-top | object-position: top; [1] object-top-left | object-position: top left; [1] object-top-right | object-position: top right; [1] Object Fit Utilities These classes control the object-fit CSS property, which determines how an element is resized to fit its container [2][4]. Class | CSS Property --- | --- object-contain | object-fit: contain; [2] object-cover | object-fit: cover; [2] object-fill | object-fit: fill; [2] object-none | object-fit: none; [2] object-scale-down | object-fit: scale-down; [2] Both types of utilities support responsive design (using prefixes like md:), arbitrary values (using square brackets like object-[center_bottom]), and state modifiers (like hover:) [1][3][2][5][6].

Citations:


Use the valid Tailwind object-fit utility.

object-obtain is not a Tailwind object-fit class, so the icon image is not constrained. Replace it with object-contain.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/components/nodes/BaseNode.tsx` at line 98, Update the img
element in BaseNode to replace the invalid object-obtain Tailwind class with
object-contain, preserving the existing sizing and other classes.

Comment on lines +39 to +45
return rawValue.replace(/\{\{([^.]+)\.([^}]+)\}\}/g, (match, nodeId, path) => {
const node = availableNodes.find(n => n.id === nodeId);
const displayName = node ? node.name : "Unknown Node";
const colorClass = getNodeColorClass(nodeId);

return `<span contenteditable="false" class="inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not inject configuration values into innerHTML.

rawValue, nodeId, path, and displayName are inserted as HTML without encoding. A saved value that contains HTML can create active DOM when the effect synchronizes the editor. Construct text and pill nodes with DOM APIs or React elements. Do not use editable configuration text as HTML.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/components/ui/RichVariableInput.tsx` around lines 39 - 45,
Update the RichVariableInput rendering logic around the rawValue replacement so
rawValue, nodeId, path, and displayName are never interpolated into innerHTML.
Build the variable pill using DOM APIs or React elements, assigning untrusted
values through textContent or equivalent escaped rendering while preserving the
existing styling, data attributes, and display behavior.

Comment on lines +44 to +45
return `<span contenteditable="false" class="inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add the pill class to serialized variable spans.

handleInput recognizes a variable only when el.classList.contains("pill") is true. The span created on Line 44 has no pill class. After the user edits a value with a variable, the serializer stores Node Name > path instead of {{nodeId.path}}.

Proposed fix
- return `<span contenteditable="false" class="inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
+ return `<span contenteditable="false" class="pill inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
📝 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
return `<span contenteditable="false" class="inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
});
return `<span contenteditable="false" class="pill inline-flex items-center px-1.5 py-0.5 mx-1 rounded text-[10px] border align-middle font-mono select-all cursor-default ${colorClass}" data-id="${nodeId}" data-path="${path}">${displayName} &gt; ${path}</span>`;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/components/ui/RichVariableInput.tsx` around lines 44 - 45, Add
the "pill" class to the serialized variable span generated in the handleInput
serialization logic, preserving all existing classes and attributes so
handleInput can recognize it and restore the {{nodeId.path}} representation.

Comment on lines +75 to +84
if (el.classList.contains("pill")) {
const nodeId = el.getAttribute("data-id");
const path = el.getAttribute("data-path");
if (nodeId && path) {
rawString += `{{${nodeId}.${path}}}`;
}
} else {
// For any pasted elements (br, divs), just extract text
rawString += el.textContent || "";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve line breaks during serialization.

A <br> has empty textContent. Block elements also concatenate their text without a separator. This control now replaces textarea fields in apps/web/app/workflows/[id]/components/ConfigModal.tsx, so multiline values lose their newlines after editing. Serialize <br> and block boundaries as \n.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/components/ui/RichVariableInput.tsx` around lines 75 - 84,
Update the serialization logic in the RichVariableInput element-processing
branch to append "\n" for br elements and block-element boundaries instead of
relying solely on textContent. Preserve pill serialization and text extraction
for inline elements, ensuring multiline values retain their line breaks when
edited.

Comment on lines +28 to +39
<div className={`flex-[3] min-w-0 text-xs text-gray-300 border-l border-[#1a1f2e]/50 font-mono break-all ${isNested && Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' ? 'p-0' : 'px-4 py-2.5'}`}>
{isNested ? (
Array.isArray(value) ? (
<span className="text-purple-400">[{value.map(v => typeof v === 'string' ? `"${v}"` : String(v)).join(', ')}]</span>
value.length > 0 && typeof value[0] === 'object' ? (
<div className=" w-full">{renderArrayOfObjectsTable(value, true)}</div>
) : (
<span className="text-purple-400">[{value.map(v => typeof v === 'string' ? `"${v}"` : String(v)).join(', ')}]</span>
)
) : (
<span className="text-gray-500 italic">Object</span>
<div className="mt-2 mb-2 w-full">{renderObjectTable(value)}</div>
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a strict predicate for nested object arrays.

typeof value[0] === 'object' also matches null and arrays. A nested matrix such as [[1, 2], [3, 4]] is therefore rendered as an object table with index-based columns. A mixed array such as [1, { id: 2 }] renders the object as [object Object].

Define one predicate for non-null, non-array objects and reuse it for the padding class and renderer selection. Serialize object values in the inline branch when mixed arrays are supported.

Proposed fix
 {entries.map(([key, value]) => {
     const isNested = typeof value === 'object' && value !== null;
+    const isArrayOfObjects =
+        Array.isArray(value) &&
+        value.length > 0 &&
+        value.every(item =>
+            item !== null &&
+            typeof item === 'object' &&
+            !Array.isArray(item)
+        );
     return (
...
-        ${isNested && Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' ? 'p-0' : 'px-4 py-2.5'}`}>
+        ${isArrayOfObjects ? 'p-0' : 'px-4 py-2.5'}`}>
...
-        value.length > 0 && typeof value[0] === 'object' ? (
+        isArrayOfObjects ? (
...
-        value.map(v => typeof v === 'string' ? `"${v}"` : String(v)).join(', ')
+        value.map(v =>
+            typeof v === 'string'
+                ? `"${v}"`
+                : typeof v === 'object' && v !== null
+                    ? JSON.stringify(v)
+                    : String(v)
+        ).join(', ')
📝 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
<div className={`flex-[3] min-w-0 text-xs text-gray-300 border-l border-[#1a1f2e]/50 font-mono break-all ${isNested && Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' ? 'p-0' : 'px-4 py-2.5'}`}>
{isNested ? (
Array.isArray(value) ? (
<span className="text-purple-400">[{value.map(v => typeof v === 'string' ? `"${v}"` : String(v)).join(', ')}]</span>
value.length > 0 && typeof value[0] === 'object' ? (
<div className=" w-full">{renderArrayOfObjectsTable(value, true)}</div>
) : (
<span className="text-purple-400">[{value.map(v => typeof v === 'string' ? `"${v}"` : String(v)).join(', ')}]</span>
)
) : (
<span className="text-gray-500 italic">Object</span>
<div className="mt-2 mb-2 w-full">{renderObjectTable(value)}</div>
)
{entries.map(([key, value]) => {
const isNested = typeof value === 'object' && value !== null;
const isArrayOfObjects =
Array.isArray(value) &&
value.length > 0 &&
value.every(item =>
item !== null &&
typeof item === 'object' &&
!Array.isArray(item)
);
return (
<div className={`flex-[3] min-w-0 text-xs text-gray-300 border-l border-[`#1a1f2e`]/50 font-mono break-all ${isArrayOfObjects ? 'p-0' : 'px-4 py-2.5'}`}>
{isNested ? (
Array.isArray(value) ? (
isArrayOfObjects ? (
<div className=" w-full">{renderArrayOfObjectsTable(value, true)}</div>
) : (
<span className="text-purple-400">[{value.map(v =>
typeof v === 'string'
? `"${v}"`
: typeof v === 'object' && v !== null
? JSON.stringify(v)
: String(v)
).join(', ')}]</span>
)
) : (
<div className="mt-2 mb-2 w-full">{renderObjectTable(value)}</div>
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/components/ui/TestPanel.tsx` around lines 28 - 39, In the nested
rendering logic, define and reuse a predicate that identifies only non-null,
non-array objects, replacing the current typeof value[0] checks for both the
padding class and renderArrayOfObjectsTable selection. Keep arrays such as
nested matrices in the inline branch, and serialize object elements there so
mixed arrays display their contents instead of “[object Object]”.

Comment on lines +155 to +164
<div className="flex items-center gap-2">
<span>Spreadsheet Data</span>
<button
onClick={() => handleInsert(`{{${formattedNodeName}.rows}}`)}
className="px-2 py-0.5 bg-blue-500/20 text-blue-400 hover:bg-blue-500/40 rounded transition-colors"
title="Insert the entire array of data"
>
Select Entire Table
</button>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how interpolation resolves node paths and whether it aliases direct arrays as `rows`.
rg -n -C 6 'resolveVariable|rows' \
  packages/common/src/interpolation.ts \
  apps/web/app/components/ui/variable-panel.tsx \
  'apps/web/app/workflows/[id]/components/ConfigModal.tsx'

Repository: Dev-Pross/BuildFlow

Length of output: 29949


Insert direct-array paths for spreadsheet outputs.

When testOutput.data is a direct 2D array, renderSpreadsheetTable builds table insertions using rows, but InterpolationContext stores the node data under context[nodeId] without aliasing .rows. Use {{${formattedNodeName}}} for the full table and {{${formattedNodeName}[${rowIndex + 1}][${colIndex}]}} for cells when data.rows is missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/components/ui/variable-panel.tsx` around lines 155 - 164, The
spreadsheet insertion logic in renderSpreadsheetTable must handle direct
2D-array data when data.rows is absent: use the node path itself for the full
table and one-based row indexing with zero-based column indexing for individual
cells. Preserve the existing .rows-based paths when rows is available, and
update the “Select Entire Table” handler in the surrounding spreadsheet output
UI consistently.

Comment on lines +32 to +57
{
name: "referenceData",
label: "Reference Data (JSON Array)",
type: "textarea",
placeholder: '{{ Database.rows }}',
required: false,
dependsOn: "operation",
showForOperation: ["new_data_only", "existing_data_only"], // Hides this field if unique_rows is selected!
description: "The master list to compare against"
},
{
name: "sourceKey",
label: "Source Column / Key",
type: "dynamic_schema_dropdown",
required: false,
description: "The specific column to use for comparison. (Optional for deduplication, required for comparisons)"
},
{
name: "referenceKey",
label: "Reference Column / Key",
type: "dynamic_schema_dropdown",
required: false,
dependsOn: "operation",
showForOperation: ["new_data_only", "existing_data_only"],
description: "The column in the Reference Data to match against."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mark referenceData and referenceKey as required when shown.

referenceData (Line 37) and referenceKey (Line 53) are required: false, but FilterExecutor.execute in packages/nodes/src/filter/filter.executor.ts returns a validation error when sourceKey/referenceKey are missing for the new_data_only and existing_data_only operations. Both fields already use showForOperation to hide themselves for unique_rows, so setting required: true on them does not affect the unique_rows flow and lets the UI catch the missing-field case before the user runs the node.

🔧 Proposed fix
         {
             name: "referenceData",
             label: "Reference Data (JSON Array)",
             type: "textarea",
             placeholder: '{{ Database.rows }}',
-            required: false,
+            required: true,
             dependsOn: "operation",
             showForOperation: ["new_data_only", "existing_data_only"], // Hides this field if unique_rows is selected!
             description: "The master list to compare against"
         },
         {
             name: "referenceKey",
             label: "Reference Column / Key",
             type: "dynamic_schema_dropdown",
-            required: false,
+            required: true,
             dependsOn: "operation",
             showForOperation: ["new_data_only", "existing_data_only"],
             description: "The column in the Reference Data to match against."
         }
📝 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
{
name: "referenceData",
label: "Reference Data (JSON Array)",
type: "textarea",
placeholder: '{{ Database.rows }}',
required: false,
dependsOn: "operation",
showForOperation: ["new_data_only", "existing_data_only"], // Hides this field if unique_rows is selected!
description: "The master list to compare against"
},
{
name: "sourceKey",
label: "Source Column / Key",
type: "dynamic_schema_dropdown",
required: false,
description: "The specific column to use for comparison. (Optional for deduplication, required for comparisons)"
},
{
name: "referenceKey",
label: "Reference Column / Key",
type: "dynamic_schema_dropdown",
required: false,
dependsOn: "operation",
showForOperation: ["new_data_only", "existing_data_only"],
description: "The column in the Reference Data to match against."
}
{
name: "referenceData",
label: "Reference Data (JSON Array)",
type: "textarea",
placeholder: '{{ Database.rows }}',
required: true,
dependsOn: "operation",
showForOperation: ["new_data_only", "existing_data_only"], // Hides this field if unique_rows is selected!
description: "The master list to compare against"
},
{
name: "sourceKey",
label: "Source Column / Key",
type: "dynamic_schema_dropdown",
required: false,
description: "The specific column to use for comparison. (Optional for deduplication, required for comparisons)"
},
{
name: "referenceKey",
label: "Reference Column / Key",
type: "dynamic_schema_dropdown",
required: true,
dependsOn: "operation",
showForOperation: ["new_data_only", "existing_data_only"],
description: "The column in the Reference Data to match against."
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/lib/nodeConfigs/filter.action.ts` around lines 32 - 57, Update
the `referenceData` and `referenceKey` field definitions in the filter action
configuration to use `required: true` instead of `required: false`. Keep their
existing `showForOperation` conditions unchanged so they remain required only
when displayed for `new_data_only` and `existing_data_only`, without affecting
`unique_rows`.

Comment on lines +86 to 90

const dotIndex = trimmed.indexOf('.');
if (dotIndex === -1) {
return context[trimmed];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve unresolved bare variables.

For {{missing}}, resolveVariable returns undefined because the bare-key branch returns context[trimmed]. The new exact-match branch stores that value directly. ConfigModal only detects unresolved values that are strings containing {{, so the missing field can reach api.execute.node without a warning.

Return the original interpolation expression when a bare lookup is missing. Add regression tests for missing variables and native values such as 0, false, null, objects, and arrays.

Proposed fix
   if (dotIndex === -1) {
-    return context[trimmed];
+    const value = context[trimmed];
+    return value !== undefined ? value : `{{${variable}}}`;
   }

Also applies to: 176-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/interpolation.ts` around lines 86 - 90, Update the
bare-key branch in resolveVariable to return the original interpolation
expression when context[trimmed] is undefined, while preserving all existing
native values including 0, false, null, objects, and arrays. Add regression
tests covering missing bare variables and each of those native value types,
including the exact-match path used by ConfigModal.

Comment on lines +183 to +187
metadata: {
operation_used: operation,
items_kept: filteredData.length,
items_discard: discardedData.length
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fix metadata key mismatch: items_discard vs. items_discarded.

FilterExecutor.execute emits metadata.items_discard (Line 186), but apps/web/app/lib/nodeConfigs/filter.action.ts declares the output field at metadata.items_discarded (Line 68). Any variable reference or dynamic-schema lookup built against the declared output path resolves to undefined at runtime because the actual key differs. Rename the executor's key to match the declared output schema.

🐛 Proposed fix
             metadata: {
                 operation_used: operation,
                 items_kept: filteredData.length,
-                items_discard: discardedData.length
+                items_discarded: discardedData.length
             }
📝 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
metadata: {
operation_used: operation,
items_kept: filteredData.length,
items_discard: discardedData.length
}
metadata: {
operation_used: operation,
items_kept: filteredData.length,
items_discarded: discardedData.length
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nodes/src/filter/filter.executor.ts` around lines 183 - 187, Update
the metadata object returned by FilterExecutor.execute so the items_discard key
is renamed to items_discarded, matching the declared output schema. Keep the
existing discardedData.length value and all other metadata fields unchanged.

@Vamsi-o
Vamsi-o merged commit 7942de1 into main Aug 3, 2026
4 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.

4 participants