mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(frontend): implement agent outputs viewer and improve UI/UX in new builder (#11421)
This PR introduces several improvements to the new builder experience: **1. Agent Outputs Feature** ✨ - Implemented a new `AgentOutputs` component that displays execution outputs from OUTPUT blocks - Added a slide-out sheet UI to view agent outputs with proper formatting - Integrated with existing output renderers from the library view - Shows output block names, descriptions, and rendered values - Added beta badge to indicate feature is still experimental **2. UI/UX Improvements** 🎨 - Fixed graph loading spinner color from violet to neutral zinc for better consistency - Adjusted node shadow styling for better visual hierarchy (reduced shadow when not selected) - Fixed credential field button spacing to prevent layout overflow - Improved array editor widget delete button positioning - Added proper link handling for integration redirects (opens in new tab) - Fixed object editor to handle null values gracefully **3. Performance & State Management** 🚀 - Fixed race condition in run input dialog by awaiting execution before closing - Added proper history initialization after graph loads - Added `outputSchema` to graph store for tracking output blocks - Fixed search bar to maintain query state properly - Added automatic fit view on graph load for better initial viewport **4. Build Actions Bar** 🔧 - Reduced padding for more compact appearance - Enabled/disabled Agent Outputs button based on presence of output blocks - Removed loading icon from manual run button when not executing ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Created and executed an agent with OUTPUT blocks to verify outputs display correctly - [x] Tested output viewer with different data types (text, JSON, images, etc.) - [x] Verified credential field layouts don't overflow in constrained spaces - [x] Tested array editor delete functionality and button positioning - [x] Confirmed graph loads with proper fit view and history initialization - [x] Tested run input dialog closes only after execution starts - [x] Verified integration links open in new tabs - [x] Tested object editor with null values
This commit is contained in:
@@ -9,7 +9,7 @@ export const BuilderActions = memo(() => {
|
||||
flowID: parseAsString,
|
||||
});
|
||||
return (
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4 rounded-full bg-white p-2 px-4 shadow-lg">
|
||||
<div className="absolute bottom-4 left-[50%] z-[100] flex -translate-x-1/2 items-center gap-4 rounded-full bg-white p-2 px-2 shadow-lg">
|
||||
<AgentOutputs flowID={flowID} />
|
||||
<RunGraph flowID={flowID} />
|
||||
<ScheduleGraph flowID={flowID} />
|
||||
|
||||
@@ -4,25 +4,138 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/__legacy__/ui/sheet";
|
||||
import { BuilderActionButton } from "../BuilderActionButton";
|
||||
import { BookOpenIcon } from "@phosphor-icons/react";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { BlockUIType } from "@/app/(platform)/build/components/types";
|
||||
import { ScrollArea } from "@/components/__legacy__/ui/scroll-area";
|
||||
import { Label } from "@/components/__legacy__/ui/label";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
globalRegistry,
|
||||
OutputItem,
|
||||
OutputActions,
|
||||
} from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/OutputRenderers";
|
||||
|
||||
export const AgentOutputs = ({ flowID }: { flowID: string | null }) => {
|
||||
const hasOutputs = useGraphStore(useShallow((state) => state.hasOutputs));
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
|
||||
const outputs = useMemo(() => {
|
||||
const outputNodes = nodes.filter(
|
||||
(node) => node.data.uiType === BlockUIType.OUTPUT,
|
||||
);
|
||||
|
||||
return outputNodes
|
||||
.map((node) => {
|
||||
const executionResult = node.data.nodeExecutionResult;
|
||||
const outputData = executionResult?.output_data?.output;
|
||||
|
||||
const renderer = globalRegistry.getRenderer(outputData);
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
name: node.data.hardcodedValues?.name || "Output",
|
||||
description:
|
||||
node.data.hardcodedValues?.description || "Output from the agent",
|
||||
},
|
||||
value: outputData ?? "No output yet",
|
||||
renderer,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
output,
|
||||
): output is typeof output & {
|
||||
renderer: NonNullable<typeof output.renderer>;
|
||||
} => output.renderer !== null,
|
||||
);
|
||||
}, [nodes]);
|
||||
|
||||
const actionItems = useMemo(() => {
|
||||
return outputs.map((output) => ({
|
||||
value: output.value,
|
||||
metadata: {},
|
||||
renderer: output.renderer,
|
||||
}));
|
||||
}, [outputs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{/* Todo: Implement Agent Outputs */}
|
||||
<BuilderActionButton disabled={!flowID}>
|
||||
<BookOpenIcon className="size-6" />
|
||||
</BuilderActionButton>
|
||||
<SheetTrigger asChild>
|
||||
<BuilderActionButton disabled={!flowID || !hasOutputs()}>
|
||||
<BookOpenIcon className="size-6" />
|
||||
</BuilderActionButton>
|
||||
</SheetTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Agent Outputs</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
<SheetContent className="flex h-full w-full flex-col overflow-hidden sm:max-w-[600px]">
|
||||
<SheetHeader className="px-2 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<SheetTitle className="text-xl">Run Outputs</SheetTitle>
|
||||
<SheetDescription className="mt-1 text-sm text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="rounded-md bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
Beta
|
||||
</span>
|
||||
<span>This feature is in beta and may contain bugs</span>
|
||||
</span>
|
||||
</SheetDescription>
|
||||
</div>
|
||||
{outputs.length > 0 && <OutputActions items={actionItems} />}
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className="flex-grow overflow-y-auto px-2 py-2">
|
||||
<ScrollArea className="h-full overflow-auto pr-4">
|
||||
<div className="space-y-6">
|
||||
{outputs && outputs.length > 0 ? (
|
||||
outputs.map((output, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-base font-semibold">
|
||||
{output.metadata.name || "Unnamed Output"}
|
||||
</Label>
|
||||
{output.metadata.description && (
|
||||
<Label className="mt-1 block text-sm text-gray-600">
|
||||
{output.metadata.description}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<OutputItem
|
||||
value={output.value}
|
||||
metadata={{}}
|
||||
renderer={output.renderer}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">
|
||||
<p>No output blocks available.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -105,7 +105,9 @@ export const RunInputDialog = ({
|
||||
onClick={handleManualRun}
|
||||
loading={isExecutingGraph}
|
||||
>
|
||||
<PlayIcon className="size-5 transition-transform group-hover:scale-110" />
|
||||
{!isExecutingGraph && (
|
||||
<PlayIcon className="size-5 transition-transform group-hover:scale-110" />
|
||||
)}
|
||||
<span className="font-semibold">Manual Run</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -78,13 +78,13 @@ export const useRunInputDialog = ({
|
||||
return dynamicUiSchema;
|
||||
}, [credentialsSchema]);
|
||||
|
||||
const handleManualRun = () => {
|
||||
setIsOpen(false);
|
||||
executeGraph({
|
||||
const handleManualRun = async () => {
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: { inputs: inputValues, credentials_inputs: credentialValues },
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (inputValues: Record<string, any>) => {
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
# FlowEditor Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The FlowEditor is the core visual graph builder component of the AutoGPT Platform. It allows users to create, edit, and execute workflows by connecting nodes (blocks) together in a visual canvas powered by React Flow (XYFlow).
|
||||
|
||||
---
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Flow Component │
|
||||
│ (Main container coordinating all sub-systems) │
|
||||
└───────────────┬──────────────────┬──────────────────────────┘
|
||||
│ │
|
||||
┌─────────▼────────┐ ┌─────▼──────────┐
|
||||
│ State Stores │ │ React Flow │
|
||||
│ (Zustand) │ │ Canvas │
|
||||
└────────┬─────────┘ └────────────────┘
|
||||
│
|
||||
┌──────────┼──────────┬──────────┐
|
||||
│ │ │ │
|
||||
┌───▼───┐ ┌──▼───┐ ┌───▼────┐ ┌─▼────────┐
|
||||
│ Node │ │ Edge │ │ Graph │ │ Control │
|
||||
│ Store │ │ Store│ │ Store │ │ Panel │
|
||||
└───────┘ └──────┘ └────────┘ └──────────┘
|
||||
│ │
|
||||
│ │
|
||||
┌───▼──────────▼────────────────────────────────────┐
|
||||
│ Custom Nodes & Edges │
|
||||
│ (Visual components rendered on canvas) │
|
||||
└───────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components Breakdown
|
||||
|
||||
### 1. **Flow Component** (`Flow/Flow.tsx`)
|
||||
|
||||
The main orchestrator component that brings everything together.
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Renders the ReactFlow canvas
|
||||
- Integrates all stores (nodes, edges, graph state)
|
||||
- Handles drag-and-drop for adding blocks
|
||||
- Manages keyboard shortcuts (copy/paste)
|
||||
- Controls lock state (editable vs read-only)
|
||||
|
||||
**Key Features:**
|
||||
|
||||
```tsx
|
||||
<ReactFlow
|
||||
nodes={nodes} // From nodeStore
|
||||
edges={edges} // From edgeStore
|
||||
onNodesChange={...} // Updates nodeStore
|
||||
onEdgesChange={...} // Updates edgeStore
|
||||
onConnect={...} // Creates new connections
|
||||
onDragOver={...} // Enables block drag-drop
|
||||
onDrop={...} // Adds blocks to canvas
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **State Management (Zustand Stores)**
|
||||
|
||||
The FlowEditor uses **4 primary Zustand stores** for state management:
|
||||
|
||||
#### **A. nodeStore** (`stores/nodeStore.ts`)
|
||||
|
||||
Manages all nodes (blocks) on the canvas.
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
nodes: CustomNode[] // All nodes on canvas
|
||||
nodeCounter: number // Auto-increment for IDs
|
||||
nodeAdvancedStates: Record<string, boolean> // Track advanced toggle
|
||||
}
|
||||
```
|
||||
|
||||
**Key Actions:**
|
||||
|
||||
- `addBlock()` - Creates a new block with position calculation
|
||||
- `updateNodeData()` - Updates block's form values
|
||||
- `addNodes()` - Bulk add (used when loading graph)
|
||||
- `updateNodeStatus()` - Updates execution status (running/success/failed)
|
||||
- `updateNodeExecutionResult()` - Stores output data from execution
|
||||
- `getBackendNodes()` - Converts to backend format for saving
|
||||
|
||||
**Flow:**
|
||||
|
||||
1. User drags block from menu → `addBlock()` called
|
||||
2. Block appears with unique ID at calculated position
|
||||
3. User edits form → `updateNodeData()` updates hardcodedValues
|
||||
4. On execution → status updates propagate via `updateNodeStatus()`
|
||||
|
||||
---
|
||||
|
||||
#### **B. edgeStore** (`stores/edgeStore.ts`)
|
||||
|
||||
Manages all connections (links) between nodes.
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
edges: CustomEdge[] // All connections
|
||||
edgeBeads: Record<string, EdgeBead[]> // Animated data flow indicators
|
||||
}
|
||||
```
|
||||
|
||||
**Key Actions:**
|
||||
|
||||
- `addLinks()` - Creates connections between nodes
|
||||
- `onConnect()` - Handles new connection creation
|
||||
- `updateEdgeBeads()` - Shows animated data flow during execution
|
||||
- `getBackendLinks()` - Converts to backend format
|
||||
|
||||
**Connection Logic:**
|
||||
|
||||
```
|
||||
Source Node (output) → Edge → Target Node (input)
|
||||
└─ outputPin │ └─ inputPin
|
||||
│
|
||||
(validated connection)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **C. graphStore** (`stores/graphStore.ts`)
|
||||
|
||||
Manages graph-level metadata and state.
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
isGraphRunning: boolean // Execution status
|
||||
inputSchema: Record<string, any> // Graph-level inputs
|
||||
credentialsInputSchema: Record<...> // Required credentials
|
||||
outputSchema: Record<string, any> // Graph-level outputs
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:**
|
||||
|
||||
- Tracks if graph is currently executing
|
||||
- Stores graph-level input/output schemas (for agent graphs)
|
||||
- Used by BuilderActions to show/hide input/output panels
|
||||
|
||||
---
|
||||
|
||||
#### **D. controlPanelStore**
|
||||
|
||||
Manages UI state for the control panel (block menu, settings).
|
||||
|
||||
**State:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
blockMenuOpen: boolean;
|
||||
selectedBlock: BlockInfo | null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **useFlow Hook** (`Flow/useFlow.ts`)
|
||||
|
||||
The main data-loading and initialization hook.
|
||||
|
||||
**Lifecycle:**
|
||||
|
||||
```
|
||||
1. Component Mounts
|
||||
↓
|
||||
2. Read URL params (flowID, flowVersion, flowExecutionID)
|
||||
↓
|
||||
3. Fetch graph data from API
|
||||
↓
|
||||
4. Fetch block definitions for all blocks in graph
|
||||
↓
|
||||
5. Convert to CustomNodes
|
||||
↓
|
||||
6. Add nodes to nodeStore
|
||||
↓
|
||||
7. Add links to edgeStore
|
||||
↓
|
||||
8. If execution exists → fetch execution details
|
||||
↓
|
||||
9. Update node statuses and results
|
||||
↓
|
||||
10. Initialize history (undo/redo)
|
||||
```
|
||||
|
||||
**Key Responsibilities:**
|
||||
|
||||
- **Data Fetching**: Loads graph, blocks, and execution data
|
||||
- **Data Transformation**: Converts backend models to frontend CustomNodes
|
||||
- **State Initialization**: Populates stores with loaded data
|
||||
- **Drag & Drop**: Handles block drag-drop from menu
|
||||
- **Cleanup**: Resets stores on unmount
|
||||
|
||||
**Important Effects:**
|
||||
|
||||
```typescript
|
||||
// Load nodes when data is ready
|
||||
useEffect(() => {
|
||||
if (customNodes.length > 0) {
|
||||
addNodes(customNodes);
|
||||
}
|
||||
}, [customNodes]);
|
||||
|
||||
// Update node execution status in real-time
|
||||
useEffect(() => {
|
||||
executionDetails.node_executions.forEach((nodeExecution) => {
|
||||
updateNodeStatus(nodeExecution.node_id, nodeExecution.status);
|
||||
updateNodeExecutionResult(nodeExecution.node_id, nodeExecution);
|
||||
});
|
||||
}, [executionDetails]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **Custom Nodes** (`nodes/CustomNode/`)
|
||||
|
||||
Visual representation of blocks on the canvas.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
CustomNode
|
||||
├── NodeContainer (selection, context menu, positioning)
|
||||
├── NodeHeader (title, icon, badges)
|
||||
├── FormCreator (input fields using FormRenderer)
|
||||
├── NodeAdvancedToggle (show/hide advanced fields)
|
||||
├── OutputHandler (output connection points)
|
||||
└── NodeDataRenderer (execution results display)
|
||||
```
|
||||
|
||||
**Node Data Structure:**
|
||||
|
||||
```typescript
|
||||
type CustomNodeData = {
|
||||
hardcodedValues: Record<string, any>; // User input values
|
||||
title: string; // Display name
|
||||
description: string; // Help text
|
||||
inputSchema: RJSFSchema; // Input form schema
|
||||
outputSchema: RJSFSchema; // Output schema
|
||||
uiType: BlockUIType; // UI variant (STANDARD, INPUT, OUTPUT, etc.)
|
||||
block_id: string; // Backend block ID
|
||||
status?: AgentExecutionStatus; // Execution state
|
||||
nodeExecutionResult?: NodeExecutionResult; // Output data
|
||||
costs: BlockCost[]; // Cost information
|
||||
categories: BlockInfoCategoriesItem[]; // Categorization
|
||||
};
|
||||
```
|
||||
|
||||
**Special Node Types:**
|
||||
|
||||
- `BlockUIType.NOTE` - Sticky note (no execution)
|
||||
- `BlockUIType.INPUT` - Graph input (no left handles)
|
||||
- `BlockUIType.OUTPUT` - Graph output (no right handles)
|
||||
- `BlockUIType.WEBHOOK` - Webhook trigger
|
||||
- `BlockUIType.AGENT` - Sub-agent execution
|
||||
|
||||
---
|
||||
|
||||
### 5. **Custom Edges** (`edges/CustomEdge.tsx`)
|
||||
|
||||
Visual connections between nodes with animated data flow.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Animated Beads**: Show data flowing during execution
|
||||
- **Type-aware Styling**: Different colors for different data types
|
||||
- **Validation**: Prevents invalid connections
|
||||
- **Deletion**: Click to remove connection
|
||||
|
||||
**Bead Animation System:**
|
||||
|
||||
```
|
||||
Node Execution Complete
|
||||
↓
|
||||
EdgeStore.updateEdgeBeads() called
|
||||
↓
|
||||
Beads created with output data
|
||||
↓
|
||||
CSS animation moves beads along edge path
|
||||
↓
|
||||
Beads removed after animation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. **Handlers (Connection Points)** (`handlers/NodeHandle.tsx`)
|
||||
|
||||
The connection points on nodes where edges attach.
|
||||
|
||||
**Handle ID Format:**
|
||||
|
||||
```typescript
|
||||
// Input handle: input-{propertyName}
|
||||
"input-text_content";
|
||||
|
||||
// Output handle: output-{propertyName}
|
||||
"output-result";
|
||||
```
|
||||
|
||||
**Connection Validation:**
|
||||
|
||||
- Type compatibility checking
|
||||
- Prevents cycles
|
||||
- Single input connection enforcement
|
||||
- Multiple output connections allowed
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: Adding a Block
|
||||
|
||||
```
|
||||
1. User drags block from BlockMenu
|
||||
↓
|
||||
2. onDragOver handler validates drop
|
||||
↓
|
||||
3. onDrop handler called
|
||||
↓
|
||||
4. Parse block data from dataTransfer
|
||||
↓
|
||||
5. Calculate position: screenToFlowPosition()
|
||||
↓
|
||||
6. nodeStore.addBlock(blockData, {}, position)
|
||||
↓
|
||||
7. New CustomNode created with:
|
||||
- Unique ID (nodeCounter++)
|
||||
- Initial position
|
||||
- Empty hardcodedValues
|
||||
- Block schema
|
||||
↓
|
||||
8. Node added to nodes array
|
||||
↓
|
||||
9. React Flow renders CustomNode component
|
||||
↓
|
||||
10. FormCreator renders input form
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: Connecting Nodes
|
||||
|
||||
```
|
||||
1. User drags from source handle to target handle
|
||||
↓
|
||||
2. React Flow calls onConnect()
|
||||
↓
|
||||
3. useCustomEdge hook processes:
|
||||
- Validate connection (type compatibility)
|
||||
- Generate edge ID
|
||||
- Check for cycles
|
||||
↓
|
||||
4. edgeStore.addEdge() creates CustomEdge
|
||||
↓
|
||||
5. Edge rendered on canvas
|
||||
↓
|
||||
6. Target node's input becomes "connected"
|
||||
↓
|
||||
7. FormRenderer hides input field (shows handle only)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: Graph Execution
|
||||
|
||||
```
|
||||
1. User clicks "Run" in BuilderActions
|
||||
↓
|
||||
2. useSaveGraph hook saves current state
|
||||
↓
|
||||
3. API call: POST /execute
|
||||
↓
|
||||
4. Backend queues execution
|
||||
↓
|
||||
5. useFlowRealtime subscribes to WebSocket
|
||||
↓
|
||||
6. Execution updates stream in:
|
||||
- Node status changes (QUEUED → RUNNING → COMPLETED)
|
||||
- Node results
|
||||
↓
|
||||
7. useFlow updates:
|
||||
- nodeStore.updateNodeStatus()
|
||||
- nodeStore.updateNodeExecutionResult()
|
||||
- edgeStore.updateEdgeBeads() (animate data flow)
|
||||
↓
|
||||
8. UI reflects changes:
|
||||
- NodeExecutionBadge shows status
|
||||
- OutputHandler displays results
|
||||
- Edges animate with beads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: Saving a Graph
|
||||
|
||||
```
|
||||
1. User edits form in CustomNode
|
||||
↓
|
||||
2. FormCreator calls handleChange()
|
||||
↓
|
||||
3. nodeStore.updateNodeData(nodeId, { hardcodedValues })
|
||||
↓
|
||||
4. historyStore.pushState() (for undo/redo)
|
||||
↓
|
||||
5. User clicks "Save"
|
||||
↓
|
||||
6. useSaveGraph hook:
|
||||
- nodeStore.getBackendNodes() → convert to backend format
|
||||
- edgeStore.getBackendLinks() → convert links
|
||||
↓
|
||||
7. API call: PUT /graph/:id
|
||||
↓
|
||||
8. Backend persists changes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Utilities and Helpers
|
||||
|
||||
### **Position Calculation** (`components/helper.ts`)
|
||||
|
||||
```typescript
|
||||
findFreePosition(existingNodes, width, margin);
|
||||
// Finds empty space on canvas to place new block
|
||||
// Uses grid-based collision detection
|
||||
```
|
||||
|
||||
### **Node Conversion** (`components/helper.ts`)
|
||||
|
||||
```typescript
|
||||
convertBlockInfoIntoCustomNodeData(blockInfo, hardcodedValues);
|
||||
// Converts backend BlockInfo → CustomNodeData
|
||||
|
||||
convertNodesPlusBlockInfoIntoCustomNodes(node, blockInfo);
|
||||
// Merges backend Node + BlockInfo → CustomNode (for loading)
|
||||
```
|
||||
|
||||
### **Handle ID Generation** (`handlers/helpers.ts`)
|
||||
|
||||
```typescript
|
||||
generateHandleId(fieldId);
|
||||
// input-{fieldId} or output-{fieldId}
|
||||
// Used to uniquely identify connection points
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### **Copy/Paste** (`Flow/useCopyPaste.ts`)
|
||||
|
||||
- Duplicates selected nodes with offset positioning
|
||||
- Preserves internal connections
|
||||
- Does not copy external connections
|
||||
|
||||
### **Undo/Redo** (`stores/historyStore.ts`)
|
||||
|
||||
- Tracks state snapshots (nodes + edges)
|
||||
- Maintains history stack
|
||||
- Triggered on significant changes (add/remove/move)
|
||||
|
||||
### **Realtime Updates** (`Flow/useFlowRealtime.ts`)
|
||||
|
||||
- WebSocket connection for live execution updates
|
||||
- Subscribes to execution events
|
||||
- Updates node status and results in real-time
|
||||
|
||||
### **Advanced Fields Toggle**
|
||||
|
||||
- Each node tracks `showAdvanced` state
|
||||
- Fields with `advanced: true` hidden by default
|
||||
- Toggle button in node UI
|
||||
- Connected fields always visible
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### **With Backend API**
|
||||
|
||||
```
|
||||
GET /v1/graphs/:id → Load graph
|
||||
GET /v2/blocks → Get block definitions
|
||||
GET /v1/executions/:id → Get execution details
|
||||
PUT /v1/graphs/:id → Save graph
|
||||
POST /v1/graphs/:id/execute → Run graph
|
||||
WebSocket /ws → Real-time updates
|
||||
```
|
||||
|
||||
### **With FormRenderer** (See ARCHITECTURE_INPUT_RENDERER.md)
|
||||
|
||||
```
|
||||
CustomNode → FormCreator → FormRenderer
|
||||
↓
|
||||
(RJSF-based form)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Memoization**: React.memo on CustomNode to prevent unnecessary re-renders
|
||||
2. **Shallow Selectors**: useShallow() with Zustand to limit re-renders
|
||||
3. **Lazy Loading**: Blocks fetched only when needed
|
||||
4. **Debounced Saves**: Form changes debounced before triggering history
|
||||
5. **Virtual Scrolling**: React Flow handles large graphs efficiently
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### **Adding a New Block Type**
|
||||
|
||||
1. Define `BlockUIType` enum value
|
||||
2. Create backend block with `uiType` field
|
||||
3. Add conditional rendering in CustomNode if needed
|
||||
4. Update handle visibility logic if required
|
||||
|
||||
### **Adding a New Field Type**
|
||||
|
||||
1. Create custom field in input-renderer/fields
|
||||
2. Register in fields/index.ts
|
||||
3. Use in block's inputSchema
|
||||
|
||||
### **Debugging Tips**
|
||||
|
||||
- Check browser DevTools → React Flow state
|
||||
- Inspect Zustand stores: `useNodeStore.getState()`
|
||||
- Look for console errors in edge validation
|
||||
- Check WebSocket connection for realtime issues
|
||||
|
||||
---
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
**Issue**: Nodes not appearing after load
|
||||
|
||||
- **Check**: `customNodes` computed correctly in useFlow
|
||||
- **Check**: `addNodes()` called after data fetched
|
||||
|
||||
**Issue**: Form not updating node data
|
||||
|
||||
- **Check**: `handleChange` in FormCreator wired correctly
|
||||
- **Check**: `updateNodeData` called with correct nodeId
|
||||
|
||||
**Issue**: Edges not connecting
|
||||
|
||||
- **Check**: Handle IDs match between source and target
|
||||
- **Check**: Type compatibility validation
|
||||
- **Check**: No cycles created
|
||||
|
||||
**Issue**: Execution status not updating
|
||||
|
||||
- **Check**: WebSocket connection active
|
||||
- **Check**: `flowExecutionID` in URL
|
||||
- **Check**: `updateNodeStatus` called in useFlow effect
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The FlowEditor is a sophisticated visual workflow builder that:
|
||||
|
||||
1. Uses **React Flow** for canvas rendering
|
||||
2. Manages state with **Zustand stores** (nodes, edges, graph, control)
|
||||
3. Loads data via **useFlow hook** from backend API
|
||||
4. Renders blocks as **CustomNodes** with dynamic forms
|
||||
5. Connects blocks via **CustomEdges** with validation
|
||||
6. Executes graphs with **real-time status updates**
|
||||
7. Saves changes back to backend in structured format
|
||||
|
||||
The architecture prioritizes:
|
||||
|
||||
- **Separation of concerns** (stores, hooks, components)
|
||||
- **Type safety** (TypeScript throughout)
|
||||
- **Performance** (memoization, shallow selectors)
|
||||
- **Developer experience** (clear data flow, utilities)
|
||||
@@ -27,7 +27,7 @@ export const GraphLoadingBox = ({
|
||||
<div className="absolute left-[50%] top-[50%] z-[99] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="flex flex-col items-center gap-4 rounded-xlarge border border-gray-200 bg-white p-8 shadow-lg dark:border-gray-700 dark:bg-slate-800">
|
||||
<div className="relative h-12 w-12">
|
||||
<div className="absolute inset-0 animate-spin rounded-full border-4 border-violet-200 border-t-violet-500 dark:border-gray-700 dark:border-t-blue-400"></div>
|
||||
<div className="absolute inset-0 animate-spin rounded-full border-4 border-zinc-100 border-t-zinc-400 dark:border-gray-700 dark:border-t-blue-400"></div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{isSaving && <Text variant="h4">Saving Graph</Text>}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useGraphStore } from "../../../stores/graphStore";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { useControlPanelStore } from "../../../stores/controlPanelStore";
|
||||
import { useHistoryStore } from "../../../stores/historyStore";
|
||||
|
||||
export const useFlow = () => {
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
@@ -36,7 +37,7 @@ export const useFlow = () => {
|
||||
const updateEdgeBeads = useEdgeStore(
|
||||
useShallow((state) => state.updateEdgeBeads),
|
||||
);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { screenToFlowPosition, fitView } = useReactFlow();
|
||||
const addBlock = useNodeStore(useShallow((state) => state.addBlock));
|
||||
const setBlockMenuOpen = useControlPanelStore(
|
||||
useShallow((state) => state.setBlockMenuOpen),
|
||||
@@ -104,6 +105,7 @@ export const useFlow = () => {
|
||||
setGraphSchemas(
|
||||
graph.input_schema as Record<string, any> | null,
|
||||
graph.credentials_input_schema as Record<string, any> | null,
|
||||
graph.output_schema as Record<string, any> | null,
|
||||
);
|
||||
}
|
||||
}, [graph]);
|
||||
@@ -131,7 +133,7 @@ export const useFlow = () => {
|
||||
executionDetails?.status === AgentExecutionStatus.QUEUED;
|
||||
|
||||
setIsGraphRunning(isRunning);
|
||||
}, [executionDetails?.status]);
|
||||
}, [executionDetails?.status, customNodes]);
|
||||
|
||||
// update node execution status in nodes
|
||||
useEffect(() => {
|
||||
@@ -144,7 +146,7 @@ export const useFlow = () => {
|
||||
updateNodeStatus(nodeExecution.node_id, nodeExecution.status);
|
||||
});
|
||||
}
|
||||
}, [executionDetails, updateNodeStatus]);
|
||||
}, [executionDetails, updateNodeStatus, customNodes]);
|
||||
|
||||
// update node execution results in nodes, also update edge beads
|
||||
useEffect(() => {
|
||||
@@ -158,7 +160,21 @@ export const useFlow = () => {
|
||||
updateEdgeBeads(nodeExecution.node_id, nodeExecution);
|
||||
});
|
||||
}
|
||||
}, [executionDetails, updateNodeExecutionResult, updateEdgeBeads]);
|
||||
}, [
|
||||
executionDetails,
|
||||
updateNodeExecutionResult,
|
||||
updateEdgeBeads,
|
||||
customNodes,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customNodes.length > 0 && graph?.links) {
|
||||
const timer = setTimeout(() => {
|
||||
useHistoryStore.getState().initializeHistory();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [customNodes, graph?.links]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -170,6 +186,10 @@ export const useFlow = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fitView({ padding: 0.2, duration: 800, maxZoom: 2 });
|
||||
}, [fitView]);
|
||||
|
||||
// Drag and drop block from block menu
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -19,8 +19,8 @@ export const NodeContainer = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-12 max-w-[370px] rounded-xlarge shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
|
||||
selected && "shadow-2xl ring-2 ring-slate-200",
|
||||
"z-12 max-w-[370px] rounded-xlarge ring-1 ring-slate-200/60",
|
||||
selected && "shadow-lg ring-2 ring-slate-200",
|
||||
status && nodeStyleBasedOnStatus[status],
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -7,7 +7,8 @@ const SEARCH_DEBOUNCE_MS = 300;
|
||||
export const useBlockMenuSearchBar = () => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [localQuery, setLocalQuery] = useState("");
|
||||
const { setSearchQuery, setSearchId, searchId } = useBlockMenuStore();
|
||||
const { setSearchQuery, setSearchId, searchId, searchQuery } =
|
||||
useBlockMenuStore();
|
||||
|
||||
const searchIdRef = useRef(searchId);
|
||||
useEffect(() => {
|
||||
@@ -39,6 +40,10 @@ export const useBlockMenuSearchBar = () => {
|
||||
debouncedSetSearchQuery.cancel();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocalQuery(searchQuery);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleClear,
|
||||
inputRef,
|
||||
|
||||
@@ -142,6 +142,7 @@ export const useSaveGraph = ({
|
||||
setGraphSchemas(
|
||||
graphData.input_schema,
|
||||
graphData.credentials_input_schema,
|
||||
graphData.output_schema,
|
||||
);
|
||||
} else {
|
||||
const data: Graph = {
|
||||
@@ -156,6 +157,7 @@ export const useSaveGraph = ({
|
||||
setGraphSchemas(
|
||||
graphData.input_schema,
|
||||
graphData.credentials_input_schema,
|
||||
graphData.output_schema,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,13 +6,17 @@ interface GraphStore {
|
||||
|
||||
inputSchema: Record<string, any> | null;
|
||||
credentialsInputSchema: Record<string, any> | null;
|
||||
outputSchema: Record<string, any> | null;
|
||||
|
||||
setGraphSchemas: (
|
||||
inputSchema: Record<string, any> | null,
|
||||
credentialsInputSchema: Record<string, any> | null,
|
||||
outputSchema: Record<string, any> | null,
|
||||
) => void;
|
||||
|
||||
hasInputs: () => boolean;
|
||||
hasCredentials: () => boolean;
|
||||
hasOutputs: () => boolean;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -20,11 +24,17 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
|
||||
isGraphRunning: false,
|
||||
inputSchema: null,
|
||||
credentialsInputSchema: null,
|
||||
outputSchema: null,
|
||||
|
||||
setIsGraphRunning: (isGraphRunning: boolean) => set({ isGraphRunning }),
|
||||
|
||||
setGraphSchemas: (inputSchema, credentialsInputSchema) =>
|
||||
set({ inputSchema, credentialsInputSchema }),
|
||||
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
|
||||
set({ inputSchema, credentialsInputSchema, outputSchema }),
|
||||
|
||||
hasOutputs: () => {
|
||||
const { outputSchema } = get();
|
||||
return Object.keys(outputSchema?.properties ?? {}).length > 0;
|
||||
},
|
||||
|
||||
hasInputs: () => {
|
||||
const { inputSchema } = get();
|
||||
|
||||
@@ -16,6 +16,7 @@ type HistoryStore = {
|
||||
future: HistoryState[];
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
initializeHistory: () => void;
|
||||
canUndo: () => boolean;
|
||||
canRedo: () => boolean;
|
||||
pushState: (state: HistoryState) => void;
|
||||
@@ -42,6 +43,16 @@ export const useHistoryStore = create<HistoryStore>((set, get) => ({
|
||||
}));
|
||||
},
|
||||
|
||||
initializeHistory: () => {
|
||||
const currentNodes = useNodeStore.getState().nodes;
|
||||
const currentEdges = useEdgeStore.getState().edges;
|
||||
|
||||
set({
|
||||
past: [{ nodes: currentNodes, edges: currentEdges }],
|
||||
future: [],
|
||||
});
|
||||
},
|
||||
|
||||
undo: () => {
|
||||
const { past, future } = get();
|
||||
if (past.length <= 1) return;
|
||||
|
||||
@@ -0,0 +1,938 @@
|
||||
# Input-Renderer Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Input-Renderer is a **JSON Schema-based form generation system** built on top of **React JSON Schema Form (RJSF)**. It dynamically creates form inputs for block nodes in the FlowEditor based on JSON schemas defined in the backend.
|
||||
|
||||
This system allows blocks to define their input requirements declaratively, and the frontend automatically generates appropriate UI components.
|
||||
|
||||
---
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ FormRenderer │
|
||||
│ (Entry point, wraps RJSF Form) │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────▼─────────┐
|
||||
│ RJSF Core │
|
||||
│ <Form /> │
|
||||
└───────┬───────────┘
|
||||
│
|
||||
┌───────────┼───────────┬──────────────┐
|
||||
│ │ │ │
|
||||
┌────▼────┐ ┌───▼────┐ ┌────▼─────┐ ┌────▼────┐
|
||||
│ Fields │ │Templates│ │ Widgets │ │ Schemas │
|
||||
└─────────┘ └─────────┘ └──────────┘ └─────────┘
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
Handles Wrapper Actual JSON Schema
|
||||
complex layouts input (from backend)
|
||||
types & labels components
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is RJSF (React JSON Schema Form)?
|
||||
|
||||
**RJSF** is a library that generates React forms from JSON Schema definitions. It follows a specific hierarchy to render forms:
|
||||
|
||||
### **RJSF Rendering Flow:**
|
||||
|
||||
```
|
||||
1. JSON Schema (defines data structure)
|
||||
↓
|
||||
2. Schema Field (decides which Field component to use)
|
||||
↓
|
||||
3. Field Component (handles specific type logic)
|
||||
↓
|
||||
4. Field Template (wraps field with label, description)
|
||||
↓
|
||||
5. Widget (actual input element - TextInput, Select, etc.)
|
||||
```
|
||||
|
||||
### **Example Flow:**
|
||||
|
||||
```json
|
||||
// JSON Schema
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Becomes:**
|
||||
|
||||
```
|
||||
SchemaField (detects "string" type)
|
||||
↓
|
||||
StringField (default RJSF field)
|
||||
↓
|
||||
FieldTemplate (adds label "Name")
|
||||
↓
|
||||
TextWidget (renders <input type="text" />)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components of Input-Renderer
|
||||
|
||||
### 1. **FormRenderer** (`FormRenderer.tsx`)
|
||||
|
||||
The main entry point that wraps RJSF `<Form />` component.
|
||||
|
||||
```typescript
|
||||
export const FormRenderer = ({
|
||||
jsonSchema, // JSON Schema from backend
|
||||
handleChange, // Callback when form changes
|
||||
uiSchema, // UI customization
|
||||
initialValues, // Pre-filled values
|
||||
formContext, // Extra context (nodeId, uiType, etc.)
|
||||
}: FormRendererProps) => {
|
||||
const preprocessedSchema = preprocessInputSchema(jsonSchema);
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={preprocessedSchema} // Modified schema
|
||||
validator={customValidator} // Custom validation logic
|
||||
fields={fields} // Custom field components
|
||||
templates={templates} // Custom layout templates
|
||||
widgets={widgets} // Custom input widgets
|
||||
formContext={formContext} // Pass context down
|
||||
onChange={handleChange} // Form change handler
|
||||
uiSchema={uiSchema} // UI customization
|
||||
formData={initialValues} // Initial values
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Key Props:**
|
||||
|
||||
- **`fields`** - Custom components for complex types (anyOf, credentials, objects)
|
||||
- **`templates`** - Layout wrappers (FieldTemplate, ArrayFieldTemplate)
|
||||
- **`widgets`** - Actual input components (TextInput, Select, FileWidget)
|
||||
- **`formContext`** - Shared data (nodeId, showHandles, size)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Schema Pre-Processing** (`utils/input-schema-pre-processor.ts`)
|
||||
|
||||
Before rendering, schemas are transformed to ensure RJSF compatibility.
|
||||
|
||||
**Purpose:**
|
||||
|
||||
- Add missing `type` fields (prevents RJSF errors)
|
||||
- Recursively process nested objects and arrays
|
||||
- Normalize inconsistent schemas from backend
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Backend schema (missing type)
|
||||
{
|
||||
"properties": {
|
||||
"value": {} // No type defined!
|
||||
}
|
||||
}
|
||||
|
||||
// After pre-processing
|
||||
{
|
||||
"properties": {
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "number" },
|
||||
{ "type": "boolean" },
|
||||
// ... all possible types
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why?** RJSF requires explicit types. Without this, it would crash or render incorrectly.
|
||||
|
||||
---
|
||||
|
||||
## The Three Pillars: Fields, Templates, Widgets
|
||||
|
||||
### **A. Fields** (`fields/`)
|
||||
|
||||
Fields handle **complex type logic** that goes beyond simple inputs.
|
||||
|
||||
**Registered Fields:**
|
||||
|
||||
```typescript
|
||||
export const fields: RegistryFieldsType = {
|
||||
AnyOfField: AnyOfField, // Handles anyOf/oneOf
|
||||
credentials: CredentialsField, // OAuth/API key handling
|
||||
ObjectField: ObjectField, // Free-form objects
|
||||
};
|
||||
```
|
||||
|
||||
#### **1. AnyOfField** (`fields/AnyOfField/AnyOfField.tsx`)
|
||||
|
||||
Handles schemas with multiple possible types (union types).
|
||||
|
||||
**When Used:**
|
||||
|
||||
```json
|
||||
{
|
||||
"anyOf": [{ "type": "string" }, { "type": "number" }, { "type": "boolean" }]
|
||||
}
|
||||
```
|
||||
|
||||
**Rendering:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Parameter Name (string) ▼ │ ← Type selector dropdown
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input] │ ← Widget for selected type
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Type selector dropdown
|
||||
- Nullable types (with toggle switch)
|
||||
- Recursive rendering (can contain arrays, objects)
|
||||
- Connection-aware (hides input when connected)
|
||||
|
||||
**Special Case: Nullable Types**
|
||||
|
||||
```json
|
||||
{
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }]
|
||||
}
|
||||
```
|
||||
|
||||
**Renders as:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Parameter Name (string | null) [✓] │ ← Toggle switch
|
||||
├─────────────────────────────────────┤
|
||||
│ [Text Input] (only if enabled) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **2. CredentialsField** (`fields/CredentialField/CredentialField.tsx`)
|
||||
|
||||
Handles authentication credentials (OAuth, API Keys, Passwords).
|
||||
|
||||
**When Used:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"credentials": {
|
||||
"provider": "google",
|
||||
"scopes": ["email", "profile"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
|
||||
```
|
||||
1. Renders SelectCredential dropdown
|
||||
↓
|
||||
2. User selects existing credential OR clicks "Add New"
|
||||
↓
|
||||
3. Modal opens (OAuthModal/APIKeyModal/PasswordModal)
|
||||
↓
|
||||
4. User authorizes/enters credentials
|
||||
↓
|
||||
5. Credential saved to backend
|
||||
↓
|
||||
6. Dropdown shows selected credential
|
||||
```
|
||||
|
||||
**Credential Types:**
|
||||
|
||||
- **OAuth** - 3rd party authorization (Google, GitHub, etc.)
|
||||
- **API Key** - Simple key-based auth
|
||||
- **Password** - Username/password pairs
|
||||
|
||||
---
|
||||
|
||||
#### **3. ObjectField** (`fields/ObjectField.tsx`)
|
||||
|
||||
Handles free-form objects (key-value pairs).
|
||||
|
||||
**When Used:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": true // Free-form
|
||||
}
|
||||
```
|
||||
|
||||
vs
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" } // Fixed schema
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- **Fixed schema** → Uses default RJSF rendering
|
||||
- **Free-form** → Uses ObjectEditorWidget (JSON editor)
|
||||
|
||||
---
|
||||
|
||||
### **B. Templates** (`templates/`)
|
||||
|
||||
Templates control **layout and wrapping** of fields.
|
||||
|
||||
#### **1. FieldTemplate** (`templates/FieldTemplate.tsx`)
|
||||
|
||||
Wraps every field with label, type indicator, and connection handle.
|
||||
|
||||
**Rendering Structure:**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ ○ Label (type) ⓘ │ ← Handle + Label + Type + Info icon
|
||||
├────────────────────────────────────────┤
|
||||
│ [Actual Input Widget] │ ← The input itself
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Shows/hides input based on connection status
|
||||
- Renders connection handle (NodeHandle)
|
||||
- Displays type information
|
||||
- Shows tooltip with description
|
||||
- Handles "advanced" field visibility
|
||||
- Formats credential field labels
|
||||
|
||||
**Key Logic:**
|
||||
|
||||
```typescript
|
||||
// Hide input if connected
|
||||
{(isAnyOf || !isConnected) && (
|
||||
<div>{children}</div>
|
||||
)}
|
||||
|
||||
// Show handle for most fields
|
||||
{shouldShowHandle && (
|
||||
<NodeHandle handleId={handleId} isConnected={isConnected} />
|
||||
)}
|
||||
```
|
||||
|
||||
**Context-Aware Behavior:**
|
||||
|
||||
- Inside `AnyOfField` → No handle (parent handles it)
|
||||
- Credential field → Special label formatting
|
||||
- Array item → Uses parent handle
|
||||
- INPUT/OUTPUT/WEBHOOK blocks → Different handle positioning
|
||||
|
||||
---
|
||||
|
||||
#### **2. ArrayFieldTemplate** (`templates/ArrayFieldTemplate.tsx`)
|
||||
|
||||
Wraps array fields to use custom ArrayEditorWidget.
|
||||
|
||||
**Simple Wrapper:**
|
||||
|
||||
```typescript
|
||||
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
const { items, canAdd, onAddClick, nodeId } = props;
|
||||
|
||||
return (
|
||||
<ArrayEditorWidget
|
||||
items={items}
|
||||
nodeId={nodeId}
|
||||
canAdd={canAdd}
|
||||
onAddClick={onAddClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **C. Widgets** (`widgets/`)
|
||||
|
||||
Widgets are **actual input components** - the final rendered HTML elements.
|
||||
|
||||
**Registered Widgets:**
|
||||
|
||||
```typescript
|
||||
export const widgets: RegistryWidgetsType = {
|
||||
TextWidget: TextInputWidget, // <input type="text" />
|
||||
SelectWidget: SelectWidget, // <select> dropdown
|
||||
CheckboxWidget: SwitchWidget, // <Switch> toggle
|
||||
FileWidget: FileWidget, // File upload
|
||||
DateWidget: DateInputWidget, // Date picker
|
||||
TimeWidget: TimeInputWidget, // Time picker
|
||||
DateTimeWidget: DateTimeInputWidget, // Combined date+time
|
||||
};
|
||||
```
|
||||
|
||||
#### **Widget Selection Logic (RJSF)**
|
||||
|
||||
RJSF automatically picks the right widget based on schema:
|
||||
|
||||
```json
|
||||
{ "type": "string" } → TextWidget
|
||||
{ "type": "string", "enum": [...] } → SelectWidget
|
||||
{ "type": "boolean" } → CheckboxWidget
|
||||
{ "type": "string", "format": "date" } → DateWidget
|
||||
{ "type": "string", "format": "time" } → TimeWidget
|
||||
```
|
||||
|
||||
#### **Special Widgets:**
|
||||
|
||||
**1. ArrayEditorWidget** (`widgets/ArrayEditorWidget/`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ○ Item 1 [Text Input] [X Remove] │
|
||||
│ ○ Item 2 [Text Input] [X Remove] │
|
||||
│ [+ Add Item] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Each array item gets its own connection handle
|
||||
- Remove button per item
|
||||
- Add button at bottom
|
||||
- Context provider for handle management
|
||||
|
||||
**ArrayEditorContext:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
isArrayItem: true,
|
||||
arrayFieldHandleId: "input-items-0", // Unique per item
|
||||
isConnected: false
|
||||
}
|
||||
```
|
||||
|
||||
**2. ObjectEditorWidget** (`widgets/ObjectEditorWidget/`)
|
||||
|
||||
- JSON editor for free-form objects
|
||||
- Key-value pair management
|
||||
- Used by ObjectField for `additionalProperties: true`
|
||||
|
||||
---
|
||||
|
||||
## The Complete Rendering Flow
|
||||
|
||||
### **Example: Rendering a Text Input**
|
||||
|
||||
```json
|
||||
// Backend Schema
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message",
|
||||
"description": "Enter your message"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step-by-Step:**
|
||||
|
||||
```
|
||||
1. FormRenderer receives schema
|
||||
↓
|
||||
2. preprocessInputSchema() normalizes it
|
||||
↓
|
||||
3. RJSF <Form /> starts rendering
|
||||
↓
|
||||
4. SchemaField detects "string" type
|
||||
↓
|
||||
5. Uses default StringField
|
||||
↓
|
||||
6. FieldTemplate wraps it:
|
||||
- Adds NodeHandle (connection point)
|
||||
- Adds label "Message (string)"
|
||||
- Adds info icon with description
|
||||
↓
|
||||
7. TextWidget renders <input />
|
||||
↓
|
||||
8. User types "Hello"
|
||||
↓
|
||||
9. onChange callback fires
|
||||
↓
|
||||
10. FormCreator updates nodeStore.updateNodeData()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Example: Rendering AnyOf Field**
|
||||
|
||||
```json
|
||||
// Backend Schema
|
||||
{
|
||||
"anyOf": [{ "type": "string" }, { "type": "number" }],
|
||||
"title": "Value"
|
||||
}
|
||||
```
|
||||
|
||||
**Rendering Flow:**
|
||||
|
||||
```
|
||||
1. RJSF detects "anyOf"
|
||||
↓
|
||||
2. Uses AnyOfField (custom field)
|
||||
↓
|
||||
3. AnyOfField renders:
|
||||
┌─────────────────────────────────┐
|
||||
│ ○ Value (string) ▼ │ ← Self-managed handle & selector
|
||||
├─────────────────────────────────┤
|
||||
│ [Text Input] │ ← Recursively renders SchemaField
|
||||
└─────────────────────────────────┘
|
||||
↓
|
||||
4. User changes type to "number"
|
||||
↓
|
||||
5. AnyOfField re-renders with NumberWidget
|
||||
↓
|
||||
6. User enters "42"
|
||||
↓
|
||||
7. onChange({ type: "number", value: 42 })
|
||||
```
|
||||
|
||||
**Key Point:** AnyOfField **does NOT use FieldTemplate** for itself. It manages its own handle and label to avoid duplication. But it **recursively calls SchemaField** for the selected type, which may use FieldTemplate.
|
||||
|
||||
---
|
||||
|
||||
### **Example: Rendering Array Field**
|
||||
|
||||
```json
|
||||
// Backend Schema
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Tags"
|
||||
}
|
||||
```
|
||||
|
||||
**Rendering Flow:**
|
||||
|
||||
```
|
||||
1. RJSF detects "array" type
|
||||
↓
|
||||
2. Uses ArrayFieldTemplate
|
||||
↓
|
||||
3. ArrayFieldTemplate passes to ArrayEditorWidget
|
||||
↓
|
||||
4. ArrayEditorWidget renders:
|
||||
┌─────────────────────────────────┐
|
||||
│ ○ Tag 1 [Text Input] [X] │ ← Each item wrapped in context
|
||||
│ ○ Tag 2 [Text Input] [X] │
|
||||
│ [+ Add Item] │
|
||||
└─────────────────────────────────┘
|
||||
↓
|
||||
5. Each item wrapped in ArrayEditorContext
|
||||
↓
|
||||
6. FieldTemplate reads context:
|
||||
- isArrayItem = true
|
||||
- Uses arrayFieldHandleId instead of own handle
|
||||
↓
|
||||
7. TextWidget renders for each item
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hierarchy: What Comes First?
|
||||
|
||||
This is the **order of execution** from schema to rendered input:
|
||||
|
||||
```
|
||||
1. JSON Schema (from backend)
|
||||
↓
|
||||
2. preprocessInputSchema() (normalization)
|
||||
↓
|
||||
3. RJSF <Form /> (library entry point)
|
||||
↓
|
||||
4. SchemaField (RJSF internal - decides which field)
|
||||
↓
|
||||
5. Field Component (AnyOfField, CredentialsField, or default)
|
||||
↓
|
||||
6. Template (FieldTemplate or ArrayFieldTemplate)
|
||||
↓
|
||||
7. Widget (TextWidget, SelectWidget, etc.)
|
||||
↓
|
||||
8. Actual HTML (<input>, <select>, etc.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts Explained
|
||||
|
||||
### **1. Why Custom Fields?**
|
||||
|
||||
RJSF's default fields don't handle:
|
||||
|
||||
- **AnyOf** - Type selection + dynamic widget switching
|
||||
- **Credentials** - OAuth flows, modal management
|
||||
- **Free-form Objects** - JSON editor instead of fixed fields
|
||||
|
||||
Custom fields fill these gaps.
|
||||
|
||||
---
|
||||
|
||||
### **2. Why Templates?**
|
||||
|
||||
Templates add **FlowEditor-specific UI**:
|
||||
|
||||
- Connection handles (left side dots)
|
||||
- Type indicators
|
||||
- Tooltips
|
||||
- Advanced field hiding
|
||||
- Connection-aware rendering
|
||||
|
||||
Default RJSF templates don't support these features.
|
||||
|
||||
---
|
||||
|
||||
### **3. Why Custom Widgets?**
|
||||
|
||||
Custom widgets provide:
|
||||
|
||||
- Consistent styling with design system
|
||||
- Integration with Zustand stores
|
||||
- Custom behaviors (e.g., FileWidget uploads)
|
||||
- Better UX (e.g., SwitchWidget vs checkbox)
|
||||
|
||||
---
|
||||
|
||||
### **4. FormContext - The Shared State**
|
||||
|
||||
FormContext passes data down the RJSF tree:
|
||||
|
||||
```typescript
|
||||
type FormContextType = {
|
||||
nodeId?: string; // Which node this form belongs to
|
||||
uiType?: BlockUIType; // Block type (INPUT, OUTPUT, etc.)
|
||||
showHandles?: boolean; // Show connection handles?
|
||||
size?: "small" | "large"; // Form size variant
|
||||
};
|
||||
```
|
||||
|
||||
**Why?** RJSF components don't have direct access to React props from parent. FormContext provides a channel.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
// In FieldTemplate
|
||||
const { nodeId, showHandles, size } = formContext;
|
||||
|
||||
// Check if input is connected
|
||||
const isConnected = useEdgeStore().isInputConnected(nodeId, handleId);
|
||||
|
||||
// Hide input if connected
|
||||
{!isConnected && <div>{children}</div>}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **5. Handle Management**
|
||||
|
||||
Connection handles are the **left-side dots** on nodes where edges connect.
|
||||
|
||||
**Handle ID Format:**
|
||||
|
||||
```typescript
|
||||
// Regular field
|
||||
generateHandleId("root_message") → "input-message"
|
||||
|
||||
// Array item
|
||||
generateHandleId("root_tags", ["0"]) → "input-tags-0"
|
||||
generateHandleId("root_tags", ["1"]) → "input-tags-1"
|
||||
|
||||
// Nested field
|
||||
generateHandleId("root_config_api_key") → "input-config-api_key"
|
||||
```
|
||||
|
||||
**Context Provider Pattern (Arrays):**
|
||||
|
||||
```typescript
|
||||
// ArrayEditorWidget wraps each item
|
||||
<ArrayEditorContext.Provider
|
||||
value={{
|
||||
isArrayItem: true,
|
||||
arrayFieldHandleId: "input-tags-0"
|
||||
}}
|
||||
>
|
||||
{element.children} // ← FieldTemplate renders here
|
||||
</ArrayEditorContext.Provider>
|
||||
|
||||
// FieldTemplate reads context
|
||||
const { isArrayItem, arrayFieldHandleId } = useContext(ArrayEditorContext);
|
||||
|
||||
// Use array handle instead of generating own
|
||||
const handleId = isArrayItem ? arrayFieldHandleId : generateHandleId(fieldId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection-Aware Rendering
|
||||
|
||||
One of the most important features: **hiding inputs when connected**.
|
||||
|
||||
**Flow:**
|
||||
|
||||
```
|
||||
1. User connects edge to input handle
|
||||
↓
|
||||
2. edgeStore.addEdge() creates connection
|
||||
↓
|
||||
3. Next render cycle:
|
||||
- FieldTemplate calls isInputConnected(nodeId, handleId)
|
||||
- Returns true
|
||||
↓
|
||||
4. FieldTemplate hides input:
|
||||
{!isConnected && <div>{children}</div>}
|
||||
↓
|
||||
5. Only handle visible (with blue highlight)
|
||||
```
|
||||
|
||||
**Why?** When a value comes from another node's output, manual input is disabled. The connection provides the value.
|
||||
|
||||
**Exception:** AnyOf fields still show type selector when connected (but hide the input).
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### **1. Advanced Field Toggle**
|
||||
|
||||
Some fields marked as `advanced: true` in schema are hidden by default.
|
||||
|
||||
**Logic in FieldTemplate:**
|
||||
|
||||
```typescript
|
||||
const showAdvanced = useNodeStore((state) => state.nodeAdvancedStates[nodeId]);
|
||||
|
||||
if (!showAdvanced && schema.advanced === true && !isConnected) {
|
||||
return null; // Hide field
|
||||
}
|
||||
```
|
||||
|
||||
**UI:** NodeAdvancedToggle button in CustomNode shows/hides these fields.
|
||||
|
||||
---
|
||||
|
||||
### **2. Nullable Type Handling**
|
||||
|
||||
```json
|
||||
{
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }]
|
||||
}
|
||||
```
|
||||
|
||||
**AnyOfField detects this pattern and renders:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Parameter (string | null) [✓] │ ← Switch to enable/disable
|
||||
├─────────────────────────────────────┤
|
||||
│ [Input only if enabled] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**State Management:**
|
||||
|
||||
```typescript
|
||||
const [isEnabled, setIsEnabled] = useState(formData !== null);
|
||||
|
||||
const handleNullableToggle = (checked: boolean) => {
|
||||
setIsEnabled(checked);
|
||||
onChange(checked ? "" : null); // Send null when disabled
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **3. Recursive Schema Rendering**
|
||||
|
||||
AnyOfField, ObjectField, and ArrayField all recursively call `SchemaField`:
|
||||
|
||||
```typescript
|
||||
const SchemaField = registry.fields.SchemaField;
|
||||
|
||||
<SchemaField
|
||||
schema={nestedSchema}
|
||||
formData={formData}
|
||||
onChange={handleChange}
|
||||
// ... propagate all props
|
||||
/>
|
||||
```
|
||||
|
||||
This allows **infinite nesting**: arrays of objects, objects with anyOf fields, etc.
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### **Adding a New Widget**
|
||||
|
||||
1. Create widget component in `widgets/`:
|
||||
|
||||
```typescript
|
||||
export const MyWidget = ({ value, onChange, ...props }: WidgetProps) => {
|
||||
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
|
||||
};
|
||||
```
|
||||
|
||||
2. Register in `widgets/index.ts`:
|
||||
|
||||
```typescript
|
||||
export const widgets: RegistryWidgetsType = {
|
||||
// ...
|
||||
MyCustomWidget: MyWidget,
|
||||
};
|
||||
```
|
||||
|
||||
3. Use in uiSchema or schema format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "string",
|
||||
"format": "my-custom-format" // RJSF maps format → widget
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Adding a New Field**
|
||||
|
||||
1. Create field component in `fields/`:
|
||||
|
||||
```typescript
|
||||
export const MyField = ({ schema, formData, onChange, ...props }: FieldProps) => {
|
||||
// Custom logic here
|
||||
return <div>...</div>;
|
||||
};
|
||||
```
|
||||
|
||||
2. Register in `fields/index.ts`:
|
||||
|
||||
```typescript
|
||||
export const fields: RegistryFieldsType = {
|
||||
// ...
|
||||
MyField: MyField,
|
||||
};
|
||||
```
|
||||
|
||||
3. RJSF uses it based on schema structure (e.g., custom keyword).
|
||||
|
||||
---
|
||||
|
||||
## Integration with FlowEditor
|
||||
|
||||
```
|
||||
CustomNode
|
||||
↓
|
||||
FormCreator
|
||||
↓
|
||||
FormRenderer ← YOU ARE HERE
|
||||
↓
|
||||
RJSF <Form />
|
||||
↓
|
||||
(Fields, Templates, Widgets)
|
||||
↓
|
||||
User Input
|
||||
↓
|
||||
onChange callback
|
||||
↓
|
||||
FormCreator.handleChange()
|
||||
↓
|
||||
nodeStore.updateNodeData(nodeId, { hardcodedValues })
|
||||
↓
|
||||
historyStore.pushState() (undo/redo)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### **Field Not Rendering**
|
||||
|
||||
- Check if `preprocessInputSchema()` is handling it correctly
|
||||
- Verify schema has `type` field
|
||||
- Check RJSF console for validation errors
|
||||
|
||||
### **Widget Wrong Type**
|
||||
|
||||
- Check schema `type` and `format` fields
|
||||
- Verify widget is registered in `widgets/index.ts`
|
||||
- Check if custom field is overriding default behavior
|
||||
|
||||
### **Handle Not Appearing**
|
||||
|
||||
- Check `showHandles` in formContext
|
||||
- Verify not inside `fromAnyOf` context
|
||||
- Check if field is credential or array item
|
||||
|
||||
### **Value Not Saving**
|
||||
|
||||
- Verify `onChange` callback is firing
|
||||
- Check `handleChange` in FormCreator
|
||||
- Look for console errors in `updateNodeData`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Input-Renderer is a sophisticated form system that:
|
||||
|
||||
1. **Uses RJSF** as the foundation for JSON Schema → React forms
|
||||
2. **Extends RJSF** with custom Fields, Templates, and Widgets
|
||||
3. **Integrates** with FlowEditor's connection system
|
||||
4. **Handles** complex types (anyOf, credentials, free-form objects)
|
||||
5. **Provides** connection-aware, type-safe input rendering
|
||||
|
||||
**Key Hierarchy (What Comes First):**
|
||||
|
||||
```
|
||||
JSON Schema
|
||||
→ Pre-processing
|
||||
→ RJSF Form
|
||||
→ SchemaField (RJSF internal)
|
||||
→ Field (AnyOfField, CredentialsField, etc.)
|
||||
→ Template (FieldTemplate, ArrayFieldTemplate)
|
||||
→ Widget (TextWidget, SelectWidget, etc.)
|
||||
→ HTML Element
|
||||
```
|
||||
|
||||
**Mental Model:**
|
||||
|
||||
- **Fields** = Smart logic layers (type selection, OAuth flows)
|
||||
- **Templates** = Layout wrappers (handles, labels, tooltips)
|
||||
- **Widgets** = Actual inputs (text boxes, dropdowns)
|
||||
|
||||
**Integration Point:**
|
||||
|
||||
- FormRenderer receives schema from `node.data.inputSchema`
|
||||
- User edits form → `onChange` → `nodeStore.updateNodeData()`
|
||||
- Values saved as `node.data.hardcodedValues`
|
||||
@@ -131,7 +131,10 @@ export const AnyOfField = ({
|
||||
<div className="mb-0 flex flex-col">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div
|
||||
className={cn("flex items-center gap-1", showHandles && "-ml-2")}
|
||||
className={cn(
|
||||
"ml-1 flex items-center gap-1",
|
||||
showHandles && "-ml-2",
|
||||
)}
|
||||
>
|
||||
{showHandles && (
|
||||
<NodeHandle
|
||||
@@ -143,7 +146,7 @@ export const AnyOfField = ({
|
||||
<Text
|
||||
variant={formContext.size === "small" ? "body" : "body-medium"}
|
||||
>
|
||||
{name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
{schema.title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
</Text>
|
||||
<Text variant="small" className={colorClass}>
|
||||
({displayType} | null)
|
||||
@@ -157,14 +160,18 @@ export const AnyOfField = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{!isConnected && isEnabled && renderInput(nonNull)}</div>
|
||||
<div className="mt-2">
|
||||
{!isConnected && isEnabled && renderInput(nonNull)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-0 flex flex-col">
|
||||
<div className={cn("flex items-center gap-1", showHandles && "-ml-2")}>
|
||||
<div
|
||||
className={cn("ml-1 flex items-center gap-1", showHandles && "-ml-2")}
|
||||
>
|
||||
{showHandles && (
|
||||
<NodeHandle
|
||||
handleId={handleId}
|
||||
@@ -173,7 +180,7 @@ export const AnyOfField = ({
|
||||
/>
|
||||
)}
|
||||
<Text variant={formContext.size === "small" ? "body" : "body-medium"}>
|
||||
{name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
{schema.title || name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
</Text>
|
||||
{!isConnected && (
|
||||
<Select
|
||||
@@ -209,8 +216,9 @@ export const AnyOfField = ({
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isConnected && currentTypeOption && renderInput(currentTypeOption)}
|
||||
<div className="mt-2">
|
||||
{!isConnected && currentTypeOption && renderInput(currentTypeOption)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ export const CredentialsField = (props: FieldProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{supportsApiKey && (
|
||||
<APIKeyCredentialsModal
|
||||
schema={schema as BlockIOCredentialsSubSchema}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
KeyIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import Link from "next/link";
|
||||
import { providerIcons } from "./helpers";
|
||||
|
||||
type SelectCredentialProps = {
|
||||
@@ -77,15 +76,18 @@ export const SelectCredential: React.FC<SelectCredentialProps> = ({
|
||||
size="small"
|
||||
hideLabel
|
||||
/>
|
||||
<Link href={`/profile/integrations`}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 border-zinc-300 p-0"
|
||||
>
|
||||
<ArrowSquareOutIcon className="h-4 w-4 text-zinc-600" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
as="NextLink"
|
||||
href="/profile/integrations"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 border-zinc-300 p-0"
|
||||
>
|
||||
<ArrowSquareOutIcon className="h-4 w-4 text-zinc-600" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -109,7 +109,7 @@ export function APIKeyCredentialsModal({ schema, provider }: Props) {
|
||||
</Dialog>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-auto min-w-0"
|
||||
className="w-fit px-2"
|
||||
size="small"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const OAuthCredentialModal = ({
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="w-fit"
|
||||
className="w-fit px-2"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleOAuthLogin();
|
||||
|
||||
@@ -88,7 +88,7 @@ export function PasswordCredentialsModal({ provider }: Props) {
|
||||
</Dialog>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-fit"
|
||||
className="w-fit px-2"
|
||||
size="small"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
|
||||
@@ -44,7 +44,8 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
|
||||
const { isArrayItem, arrayFieldHandleId } = useContext(ArrayEditorContext);
|
||||
|
||||
const isAnyOf = Array.isArray((schema as any)?.anyOf);
|
||||
const isAnyOf =
|
||||
Array.isArray((schema as any)?.anyOf) && !(schema as any)?.enum;
|
||||
const isOneOf = Array.isArray((schema as any)?.oneOf);
|
||||
const isCredential = isCredentialFieldSchema(schema);
|
||||
const suppressHandle = isAnyOf || isOneOf;
|
||||
@@ -96,7 +97,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
size === "small" ? "w-[350px]" : "w-full",
|
||||
)}
|
||||
>
|
||||
{label && schema.type && (
|
||||
{!isAnyOf && !fromAnyOf && label && (
|
||||
<label htmlFor={fieldId} className="flex items-center gap-1">
|
||||
{shouldShowHandle && (
|
||||
<NodeHandle
|
||||
@@ -105,31 +106,27 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
{!fromAnyOf && (
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn(
|
||||
"line-clamp-1",
|
||||
isCredential && !shouldShowHandle && "ml-3",
|
||||
size == "large" && "ml-0",
|
||||
uiType === BlockUIType.OUTPUT &&
|
||||
fieldId === "root_name" &&
|
||||
"ml-3",
|
||||
uiType === BlockUIType.INPUT && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK_MANUAL && "ml-3",
|
||||
)}
|
||||
>
|
||||
{isCredential && credentialProvider
|
||||
? toDisplayName(credentialProvider) + " credentials"
|
||||
: label}
|
||||
</Text>
|
||||
)}
|
||||
{!fromAnyOf && (
|
||||
<Text variant="small" className={colorClass}>
|
||||
({displayType})
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
variant={formContext.size === "small" ? "body" : "body-medium"}
|
||||
className={cn(
|
||||
"line-clamp-1",
|
||||
isCredential && !shouldShowHandle && "ml-3",
|
||||
size == "large" && "ml-0",
|
||||
uiType === BlockUIType.OUTPUT &&
|
||||
fieldId === "root_name" &&
|
||||
"ml-3",
|
||||
uiType === BlockUIType.INPUT && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK && "ml-3",
|
||||
uiType === BlockUIType.WEBHOOK_MANUAL && "ml-3",
|
||||
)}
|
||||
>
|
||||
{isCredential && credentialProvider
|
||||
? toDisplayName(credentialProvider) + " credentials"
|
||||
: schema.title || label}
|
||||
</Text>
|
||||
<Text variant="small" className={colorClass}>
|
||||
({displayType})
|
||||
</Text>
|
||||
{required && <span style={{ color: "red" }}>*</span>}
|
||||
{description?.props?.description && (
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const ArrayEditorWidget = ({
|
||||
[element.index.toString()],
|
||||
HandleIdType.ARRAY,
|
||||
);
|
||||
const isConnected = isInputConnected(nodeId, fieldId);
|
||||
const isConnected = isInputConnected(nodeId, arrayFieldHandleId);
|
||||
return (
|
||||
<div
|
||||
key={element.key}
|
||||
@@ -62,11 +62,11 @@ export const ArrayEditorWidget = ({
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="relative top-2 min-w-0"
|
||||
className="relative top-1.5 min-w-0 p-2 px-3"
|
||||
size="small"
|
||||
onClick={element.onDropIndexClick(element.index)}
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
(string)
|
||||
</Text>
|
||||
</div>
|
||||
{!isDynamicPropertyConnected && propertyValue !== null && (
|
||||
{!isDynamicPropertyConnected && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
hideLabel={true}
|
||||
@@ -143,7 +143,7 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
label=""
|
||||
id={`value-${idx}`}
|
||||
size="small"
|
||||
value={propertyValue as string}
|
||||
value={propertyValue ?? ""}
|
||||
onChange={(e) => setProperty(key, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
wrapperClassName="mb-0"
|
||||
|
||||
Reference in New Issue
Block a user