diff --git a/.gitignore b/.gitignore index 5fa7951..7274fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ apps/http-backend/tsconfig.tsbuildinfo packages/common/tsconfig.tsbuildinfo packages/db/tsconfig.tsbuildinfo apps/http-backend/tsconfig.tsbuildinfo +packages/nodes/tsconfig.tsbuildinfo diff --git a/apps/web/app/components/ui/TestPanel.tsx b/apps/web/app/components/ui/TestPanel.tsx new file mode 100644 index 0000000..320d708 --- /dev/null +++ b/apps/web/app/components/ui/TestPanel.tsx @@ -0,0 +1,296 @@ +"use client"; + +interface TestPanelProps { + testResult: any; + nodeName?: string; + nodeIcon?: string; +} + +export function TestPanel({ testResult, nodeName, nodeIcon }: TestPanelProps) { + + const renderObjectTable = (data: Record) => { + const entries = Object.entries(data); + if (entries.length === 0) return ; + + return ( +
+
+
Field
+
Value
+
+ {entries.map(([key, value]) => { + const isNested = typeof value === 'object' && value !== null; + return ( +
+
+ {key} +
+
+ {isNested ? ( + Array.isArray(value) ? ( + [{value.map(v => typeof v === 'string' ? `"${v}"` : String(v)).join(', ')}] + ) : ( + Object + ) + ) : typeof value === 'boolean' ? ( + {String(value)} + ) : value === null || value === undefined ? ( + null + ) : ( + {String(value)} + )} +
+
+ ); + })} +
+ ); + }; + + // Spreadsheet table for arrays of arrays (rows data) + const renderSpreadsheetTable = (rows: any[][]) => { + if (rows.length === 0) return ; + const headers = rows[0] as string[]; + const dataRows = rows.slice(1); + + return ( +
+ + + + + {headers.map((h, i) => ( + + ))} + + + + {dataRows.map((row, ri) => ( + + + {headers.map((_, ci) => ( + + ))} + + ))} + +
# + {h || `Column ${i + 1}`} +
{ri + 1} + {String(row[ci] ?? '')} +
+
+ ); + }; + + // Array of objects table + const renderArrayOfObjectsTable = (data: any[]) => { + const keysSet = new Set(); + data.slice(0, 100).forEach(item => { + if (item && typeof item === 'object') Object.keys(item).forEach(k => keysSet.add(k)); + }); + const headers = Array.from(keysSet); + + if (headers.length === 0) { + // Array of primitives + return ( +
+ {data.slice(0, 100).map((item, i) => ( +
+
{i}
+
{String(item)}
+
+ ))} +
+ ); + } + + return ( +
+ + + + + {headers.map(h => ( + + ))} + + + + {data.slice(0, 100).map((item, ri) => ( + + + {headers.map(h => { + const val = item?.[h]; + const display = typeof val === 'object' && val !== null ? JSON.stringify(val) : String(val ?? ''); + return ( + + ); + })} + + ))} + +
#{h}
{ri}{display}
+
+ ); + }; + + // Decide which renderer to use + const renderResult = (data: any) => { + if (data === null || data === undefined) return ; + + // String result + if (typeof data === 'string') { + return ( +
+

{data}

