Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions apps/web/app/components/nodes/BaseNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,23 +94,23 @@ export default function BaseNode({ id, type, data }: BaseNodeProps) {
{/* Icon + Label */}
<div className="flex flex-col items-center gap-2 mb-1">
<span className="text-xl p-2 rounded-full object-center">
{ icon ?
<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 +97 to +99

</span>
<span className="font-semibold text-sm">{label}</span>
</div>
<div className="flex justify-center w-full">
{data.isConfigured ? (
<span className="ml-2 px-2 py-1 bg-green-100 text-green-800 text-center text-xs rounded-full">
✓ Configured
</span>
) : (
<span className="ml-2 px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">
Not Configured
</span>
)}
{data.isConfigured ? (
<span className="ml-2 px-2 py-1 bg-green-100 text-green-800 text-center text-xs rounded-full">
✓ Configured
</span>
) : (
<span className="ml-2 px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">
Not Configured
</span>
)}
</div>

{/* Buttons */}
Expand All @@ -120,7 +120,7 @@ export default function BaseNode({ id, type, data }: BaseNodeProps) {
onClick={onConfigure}
className="text-xs px-2 py-1 bg-blue-100 rounded hover:bg-blue-200"
>
<svg width="10px" height="10px" viewBox="0 0 32.00 32.00" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round" stroke="#000000" strokeWidth="0.576"></g><g id="SVGRepo_iconCarrier"><title>file_type_config</title><path d="M23.265,24.381l.9-.894c4.164.136,4.228-.01,4.411-.438l1.144-2.785L29.805,20l-.093-.231c-.049-.122-.2-.486-2.8-2.965V15.5c3-2.89,2.936-3.038,2.765-3.461L28.538,9.225c-.171-.422-.236-.587-4.37-.474l-.9-.93a20.166,20.166,0,0,0-.141-4.106l-.116-.263-2.974-1.3c-.438-.2-.592-.272-3.4,2.786l-1.262-.019c-2.891-3.086-3.028-3.03-3.461-2.855L9.149,3.182c-.433.175-.586.237-.418,4.437l-.893.89c-4.162-.136-4.226.012-4.407.438L2.285,11.733,2.195,12l.094.232c.049.12.194.48,2.8,2.962l0,1.3c-3,2.89-2.935,3.038-2.763,3.462l1.138,2.817c.174.431.236.584,4.369.476l.9.935a20.243,20.243,0,0,0,.137,4.1l.116.265,2.993,1.308c.435.182.586.247,3.386-2.8l1.262.016c2.895,3.09,3.043,3.03,3.466,2.859l2.759-1.115C23.288,28.644,23.44,28.583,23.265,24.381ZM11.407,17.857a4.957,4.957,0,1,1,6.488,2.824A5.014,5.014,0,0,1,11.407,17.857Z" fill="#000000"></path></g></svg>
<svg width="10px" height="10px" viewBox="0 0 32.00 32.00" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round" stroke="#000000" strokeWidth="0.576"></g><g id="SVGRepo_iconCarrier"><title>file_type_config</title><path d="M23.265,24.381l.9-.894c4.164.136,4.228-.01,4.411-.438l1.144-2.785L29.805,20l-.093-.231c-.049-.122-.2-.486-2.8-2.965V15.5c3-2.89,2.936-3.038,2.765-3.461L28.538,9.225c-.171-.422-.236-.587-4.37-.474l-.9-.93a20.166,20.166,0,0,0-.141-4.106l-.116-.263-2.974-1.3c-.438-.2-.592-.272-3.4,2.786l-1.262-.019c-2.891-3.086-3.028-3.03-3.461-2.855L9.149,3.182c-.433.175-.586.237-.418,4.437l-.893.89c-4.162-.136-4.226.012-4.407.438L2.285,11.733,2.195,12l.094.232c.049.12.194.48,2.8,2.962l0,1.3c-3,2.89-2.935,3.038-2.763,3.462l1.138,2.817c.174.431.236.584,4.369.476l.9.935a20.243,20.243,0,0,0,.137,4.1l.116.265,2.993,1.308c.435.182.586.247,3.386-2.8l1.262.016c2.895,3.09,3.043,3.03,3.466,2.859l2.759-1.115C23.288,28.644,23.44,28.583,23.265,24.381ZM11.407,17.857a4.957,4.957,0,1,1,6.488,2.824A5.014,5.014,0,0,1,11.407,17.857Z" fill="#000000"></path></g></svg>
</button>
)}
{onTest && (
Expand Down
102 changes: 102 additions & 0 deletions apps/web/app/components/ui/RichVariableInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";
import React, { useRef, useEffect } from 'react';

export interface AvailableNode {
id: string;
name: string;
}

interface RichVariableInputProps {
value: string;
onChange: (newValue: string) => void;
availableNodes: AvailableNode[];
placeholder?: string;
onFocus?: () => void;
}

const COLORS = [
"bg-blue-500/20 text-blue-300 border-blue-500/50",
"bg-green-500/20 text-green-300 border-green-500/50",
"bg-purple-500/20 text-purple-300 border-purple-500/50",
"bg-orange-500/20 text-orange-300 border-orange-500/50",
"bg-pink-500/20 text-pink-300 border-pink-500/50",
"bg-teal-500/20 text-teal-300 border-teal-500/50",
];

export function getNodeColorClass(nodeId: string): string {
if (!nodeId) return COLORS[0]!;
let hash = 0;
for (let i = 0; i < nodeId.length; i++) {
hash = nodeId.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % COLORS.length;
return COLORS[index]!;
}

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>`;
});
Comment on lines +39 to +45

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

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 +36 to +46

export function RichVariableInput({ value, onChange, availableNodes, placeholder, onFocus }: RichVariableInputProps) {
const editorRef = useRef<HTMLDivElement>(null);
const isInternalUpdate = useRef(false);

// Initial injection of HTML when the external value changes
useEffect(() => {
if (editorRef.current && !isInternalUpdate.current) {
const newHtml = parseValueToHtml(value, availableNodes);
if (editorRef.current.innerHTML !== newHtml) {
editorRef.current.innerHTML = newHtml;
}
}
isInternalUpdate.current = false;
}, [value, availableNodes]);

// Handle user input and serialize back to raw string
const handleInput = () => {
if (!editorRef.current) return;

let rawString = "";

// Iterate through the DOM children to reconstruct the string
editorRef.current.childNodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
rawString += node.textContent || "";
} else if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as HTMLElement;
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 || "";
}
Comment on lines +75 to +84

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.

}
});

isInternalUpdate.current = true;
onChange(rawString);
};

return (
<div
ref={editorRef}
contentEditable={true}
onInput={handleInput}
onFocus={onFocus}
data-placeholder={placeholder}
className="w-full min-h-[40px] px-3 py-2 rounded-md border border-[#2a3525] bg-[#141a14] text-sm text-[#e8e8d8] focus:outline-none focus:border-[#baf266]/50 empty:before:content-[attr(data-placeholder)] empty:before:text-[#4a5440]"
/>
);
}
60 changes: 32 additions & 28 deletions apps/web/app/components/ui/TestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ export function TestPanel({ testResult, nodeName, nodeIcon }: TestPanelProps) {
<div className="flex-[2] px-4 py-2.5 text-xs font-medium text-blue-300 truncate">
{key}
</div>
<div className="flex-[3] px-4 py-2.5 text-xs text-gray-300 border-l border-[#1a1f2e]/50 font-mono break-all">
<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>
)

Comment on lines +28 to +39

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]”.

) : typeof value === 'boolean' ? (
<span className={value ? 'text-green-400' : 'text-red-400'}>{String(value)}</span>
) : value === null || value === undefined ? (
Expand Down Expand Up @@ -84,7 +89,7 @@ export function TestPanel({ testResult, nodeName, nodeIcon }: TestPanelProps) {
};

// Array of objects table
const renderArrayOfObjectsTable = (data: any[]) => {
const renderArrayOfObjectsTable = (data: any[], isNestedTable = false) => {
const keysSet = new Set<string>();
data.slice(0, 100).forEach(item => {
if (item && typeof item === 'object') Object.keys(item).forEach(k => keysSet.add(k));
Expand All @@ -106,31 +111,30 @@ export function TestPanel({ testResult, nodeName, nodeIcon }: TestPanelProps) {
}

return (
<div className="w-full border border-[#2a2f3e] rounded-lg overflow-hidden overflow-x-auto">
<table className="w-full text-left text-xs text-gray-300 min-w-max border-collapse">
<thead className="bg-[#161b22] sticky top-0">
<tr>
<th className="px-3 py-2.5 w-10 text-center border-r border-b border-[#2a2f3e] text-gray-500 font-normal text-[10px]">#</th>
{headers.map(h => (
<th key={h} className="px-4 py-2.5 border-r border-b border-[#2a2f3e] font-medium text-blue-300 truncate max-w-[160px]">{h}</th>
))}
</tr>
</thead>
<tbody>
{data.slice(0, 100).map((item, ri) => (
<tr key={ri} className="border-b border-[#1a1f2e]/50 hover:bg-[#1f2536] transition-colors">
<td className="px-3 py-2 text-center border-r border-[#1a1f2e]/50 text-gray-600 bg-[#161b26] text-[10px]">{ri}</td>
{headers.map(h => {
const val = item?.[h];
const display = typeof val === 'object' && val !== null ? JSON.stringify(val) : String(val ?? '');
return (
<td key={h} className="px-4 py-2 border-r border-[#1a1f2e]/50 truncate max-w-[200px] text-gray-300">{display}</td>
);
})}
</tr>
<div className={`w-full overflow-hidden overflow-x-auto ${isNestedTable ? '' : 'border border-[#2a2f3e] rounded-lg'}`}> <table className="w-full text-left text-xs text-gray-300 min-w-max border-collapse">
<thead className="bg-[#161b22] sticky top-0">
<tr>
<th className="px-3 py-2.5 w-10 text-center border-r border-b border-[#2a2f3e] text-gray-500 font-normal text-[10px]">#</th>
{headers.map(h => (
<th key={h} className="px-4 py-2.5 border-r border-b border-[#2a2f3e] font-medium text-blue-300 truncate max-w-[160px]">{h}</th>
))}
</tbody>
</table>
</tr>
</thead>
<tbody>
{data.slice(0, 100).map((item, ri) => (
<tr key={ri} className="border-b border-[#1a1f2e]/50 hover:bg-[#1f2536] transition-colors">
<td className="px-3 py-2 text-center border-r border-[#1a1f2e]/50 text-gray-600 bg-[#161b26] text-[10px]">{ri}</td>
{headers.map(h => {
const val = item?.[h];
const display = typeof val === 'object' && val !== null ? JSON.stringify(val) : String(val ?? '');
return (
<td key={h} className="px-4 py-2 border-r border-[#1a1f2e]/50 truncate max-w-[200px] text-gray-300">{display}</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
};
Expand Down
27 changes: 18 additions & 9 deletions apps/web/app/components/ui/variable-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function VariablePanel({ previousNodes, onInsert, activeField, onTestNode
);
}

const formattedNodeName = node.nodeName.toLowerCase().replace(/\s+/g, '_');
const formattedNodeName = node.nodeId

Comment on lines +65 to 66
const renderRows = (vars: VariableDefinition[], depth: number = 0, currParentPath: string = "") => {
return vars.map((variable, idx) => {
Expand Down Expand Up @@ -141,8 +141,8 @@ export function VariablePanel({ previousNodes, onInsert, activeField, onTestNode
};

// Rendering for Spreadsheet (Arrays of arrays)
const renderSpreadsheetTable = (nodeName: string, data: any) => {
const formattedNodeName = nodeName.toLowerCase().replace(/\s+/g, '_');
const renderSpreadsheetTable = (nodeName: string, nodeId: string, data: any) => {
const formattedNodeName = nodeId;
const rows = data.rows || data; // Handle data directly if it's the 2D array
if (!Array.isArray(rows) || rows.length === 0) return null;

Expand All @@ -152,7 +152,16 @@ export function VariablePanel({ previousNodes, onInsert, activeField, onTestNode
return (
<div className="overflow-x-auto w-full border-t border-gray-800 scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-transparent">
<div className="text-[10px] p-2 bg-gray-800/50 text-gray-400 flex justify-between items-center border-b border-gray-800">
<span>Spreadsheet Data</span>
<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>
Comment on lines +155 to +164

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.

<span>{dataRows.length} rows</span>
</div>
<table className="w-full text-left text-xs text-gray-300 min-w-max border-collapse">
Expand Down Expand Up @@ -199,8 +208,8 @@ export function VariablePanel({ previousNodes, onInsert, activeField, onTestNode
};

// Rendering for standard array of objects
const renderArrayTable = (nodeName: string, dataArray: any[], isTested: boolean) => {
const formattedNodeName = nodeName.toLowerCase().replace(/\s+/g, '_');
const renderArrayTable = (nodeName: string, nodeId: string, dataArray: any[], isTested: boolean) => {
const formattedNodeName = nodeId;

// Find all unique keys across objects to form headers
const keysSet = new Set<string>();
Expand Down Expand Up @@ -451,17 +460,17 @@ export function VariablePanel({ previousNodes, onInsert, activeField, onTestNode
{/* Try matching Spreadsheet Pattern */}
{testOutput.data.rows && Array.isArray(testOutput.data.rows) && testOutput.data.rows.length > 0 && Array.isArray(testOutput.data.rows[0]) ? (
<div className="bg-[#111620]">
{renderSpreadsheetTable(node.nodeName, testOutput.data)}
{renderSpreadsheetTable(node.nodeName, node.nodeId, testOutput.data)}
</div>
) : Array.isArray(testOutput.data) && testOutput.data.length > 0 && Array.isArray(testOutput.data[0]) ? (
<div className="bg-[#111620]">
{renderSpreadsheetTable(node.nodeName, testOutput.data)}
{renderSpreadsheetTable(node.nodeName, node.nodeId, testOutput.data)}
</div>
) :
/* Try matching Standard Array pattern */
Array.isArray(testOutput.data) ? (
<div className="bg-[#111620]">
{renderArrayTable(node.nodeName, testOutput.data, isTested)}
{renderArrayTable(node.nodeName, node.nodeId, testOutput.data, isTested)}
</div>
) : (
/* Fallback to Tree Table if it's an object or string */
Expand Down
Loading