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:
Abhimanyu Yadav
2025-11-20 22:36:02 +05:30
committed by GitHub
parent 41dc39b97d
commit e64d3d9b99
22 changed files with 1772 additions and 74 deletions

View File

@@ -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} />

View File

@@ -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>
);
};

View File

@@ -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>
)}

View File

@@ -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>) => {

View File

@@ -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)

View File

@@ -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>}

View File

@@ -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();

View File

@@ -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],
)}
>

View File

@@ -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,

View File

@@ -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,
);
}
},

View File

@@ -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();

View File

@@ -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;

View File

@@ -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`

View File

@@ -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>
);
};

View File

@@ -58,7 +58,7 @@ export const CredentialsField = (props: FieldProps) => {
/>
)}
<div>
<div className="flex flex-wrap gap-2">
{supportsApiKey && (
<APIKeyCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}

View File

@@ -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>
);
};

View File

@@ -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)}
>

View File

@@ -39,7 +39,7 @@ export const OAuthCredentialModal = ({
<Button
type="button"
className="w-fit"
className="w-fit px-2"
size="small"
onClick={() => {
handleOAuthLogin();

View File

@@ -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)}
>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"