+
+ ); + } + + // Spreadsheet pattern: { rows: [[headers], [row1], ...] } + if (data.rows && Array.isArray(data.rows) && data.rows.length > 0 && Array.isArray(data.rows[0])) { + return ( + <> +
+ Spreadsheet + {data.rows.length - 1} rows +
+ {renderSpreadsheetTable(data.rows)} + + ); + } + + // Direct 2D array + if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) { + return ( + <> +
+ Table + {data.length - 1} rows +
+ {renderSpreadsheetTable(data)} + + ); + } + + // Array of objects + if (Array.isArray(data) && data.length > 0) { + return ( + <> +
+ Array + {data.length} items +
+ {renderArrayOfObjectsTable(data)} + + ); + } + + // Plain object (Gmail output, generic results) + if (typeof data === 'object') { + // Check if there's a summary field (enriched outputs like Gmail) + const hasSummary = data.summary && typeof data.summary === 'string'; + return ( + <> + {hasSummary && ( +
+ โœ… + {data.summary} +
+ )} + {renderObjectTable(data)} + + ); + } + + // Fallback + return ( +
+
{JSON.stringify(data, null, 2)}
+
+ ); + }; + + return ( +
+ {/* Header */} +
+
+ {nodeIcon && ( + + )} +

+ {testResult !== null && testResult !== undefined ? ( + + + + ) : ( + + + + )} + {nodeName ? `${nodeName} Output` : 'Test Output'} +

+
+ {testResult !== null && testResult !== undefined ? ( +
+ {typeof testResult === 'object' && !Array.isArray(testResult) && testResult.status === 'sent' + ? 'Email sent successfully' + : Array.isArray(testResult) + ? `${testResult.length} items returned` + : testResult?.rows + ? `${testResult.rows.length - 1} rows returned` + : 'Execution completed' + } +
+ ) : ( +
+ Not tested yet +
+ )} +
+ + {/* Body */} +
+ {testResult !== null && testResult !== undefined + ? renderResult(testResult) + : + } +
+
+ ); +} + +// "Not Tested Yet" state โ€” shown before any test is run +function NotTestedState({ nodeName }: { nodeName?: string }) { + return ( +
+
+ + + +
+

Not Tested Yet

+

+ Click Test Node to execute {nodeName || 'this node'} and see the output here +

+
+ ); +} + +// Empty state for tested but empty response +function EmptyState({ message }: { message: string }) { + return ( +
+
+ + + +
+

{message}

+
+ ); +} + +export default TestPanel; \ No newline at end of file diff --git a/apps/web/app/components/ui/variable-panel.tsx b/apps/web/app/components/ui/variable-panel.tsx index 3de49f1..336a7a6 100644 --- a/apps/web/app/components/ui/variable-panel.tsx +++ b/apps/web/app/components/ui/variable-panel.tsx @@ -3,35 +3,31 @@ import { PreviousNodeOutput, VariableDefinition } from "@/app/lib/types/node.types"; import { useAppSelector } from "@/app/hooks/redux"; import { selectAllOutputs, NodeTestOutput } from "@/store/slices/nodeOutputSlice"; +import { useState } from "react"; interface VariablePanelProps { previousNodes: PreviousNodeOutput[]; onInsert: (variableSyntax: string) => void; activeField: string | null; + onTestNode?: (nodeId: string) => void; } -export function VariablePanel({ previousNodes, onInsert, activeField }: VariablePanelProps) { +export function VariablePanel({ previousNodes, onInsert, activeField, onTestNode }: VariablePanelProps) { + // State for accordion behavior + const [expandedNodeId, setExpandedNodeId] = useState( + previousNodes?.length > 0 ? previousNodes[previousNodes.length - 1]!.nodeId : null + ); + const state = useAppSelector((state) => state.nodeOutput); + // Get tested outputs from Redux const testedOutputs = useAppSelector(selectAllOutputs); - - // Debug logging - console.log('[VariablePanel] previousNodes:', previousNodes); - console.log('[VariablePanel] testedOutputs from Redux:', testedOutputs); - + // Merge static outputSchema with dynamic tested data - const enrichedNodes: PreviousNodeOutput[] = previousNodes.map(node => { + const enrichedNodes: PreviousNodeOutput[] = previousNodes?.map(node => { const testedData = testedOutputs[node.nodeId]; - - console.log(`[VariablePanel] Node ${node.nodeName} (${node.nodeId}):`, { - hasTestedData: !!testedData, - testedSuccess: testedData?.success, - testedVariablesCount: testedData?.variables?.length, - staticVariablesCount: node.variables?.length - }); - + // If node was tested, use the dynamic variables from real output - if (testedData?.success && testedData.variables.length > 0) { - console.log(`[VariablePanel] Using DYNAMIC variables for ${node.nodeName}:`, testedData.variables); + if (testedData?.success && testedData.variables && testedData.variables.length > 0) { return { ...node, variables: testedData.variables, @@ -39,127 +35,449 @@ export function VariablePanel({ previousNodes, onInsert, activeField }: Variable _tested: true } as PreviousNodeOutput & { _tested?: boolean }; } - + // Otherwise use the static outputSchema - console.log(`[VariablePanel] Using STATIC variables for ${node.nodeName}:`, node.variables); return node; - }); - - const handleClick = (nodeName: string, variable: VariableDefinition) => { - // Build the variable syntax: {{nodeName.path}} - const syntax = `{{${nodeName}.${variable.path}}}`; - onInsert(syntax); + }) || []; + + // Handlers for insertion + const handleInsert = (syntax: string) => { + if (activeField) { + onInsert(syntax); + } + }; + + // --- Render Helpers --- + + // Renders a tabular tree view for standard variables + const renderVariableTable = (node: PreviousNodeOutput, isTested: boolean) => { + const isWebhook = node.nodeName.toLowerCase().includes('webhook'); + if (!node.variables || node.variables.length === 0) { + return ( +

+ {isWebhook + ? 'Webhook data will be available when the workflow is triggered' + : 'Test this node to discover available variables'} +

+ ); + } + + const formattedNodeName = node.nodeName.toLowerCase().replace(/\s+/g, '_'); + + const renderRows = (vars: VariableDefinition[], depth: number = 0, currParentPath: string = "") => { + return vars.map((variable, idx) => { + // Build path strictly by concatenating dot if needed + let strictConcatPath = variable.path; + if (currParentPath) { + const prefix = variable.path.startsWith('.') || variable.path.startsWith('[') ? '' : '.'; + strictConcatPath = `${currParentPath}${prefix}${variable.path.replace(/^\./, '')}`; + } + + const hasChildren = variable.children && variable.children.length > 0; + + return ( +
+
{ + e.stopPropagation(); + if (activeField && isTested) { + handleInsert(`{{${formattedNodeName}.${strictConcatPath}}}`); + } + }} + title={ + !isTested + ? "Test node first to map data" + : activeField + ? `Insert {{${formattedNodeName}.${strictConcatPath}}}` + : "Select a field first" + } + > +
+ + {getTypeIcon(variable.type)} + + + {variable.name} + + {variable.type && ( + + {variable.type} + + )} +
+
+ {variable.sampleValue !== undefined && variable.sampleValue !== null + ? (typeof variable.sampleValue === 'object' + ? Object/Array + : String(variable.sampleValue)) + : No Data + } +
+
+ {hasChildren && renderRows(variable.children || [], depth + 1, strictConcatPath)} +
+ ); + }); + }; + + return ( +
+
+
Name
+
Value
+
+
+ {renderRows(node.variables)} +
+
+ ); + }; + + // Rendering for Spreadsheet (Arrays of arrays) + const renderSpreadsheetTable = (nodeName: string, data: any) => { + const formattedNodeName = nodeName.toLowerCase().replace(/\s+/g, '_'); + const rows = data.rows || data; // Handle data directly if it's the 2D array + if (!Array.isArray(rows) || rows.length === 0) return null; + + const headers = rows[0] as string[]; + const dataRows = rows.slice(1); + + return ( +
+
+ Spreadsheet Data + {dataRows.length} rows +
+ + + + + {headers.map((header, colIndex) => { + const colPath = String(header).trim().toLowerCase().replace(/\s+/g, '_') || `column_${colIndex + 1}`; + return ( + + ); + })} + + + + {dataRows.slice(0, 50).map((row: any[], rowIndex: number) => ( + + + {headers.map((_, colIndex) => ( + + ))} + + ))} + +
# handleInsert(`{{${formattedNodeName}.${colPath}}}`)} + title={`Insert entire column array: {{${formattedNodeName}.${colPath}}}`} + > + {header || `Column ${colIndex + 1}`} +
{rowIndex + 1} handleInsert(`{{${formattedNodeName}.rows[${rowIndex + 1}][${colIndex}]}}`)} + title={`Insert cell {{${formattedNodeName}.rows[${rowIndex + 1}][${colIndex}]}}`} + > + {String(row[colIndex] ?? '')} +
+
+ ); + }; + + // Rendering for standard array of objects + const renderArrayTable = (nodeName: string, dataArray: any[], isTested: boolean) => { + const formattedNodeName = nodeName.toLowerCase().replace(/\s+/g, '_'); + + // Find all unique keys across objects to form headers + const keysSet = new Set(); + dataArray.slice(0, 50).forEach(item => { + if (item && typeof item === 'object') { + Object.keys(item).forEach(k => keysSet.add(k)); + } + }); + const headers = Array.from(keysSet); + + if (headers.length === 0) { + // It's an array of primitives + return ( +
+ + + {dataArray.slice(0, 100).map((item, index) => ( + + + + + ))} + +
{index} handleInsert(`{{${formattedNodeName}[${index}]}}`)} + > + {String(item)} +
+
+ ); + } + + return ( +
+
+ JSON Array + {dataArray.length} items +
+ + + + + {headers.map((header) => ( + + ))} + + + + {dataArray.slice(0, 50).map((item, rowIndex) => ( + + + {headers.map(header => { + const val = item?.[header]; + const displayStr = typeof val === 'object' && val !== null ? JSON.stringify(val) : String(val ?? ''); + return ( + + ); + })} + + ))} + +
# { + const variableText = `{{${formattedNodeName}.map(item => item.${header})}}`; + + e.dataTransfer.setData('text/plain', variableText); + + e.dataTransfer.setData('application/buildflow-variable', JSON.stringify({ + nodeName: formattedNodeName, + path: header, + display: `${formattedNodeName}.${header}` + })) + + e.dataTransfer.effectAllowed = 'copy'; + + }} + key={header} + className={`px-3 py-2 border-r border-b border-[#2a2f3e] font-medium text-gray-400 truncate max-w-[150px] transition-colors + ${activeField && isTested ? "hover:bg-blue-500/20 hover:text-blue-300 cursor-grab active:cursor-grabbing" : "cursor-not-allowed opacity-50"} + `} + onClick={() => { + if (activeField && isTested) { + handleInsert(`{{${formattedNodeName}.map(item => item.${header})}}`); + } + }} + title={ + !isTested + ? "Test node first to map data" + : activeField + ? `Insert entire column as array: {{${formattedNodeName}.map(item => item.${header})}}` + : "Select a field first" + } + > + {header} +
{rowIndex} { + if (activeField && isTested) { + handleInsert(`{{${formattedNodeName}[${rowIndex}].${header}}}`); + } + }} + title={ + !isTested + ? "Test node first to map data" + : activeField + ? `Insert {{${formattedNodeName}[${rowIndex}].${header}}}` + : "Select a field first" + } + > + {displayStr} +
+
+ ); }; if (enrichedNodes.length === 0) { return ( -
-

No previous nodes available.

-

Add a trigger or action before this node to use its output data.

+
+
+ +
+

No Data Available

+

+ Add a trigger or action before this node to map variables. +

); } return ( -
-
- ๐Ÿ“ฆ Available Variables +
+ + {/* Panel Header */} +
+

+ + Data Mapping +

+ {!activeField ? ( +
+
โ„น๏ธ
+
Select an input field in the configuration panel on the right before inserting data.
+
+ ) : ( +
+ + Ready to map data. Select a variable or cell below. +
+ )}
- - {!activeField && ( -
- Click on a field first, then click a variable to insert it. -
- )} - {enrichedNodes.map((node) => { - const isTested = testedOutputs[node.nodeId]?.success; - - return ( -
- {/* Node Header */} -
-
- - {node.nodeName} -
- {isTested && ( - - โœ“ Tested - - )} -
- - {/* No variables message */} - {node.variables.length === 0 && ( -

- Test this node to discover available variables -

- )} - - {/* Variable Pills */} -
- {node.variables.map((variable) => ( - - ))} -
+
+
+ {node.icon ? ( + {node.nodeName} + ) : ( +
+ +
+ )} + {isTested && ( + + )} +
+
+

+ {node.nodeName} +

+

+ {isTested ? 'Data available' : 'Using sample schema'} +

+
+
- {/* Render nested children if any */} - {node.variables - .filter(v => v.children && v.children.length > 0) - .map(variable => ( -
-
โ”” {variable.name} fields:
-
- {variable.children?.map(child => ( +
+ {onTestNode && !node.nodeName.toLowerCase().includes('webhook') && ( - ))} + )} +
+ {isExpanded ? ( + + ) : ( + + )} +
- ))} -
- ); - })} -
+ + {/* Accordion Body */} + {isExpanded && ( +
+ {hasData ? ( + <> + {/* Try matching Spreadsheet Pattern */} + {testOutput.data.rows && Array.isArray(testOutput.data.rows) && testOutput.data.rows.length > 0 && Array.isArray(testOutput.data.rows[0]) ? ( +
+ {renderSpreadsheetTable(node.nodeName, testOutput.data)} +
+ ) : Array.isArray(testOutput.data) && testOutput.data.length > 0 && Array.isArray(testOutput.data[0]) ? ( +
+ {renderSpreadsheetTable(node.nodeName, testOutput.data)} +
+ ) : + /* Try matching Standard Array pattern */ + Array.isArray(testOutput.data) ? ( +
+ {renderArrayTable(node.nodeName, testOutput.data, isTested)} +
+ ) : ( + /* Fallback to Tree Table if it's an object or string */ + renderVariableTable(node, isTested) + )} + + ) : ( + renderVariableTable(node, isTested) + )} +
+ )} +
+ ); + })} +
+
); } diff --git a/apps/web/app/workflows/[id]/components/ConfigModal.tsx b/apps/web/app/workflows/[id]/components/ConfigModal.tsx index e7b1ec2..aa8b980 100644 --- a/apps/web/app/workflows/[id]/components/ConfigModal.tsx +++ b/apps/web/app/workflows/[id]/components/ConfigModal.tsx @@ -9,15 +9,16 @@ import { useCredentials } from "@/app/hooks/useCredential"; import { api } from "@/app/lib/api"; import { PreviousNodeOutput, VariableDefinition } from "@/app/lib/types/node.types"; import { VariablePanel } from "@/app/components/ui/variable-panel"; -import { - setNodeOutput, - setNodeLoading, - selectNodeOutput, +import { + setNodeOutput, + setNodeLoading, + selectNodeOutput, selectNodeLoading, selectAllOutputs, - NodeTestOutput + NodeTestOutput } from "@/store/slices/nodeOutputSlice"; import { workflowActions } from "@/store/slices/workflowSlice"; +import { TestPanel } from "@/app/components/ui/TestPanel"; interface ConfigModalProps { isOpen: boolean; @@ -39,49 +40,54 @@ export default function ConfigModal({ const [loading, setLoading] = useState(false); const [activeField, setActiveField] = useState(null); const [testResult, setTestResult] = useState(null); - + + // Reset test result when switching to a different node + useEffect(() => { + setTestResult(null); + }, [selectedNode?.id]); + const dispatch = useAppDispatch(); const userId = useAppSelector((state) => state.user.userId) as string; const reduxWorkflow = useAppSelector((state) => state.workflow.data); - + // Get all tested outputs from Redux (for variable resolution) const allTestedOutputs = useAppSelector(selectAllOutputs); - + // Get test output from Redux for this node - const nodeTestOutput = useAppSelector((state) => + const nodeTestOutput = useAppSelector((state) => selectedNode ? selectNodeOutput(state, selectedNode.id) : undefined ); - const isTestingNode = useAppSelector((state) => + const isTestingNode = useAppSelector((state) => selectedNode ? selectNodeLoading(state, selectedNode.id) : false ); const fetchOptionsMap: Record Promise> = { - "google.getDocuments" : ({credentialId}) => api.google.getDocuments(credentialId), - "google.getSheets" : ({spreadsheetId, credentialId}) => api.google.getSheets(spreadsheetId, credentialId) + "google.getDocuments": ({ credentialId }) => api.google.getDocuments(credentialId), + "google.getSheets": ({ spreadsheetId, credentialId }) => api.google.getSheets(spreadsheetId, credentialId) } const dispatchConfig = (newConfig: Record) => { if (!selectedNode) return; const isTrigger = reduxWorkflow.trigger?.TriggerId === selectedNode.id; if (isTrigger) { - dispatch(workflowActions.updateTriggerConfig({ config: newConfig })); + dispatch(workflowActions.updateTriggerConfig({ config: newConfig })); } else { - dispatch(workflowActions.updateNodeConfig({ nodeId: selectedNode.id, config: newConfig })); + dispatch(workflowActions.updateNodeConfig({ nodeId: selectedNode.id, config: newConfig })); } -}; - + }; + // Build interpolation context from all previously tested nodes const buildTestContext = (): InterpolationContext => { const context: InterpolationContext = {}; console.log('[buildTestContext] All tested outputs:', allTestedOutputs); - + for (const [nodeId, testOutput] of Object.entries(allTestedOutputs)) { console.log(`[buildTestContext] Processing node ${nodeId}:`, { nodeName: testOutput.nodeName, success: testOutput.success, hasData: !!testOutput.data }); - + if (testOutput.success && testOutput.data) { // Normalize node name: "Google Sheet" -> "google_sheet" const normalizedName = testOutput.nodeName.toLowerCase().replace(/\s+/g, '_'); @@ -89,7 +95,7 @@ export default function ConfigModal({ context[normalizedName] = testOutput.data; } } - + console.log('[buildTestContext] Final context:', context); return context; }; @@ -97,40 +103,40 @@ export default function ConfigModal({ // Test the current node and store output in Redux const handleTestNode = async () => { if (!selectedNode) return; - + console.log('[ConfigModal] Testing node:', selectedNode.id, selectedNode.name); dispatch(setNodeLoading({ nodeId: selectedNode.id, loading: true })); - + try { // Build context from previously tested nodes for variable resolution const interpolationContext = buildTestContext(); console.log('[ConfigModal] Interpolation context:', interpolationContext); - + // Resolve any {{variable}} in the config before testing const resolvedConfig = resolveConfigVariables(config, interpolationContext); console.log('[ConfigModal] Original config:', config); console.log('[ConfigModal] Resolved config:', resolvedConfig); - + // Check if any variables couldn't be resolved const unresolvedVars = Object.entries(resolvedConfig) .filter(([_, value]) => typeof value === 'string' && value.includes('{{')) .map(([key, value]) => `${key}: ${value}`); - + if (unresolvedVars.length > 0) { toast.warning(`Some variables couldn't be resolved. Test the previous nodes first.\n${unresolvedVars.join('\n')}`); } - + const response = await api.execute.node(selectedNode.id, resolvedConfig); console.log('[ConfigModal] API response (already extracted output):', response); - + // api.execute.node already returns res.data.data.output directly const outputData = response; console.log('[ConfigModal] Output data for extraction:', outputData); - + // Extract variables from the output for the variable panel const extractedVariables = extractVariablesFromOutput(outputData); console.log('[ConfigModal] Extracted variables:', extractedVariables); - + // Convert to VariableDefinition format const variables: VariableDefinition[] = extractedVariables.map(v => ({ name: v.name, @@ -138,7 +144,7 @@ export default function ConfigModal({ type: v.type as any, sampleValue: v.sampleValue })); - + // Store in Redux const testOutput: NodeTestOutput = { nodeId: selectedNode.id, @@ -149,13 +155,13 @@ export default function ConfigModal({ testedAt: Date.now(), success: true }; - + dispatch(setNodeOutput(testOutput)); setTestResult(outputData); toast.success("Node test successful!"); } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || "Test failed"; - + dispatch(setNodeOutput({ nodeId: selectedNode.id, nodeName: selectedNode.name || 'Node', @@ -166,16 +172,96 @@ export default function ConfigModal({ success: false, error: errorMessage })); - + toast.error(`Test failed: ${errorMessage}`); } }; - const handleVariableInsert = (variableSyntax: string)=> { - if(!activeField) return; + // Test any previous node on the fly from the Variable Panel + const handleTestPreviousNode = async (nodeId: string) => { + console.log('[ConfigModal] Testing previous node:', nodeId); + dispatch(setNodeLoading({ nodeId, loading: true })); + + try { + // Find the previous node config + let targetNodeConfig: any = null; + let targetNodeType = ""; + let targetNodeName = ""; + + const isTriggerNode = reduxWorkflow.trigger?.TriggerId === nodeId; + if (isTriggerNode) { + targetNodeConfig = reduxWorkflow.trigger?.Config || {}; + targetNodeType = reduxWorkflow.trigger?.type || "webhook"; + targetNodeName = reduxWorkflow.trigger?.name || "Webhook"; + } else { + const foundNode = reduxWorkflow.nodes.find(n => n.NodeId === nodeId); + if (foundNode) { + targetNodeConfig = foundNode.Config || {}; + targetNodeType = foundNode.type || ""; + targetNodeName = foundNode.name || foundNode.name || 'Node'; + } + } + + if (!targetNodeConfig) { + throw new Error("Node configuration not found"); + } + + // Build context from previously tested nodes for variable resolution + const interpolationContext = buildTestContext(); + + // Resolve any {{variable}} in the config before testing + const resolvedConfig = resolveConfigVariables(targetNodeConfig, interpolationContext); + + const response = await api.execute.node(nodeId, resolvedConfig); + const outputData = response; + + // Extract variables from the output for the variable panel + const extractedVariables = extractVariablesFromOutput(outputData); + + // Convert to VariableDefinition format + const variables: VariableDefinition[] = extractedVariables.map(v => ({ + name: v.name, + path: v.path, + type: v.type as any, + sampleValue: v.sampleValue + })); + + // Store in Redux + const testOutput: NodeTestOutput = { + nodeId, + nodeName: targetNodeName, + nodeType: targetNodeType, + data: outputData, + variables, + testedAt: Date.now(), + success: true + }; + + dispatch(setNodeOutput(testOutput)); + toast.success(`${targetNodeName} test successful!`); + } catch (error: any) { + const errorMessage = error.response?.data?.message || error.message || "Test failed"; + + dispatch(setNodeOutput({ + nodeId, + nodeName: 'Node', + nodeType: '', + data: null, + variables: [], + testedAt: Date.now(), + success: false, + error: errorMessage + })); + + toast.error(`Test failed for previous node: ${errorMessage}`); + } + }; + + const handleVariableInsert = (variableSyntax: string) => { + if (!activeField) return; const currentValue = config[activeField] || ""; - const newConfig = {...config, [activeField]: currentValue + variableSyntax}; + const newConfig = { ...config, [activeField]: currentValue + variableSyntax }; setConfig(newConfig) dispatchConfig(newConfig) } @@ -190,8 +276,8 @@ export default function ConfigModal({ console.log(config, "from handle field fun - 2") console.log({ ...config, [fieldName]: value }, "what we're setting") // Find fields that depend on this field - const dependentFields = nodeConfig.fields.filter((f:any) => f.dependsOn === fieldName); - + const dependentFields = nodeConfig.fields.filter((f: any) => f.dependsOn === fieldName); + for (const depField of dependentFields) { const fetchFn = depField.fetchOptions ? fetchOptionsMap[depField.fetchOptions] : undefined; console.log(fetchFn, "fecth FN") @@ -223,14 +309,14 @@ export default function ConfigModal({ 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]){ + if (nodeConfig?.fields) { + for (const field of nodeConfig.fields) { + if (field.fetchOptions && field.dependsOn && loadedConfig[field.dependsOn]) { const fetchFn = fetchOptionsMap[field.fetchOptions]; - if(fetchFn){ + if (fetchFn) { fetchFn(loadedConfig) - .then((option: any[])=> setDynamicOptions(prev => ({...prev, [field.name]: option}))) - .catch(()=> {}) + .then((option: any[]) => setDynamicOptions(prev => ({ ...prev, [field.name]: option }))) + .catch(() => { }) } } } @@ -258,21 +344,21 @@ export default function ConfigModal({ if (field.type === "dropdown" && field.name === "credentialId") { // Use the values from useCredentials: credentials and authUrl return ( -
-