mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(frontend): Add dynamic input dialog for agent execution with credential support (#11301)
### Changes 🏗️ This PR enhances the agent execution functionality by introducing a dynamic input dialog that collects both regular inputs and credentials before running agents. <img width="1309" height="826" alt="Screenshot 2025-11-03 at 10 16 38 AM" src="https://github.com/user-attachments/assets/2015da5d-055d-49c5-8e7e-31bd0fe369f4" /> #### ✨ New Features - **Dynamic Input Dialog**: Added a new `RunInputDialog` component that automatically detects when agents require inputs or credentials and prompts users before execution - **Credential Management**: Integrated credential input handling directly into the execution flow, supporting various credential types (API keys, OAuth, passwords) - **Enhanced Run Controls**: Improved the `RunGraph` component with better state management and visual feedback for running/stopping agents - **Form Renderer**: Created a new unified `FormRenderer` component for consistent input rendering across the application #### 🔧 Refactoring - **Input Renderer Migration**: Moved input renderer components from FlowEditor-specific location to a shared components directory for better reusability: - Migrated fields (AnyOfField, CredentialField, ObjectField) - Migrated widgets (ArrayEditor, DateInput, SelectWidget, TextInput, etc.) - Migrated templates (FieldTemplate, ArrayFieldTemplate) - **State Management**: Enhanced `graphStore` with schemas for inputs and credentials, including helper methods to check for their presence - **Component Organization**: Restructured BuilderActions components for better modularity #### 🗑️ Cleanup - Removed outdated FlowEditor documentation files (FORM_CREATOR.md, README.md) - Removed deprecated `RunGraph` and `useRunGraph` implementations from FlowEditor - Consolidated duplicate functionality into new shared components #### 🎨 UI/UX Improvements - Added gradient styling to Run/Stop button for better visual appeal - Improved dialog layout with clear sections for Credentials and Inputs - Enhanced form fields with size variants (small, medium, large) for better responsiveness - Added loading states and proper error handling during execution ### Technical Details - The new system automatically detects input requirements from the graph schema - Credentials are handled separately with special UI treatment based on credential type - The dialog only appears when inputs or credentials are actually required - Execution flow: Save graph → Check for inputs/credentials → Show dialog if needed → Execute with provided values ### 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] Create an agent without inputs and verify it runs directly without dialog - [x] Create an agent with input blocks and verify the dialog appears with correct fields - [x] Create an agent requiring credentials and verify credential selection/creation works - [x] Test agent execution with both inputs and credentials - [x] Verify Stop Agent functionality during execution - [x] Test error handling for invalid inputs or missing credentials - [x] Verify that the dialog closes properly after submission - [x] Test that execution state is properly reflected in the UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { RunGraph } from "./components/RunGraph";
|
||||
import { RunGraph } from "./components/RunGraph/RunGraph";
|
||||
|
||||
export const BuilderActions = () => {
|
||||
return (
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlayIcon } from "lucide-react";
|
||||
import { useRunGraph } from "./useRunGraph";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { StopIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
||||
|
||||
export const RunGraph = () => {
|
||||
const {
|
||||
handleRunGraph,
|
||||
handleStopGraph,
|
||||
isSaving,
|
||||
openRunInputDialog,
|
||||
setOpenRunInputDialog,
|
||||
} = useRunGraph();
|
||||
const isGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.isGraphRunning),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className={cn(
|
||||
"relative min-w-44 border-none bg-gradient-to-r from-purple-500 to-pink-500 text-lg",
|
||||
)}
|
||||
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
|
||||
>
|
||||
{!isGraphRunning && !isSaving ? (
|
||||
<PlayIcon className="mr-1 size-5" />
|
||||
) : (
|
||||
<StopIcon className="mr-1 size-5" />
|
||||
)}
|
||||
{isGraphRunning || isSaving ? "Stop Agent" : "Run Agent"}
|
||||
</Button>
|
||||
<RunInputDialog
|
||||
isOpen={openRunInputDialog}
|
||||
setIsOpen={setOpenRunInputDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
usePostV1ExecuteGraphAgent,
|
||||
usePostV1StopGraphExecution,
|
||||
} from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useNewSaveControl } from "../../../NewControlPanel/NewSaveControl/useNewSaveControl";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { GraphExecutionMeta } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/use-agent-runs";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useRunGraph = () => {
|
||||
const { onSubmit: onSaveGraph, isLoading: isSaving } = useNewSaveControl({
|
||||
showToast: false,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
const hasInputs = useGraphStore(useShallow((state) => state.hasInputs));
|
||||
const hasCredentials = useGraphStore(
|
||||
useShallow((state) => state.hasCredentials),
|
||||
);
|
||||
const [openRunInputDialog, setOpenRunInputDialog] = useState(false);
|
||||
|
||||
const [{ flowID, flowVersion, flowExecutionID }, setQueryStates] =
|
||||
useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
flowExecutionID: parseAsString,
|
||||
});
|
||||
|
||||
const { mutateAsync: executeGraph } = usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const { id } = response.data as GraphExecutionMeta;
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: stopGraph } = usePostV1StopGraphExecution({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setIsGraphRunning(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleRunGraph = async () => {
|
||||
await onSaveGraph(undefined);
|
||||
|
||||
if (hasInputs() || hasCredentials()) {
|
||||
setOpenRunInputDialog(true);
|
||||
} else {
|
||||
setIsGraphRunning(true);
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: { inputs: {}, credentials_inputs: {} },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopGraph = async () => {
|
||||
if (!flowExecutionID) {
|
||||
return;
|
||||
}
|
||||
await stopGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphExecId: flowExecutionID,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
handleRunGraph,
|
||||
handleStopGraph,
|
||||
isSaving,
|
||||
openRunInputDialog,
|
||||
setOpenRunInputDialog,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlayIcon } from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
import { useRunInputDialog } from "./useRunInputDialog";
|
||||
|
||||
export const RunInputDialog = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const hasInputs = useGraphStore((state) => state.hasInputs);
|
||||
const hasCredentials = useGraphStore((state) => state.hasCredentials);
|
||||
const inputSchema = useGraphStore((state) => state.inputSchema);
|
||||
const credentialsSchema = useGraphStore(
|
||||
(state) => state.credentialsInputSchema,
|
||||
);
|
||||
|
||||
const {
|
||||
credentialsUiSchema,
|
||||
handleManualRun,
|
||||
handleInputChange,
|
||||
handleCredentialChange,
|
||||
isExecutingGraph,
|
||||
} = useRunInputDialog({ setIsOpen });
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Run Agent"
|
||||
controlled={{
|
||||
isOpen,
|
||||
set: setIsOpen,
|
||||
}}
|
||||
styling={{ maxWidth: "700px" }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="space-y-6 p-1">
|
||||
{/* Credentials Section */}
|
||||
{hasCredentials() && (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Text variant="h4" className="text-gray-900">
|
||||
Credentials
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<FormRenderer
|
||||
jsonSchema={credentialsSchema as RJSFSchema}
|
||||
handleChange={(v) => handleCredentialChange(v.formData)}
|
||||
uiSchema={credentialsUiSchema}
|
||||
initialValues={{}}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inputs Section */}
|
||||
{hasInputs() && (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Text variant="h4" className="text-gray-900">
|
||||
Inputs
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<FormRenderer
|
||||
jsonSchema={inputSchema as RJSFSchema}
|
||||
handleChange={(v) => handleInputChange(v.formData)}
|
||||
uiSchema={uiSchema}
|
||||
initialValues={{}}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "large",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="group h-fit min-w-0 gap-2 border-none bg-gradient-to-r from-blue-600 to-purple-600 px-8 transition-all"
|
||||
onClick={handleManualRun}
|
||||
loading={isExecutingGraph}
|
||||
>
|
||||
<PlayIcon className="size-5 transition-transform group-hover:scale-110" />
|
||||
<span className="font-semibold">Manual Run</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import {
|
||||
CredentialsMetaInput,
|
||||
GraphExecutionMeta,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
|
||||
import { isCredentialFieldSchema } from "@/components/renderers/input-renderer/fields/CredentialField/helpers";
|
||||
|
||||
export const useRunInputDialog = ({
|
||||
setIsOpen,
|
||||
}: {
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const credentialsSchema = useGraphStore(
|
||||
(state) => state.credentialsInputSchema,
|
||||
);
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>({});
|
||||
const [credentialValues, setCredentialValues] = useState<
|
||||
Record<string, CredentialsMetaInput>
|
||||
>({});
|
||||
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
|
||||
flowExecutionID: parseAsString,
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
});
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: executeGraph, isPending: isExecutingGraph } =
|
||||
usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const { id } = response.data as GraphExecutionMeta;
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
setIsGraphRunning(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// We are rendering the credentials field differently compared to other fields.
|
||||
// In the node, we have the field name as "credential" - so our library catches it and renders it differently.
|
||||
// But here we have a different name, something like `Firecrawl credentials`, so here we are telling the library that this field is a credential field type.
|
||||
|
||||
const credentialsUiSchema = useMemo(() => {
|
||||
const dynamicUiSchema: any = { ...uiSchema };
|
||||
|
||||
if (credentialsSchema?.properties) {
|
||||
Object.keys(credentialsSchema.properties).forEach((fieldName) => {
|
||||
const fieldSchema = credentialsSchema.properties[fieldName];
|
||||
if (isCredentialFieldSchema(fieldSchema)) {
|
||||
dynamicUiSchema[fieldName] = {
|
||||
...dynamicUiSchema[fieldName],
|
||||
"ui:field": "credentials",
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return dynamicUiSchema;
|
||||
}, [credentialsSchema]);
|
||||
|
||||
const handleManualRun = () => {
|
||||
setIsOpen(false);
|
||||
setIsGraphRunning(true);
|
||||
executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: { inputs: inputValues, credentials_inputs: credentialValues },
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (inputValues: Record<string, any>) => {
|
||||
setInputValues(inputValues);
|
||||
};
|
||||
const handleCredentialChange = (
|
||||
credentialValues: Record<string, CredentialsMetaInput>,
|
||||
) => {
|
||||
setCredentialValues(credentialValues);
|
||||
};
|
||||
|
||||
return {
|
||||
credentialsUiSchema,
|
||||
inputValues,
|
||||
credentialValues,
|
||||
isExecutingGraph,
|
||||
handleInputChange,
|
||||
handleCredentialChange,
|
||||
handleManualRun,
|
||||
};
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlayIcon } from "lucide-react";
|
||||
import { useRunGraph } from "./useRunGraph";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { StopIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const RunGraph = () => {
|
||||
const { runGraph, isSaving } = useRunGraph();
|
||||
const isGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.isGraphRunning),
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className={cn(
|
||||
"relative min-w-44 border-none bg-gradient-to-r from-purple-500 to-pink-500 text-lg",
|
||||
)}
|
||||
onClick={() => runGraph()}
|
||||
>
|
||||
{!isGraphRunning && !isSaving ? (
|
||||
<PlayIcon className="mr-1 size-5" />
|
||||
) : (
|
||||
<StopIcon className="mr-1 size-5" />
|
||||
)}
|
||||
{isGraphRunning || isSaving ? "Stop Agent" : "Run Agent"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useNewSaveControl } from "../../../NewControlPanel/NewSaveControl/useNewSaveControl";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { GraphExecutionMeta } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/use-agent-runs";
|
||||
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const useRunGraph = () => {
|
||||
const { onSubmit: onSaveGraph, isLoading: isSaving } = useNewSaveControl({
|
||||
showToast: false,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
const [{ flowID, flowVersion }, setQueryStates] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
flowExecutionID: parseAsString,
|
||||
});
|
||||
|
||||
const { mutateAsync: executeGraph } = usePostV1ExecuteGraphAgent({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
const { id } = response.data as GraphExecutionMeta;
|
||||
setQueryStates({
|
||||
flowExecutionID: id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsGraphRunning(false);
|
||||
|
||||
toast({
|
||||
title: (error.detail as string) ?? "An unexpected error occurred.",
|
||||
description: "An unexpected error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const runGraph = async () => {
|
||||
setIsGraphRunning(true);
|
||||
await onSaveGraph(undefined);
|
||||
|
||||
// Todo : We need to save graph which has inputs and credentials inputs
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: {
|
||||
inputs: {},
|
||||
credentials_inputs: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
runGraph,
|
||||
isSaving,
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import { CustomNode } from "../nodes/CustomNode/CustomNode";
|
||||
import { useCustomEdge } from "../edges/useCustomEdge";
|
||||
import { useFlowRealtime } from "./useFlowRealtime";
|
||||
import { GraphLoadingBox } from "./components/GraphLoadingBox";
|
||||
import { BuilderActions } from "../BuilderActions/BuilderActions";
|
||||
import { BuilderActions } from "../../BuilderActions/BuilderActions";
|
||||
import { RunningBackground } from "./components/RunningBackground";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ export const useFlow = () => {
|
||||
const setIsGraphRunning = useGraphStore(
|
||||
useShallow((state) => state.setIsGraphRunning),
|
||||
);
|
||||
const setGraphSchemas = useGraphStore(
|
||||
useShallow((state) => state.setGraphSchemas),
|
||||
);
|
||||
const [{ flowID, flowVersion, flowExecutionID }] = useQueryStates({
|
||||
flowID: parseAsString,
|
||||
flowVersion: parseAsInteger,
|
||||
@@ -83,6 +86,14 @@ export const useFlow = () => {
|
||||
}, [nodes, blocks]);
|
||||
|
||||
useEffect(() => {
|
||||
// load graph schemas
|
||||
if (graph) {
|
||||
setGraphSchemas(
|
||||
graph.input_schema as Record<string, any> | null,
|
||||
graph.credentials_input_schema as Record<string, any> | null,
|
||||
);
|
||||
}
|
||||
|
||||
// adding nodes
|
||||
if (customNodes.length > 0) {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
@@ -128,6 +139,7 @@ export const useFlow = () => {
|
||||
return () => {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
useEdgeStore.getState().setConnections([]);
|
||||
useGraphStore.getState().reset();
|
||||
setIsGraphRunning(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1,580 +0,0 @@
|
||||
# Form Creator System
|
||||
|
||||
The Form Creator is a dynamic form generation system built on React JSON Schema Form (RJSF) that automatically creates interactive forms based on JSON schemas. It's the core component that powers the input handling in the FlowEditor.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Schema Processing](#schema-processing)
|
||||
- [Widget System](#widget-system)
|
||||
- [Field System](#field-system)
|
||||
- [Template System](#template-system)
|
||||
- [Customization Guide](#customization-guide)
|
||||
- [Advanced Features](#advanced-features)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Form Creator system consists of several interconnected layers:
|
||||
|
||||
```
|
||||
FormCreator
|
||||
├── Schema Preprocessing
|
||||
│ └── input-schema-pre-processor.ts
|
||||
├── Widget System
|
||||
│ ├── TextInputWidget
|
||||
│ ├── SelectWidget
|
||||
│ ├── SwitchWidget
|
||||
│ └── ... (other widgets)
|
||||
├── Field System
|
||||
│ ├── AnyOfField
|
||||
│ ├── ObjectField
|
||||
│ └── CredentialsField
|
||||
├── Template System
|
||||
│ ├── FieldTemplate
|
||||
│ └── ArrayFieldTemplate
|
||||
└── UI Schema
|
||||
└── uiSchema.ts
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. **Schema Input**
|
||||
|
||||
The FormCreator receives a JSON schema that defines the structure of the form:
|
||||
|
||||
```typescript
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
title: "Message",
|
||||
description: "Enter your message",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
title: "Count",
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 2. **Schema Preprocessing**
|
||||
|
||||
The schema is preprocessed to ensure all properties have proper types:
|
||||
|
||||
```typescript
|
||||
// Before preprocessing
|
||||
{
|
||||
"properties": {
|
||||
"name": { "title": "Name" } // No type defined
|
||||
}
|
||||
}
|
||||
|
||||
// After preprocessing
|
||||
// if there is no type - that means it can accept any type
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "number" },
|
||||
{ "type": "boolean" },
|
||||
{ "type": "array", "items": { "type": "string" } },
|
||||
{ "type": "object" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Widget Mapping**
|
||||
|
||||
Schema types are mapped to appropriate input widgets:
|
||||
|
||||
```typescript
|
||||
// Schema type -> Widget mapping
|
||||
"string" -> TextInputWidget
|
||||
"number" -> TextInputWidget (with number type)
|
||||
"boolean" -> SwitchWidget
|
||||
"array" -> ArrayFieldTemplate
|
||||
"object" -> ObjectField
|
||||
"enum" -> SelectWidget
|
||||
```
|
||||
|
||||
### 4. **Form Rendering**
|
||||
|
||||
RJSF renders the form using the mapped widgets and templates:
|
||||
|
||||
```typescript
|
||||
<Form
|
||||
schema={preprocessedSchema}
|
||||
validator={validator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={{ nodeId }}
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
```
|
||||
|
||||
## Schema Processing
|
||||
|
||||
### Input Schema Preprocessor
|
||||
|
||||
The `preprocessInputSchema` function ensures all properties have proper types:
|
||||
|
||||
```typescript
|
||||
export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema {
|
||||
// Recursively processes properties
|
||||
if (processedSchema.properties) {
|
||||
for (const [key, property] of Object.entries(processedSchema.properties)) {
|
||||
// Add type if none exists
|
||||
if (
|
||||
!processedProperty.type &&
|
||||
!processedProperty.anyOf &&
|
||||
!processedProperty.oneOf &&
|
||||
!processedProperty.allOf
|
||||
) {
|
||||
processedProperty.anyOf = [
|
||||
{ type: "string" },
|
||||
{ type: "number" },
|
||||
{ type: "integer" },
|
||||
{ type: "boolean" },
|
||||
{ type: "array", items: { type: "string" } },
|
||||
{ type: "object" },
|
||||
{ type: "null" },
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Type Safety**: Ensures all properties have types
|
||||
2. **Recursive Processing**: Handles nested objects and arrays
|
||||
3. **Array Item Processing**: Processes array item schemas
|
||||
4. **Schema Cleanup**: Removes titles and descriptions from root schema
|
||||
|
||||
## Widget System
|
||||
|
||||
Widgets are the actual input components that users interact with.
|
||||
|
||||
### Available Widgets
|
||||
|
||||
#### TextInputWidget
|
||||
|
||||
Handles text, number, password, and textarea inputs:
|
||||
|
||||
```typescript
|
||||
export const TextInputWidget = (props: WidgetProps) => {
|
||||
const { schema } = props;
|
||||
const mapped = mapJsonSchemaTypeToInputType(schema);
|
||||
|
||||
const inputConfig = {
|
||||
[InputType.TEXT_AREA]: {
|
||||
htmlType: "textarea",
|
||||
placeholder: "Enter text...",
|
||||
handleChange: (v: string) => (v === "" ? undefined : v),
|
||||
},
|
||||
[InputType.PASSWORD]: {
|
||||
htmlType: "password",
|
||||
placeholder: "Enter secret text...",
|
||||
handleChange: (v: string) => (v === "" ? undefined : v),
|
||||
},
|
||||
[InputType.NUMBER]: {
|
||||
htmlType: "number",
|
||||
placeholder: "Enter number value...",
|
||||
handleChange: (v: string) => (v === "" ? undefined : Number(v)),
|
||||
}
|
||||
};
|
||||
|
||||
return <Input {...config} />;
|
||||
};
|
||||
```
|
||||
|
||||
#### SelectWidget
|
||||
|
||||
Handles dropdown and multi-select inputs:
|
||||
|
||||
```typescript
|
||||
export const SelectWidget = (props: WidgetProps) => {
|
||||
const { options, value, onChange, schema } = props;
|
||||
const enumOptions = options.enumOptions || [];
|
||||
const type = mapJsonSchemaTypeToInputType(schema);
|
||||
|
||||
if (type === InputType.MULTI_SELECT) {
|
||||
return <MultiSelector values={value} onValuesChange={onChange} />;
|
||||
}
|
||||
|
||||
return <Select value={value} onValueChange={onChange} options={enumOptions} />;
|
||||
};
|
||||
```
|
||||
|
||||
#### SwitchWidget
|
||||
|
||||
Handles boolean toggles:
|
||||
|
||||
```typescript
|
||||
export function SwitchWidget(props: WidgetProps) {
|
||||
const { value = false, onChange, disabled, readonly } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={(checked) => onChange(checked)}
|
||||
disabled={disabled || readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Widget Registration
|
||||
|
||||
Widgets are registered in the widgets registry:
|
||||
|
||||
```typescript
|
||||
export const widgets: RegistryWidgetsType = {
|
||||
TextWidget: TextInputWidget,
|
||||
SelectWidget: SelectWidget,
|
||||
CheckboxWidget: SwitchWidget,
|
||||
FileWidget: FileWidget,
|
||||
DateWidget: DateInputWidget,
|
||||
TimeWidget: TimeInputWidget,
|
||||
DateTimeWidget: DateTimeInputWidget,
|
||||
};
|
||||
```
|
||||
|
||||
## Field System
|
||||
|
||||
Fields handle complex data structures and provide custom rendering logic.
|
||||
|
||||
### AnyOfField
|
||||
|
||||
Handles union types and nullable fields:
|
||||
|
||||
```typescript
|
||||
export const AnyOfField = ({ schema, formData, onChange, ...props }: FieldProps) => {
|
||||
const { isNullableType, selectedType, handleTypeChange, currentTypeOption } = useAnyOfField(schema, formData, onChange);
|
||||
|
||||
if (isNullableType) {
|
||||
return (
|
||||
<div>
|
||||
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
|
||||
<Switch checked={isEnabled} onCheckedChange={handleNullableToggle} />
|
||||
{isEnabled && renderInput(nonNull)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
|
||||
<Select value={selectedType} onValueChange={handleTypeChange} />
|
||||
{renderInput(currentTypeOption)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### ObjectField
|
||||
|
||||
Handles free-form object editing:
|
||||
|
||||
```typescript
|
||||
export const ObjectField = (props: FieldProps) => {
|
||||
const { schema, formData = {}, onChange, name, idSchema, formContext } = props;
|
||||
|
||||
// Use default field for fixed-schema objects
|
||||
if (idSchema?.$id === "root" || !isFreeForm) {
|
||||
return <DefaultObjectField {...props} />;
|
||||
}
|
||||
|
||||
// Use custom ObjectEditor for free-form objects
|
||||
return (
|
||||
<ObjectEditor
|
||||
id={`${name}-input`}
|
||||
nodeId={nodeId}
|
||||
fieldKey={fieldKey}
|
||||
value={formData}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Field Registration
|
||||
|
||||
Fields are registered in the fields registry:
|
||||
|
||||
```typescript
|
||||
export const fields: RegistryFieldsType = {
|
||||
AnyOfField: AnyOfField,
|
||||
credentials: CredentialsField,
|
||||
ObjectField: ObjectField,
|
||||
};
|
||||
```
|
||||
|
||||
## Template System
|
||||
|
||||
Templates provide custom rendering for form structure elements.
|
||||
|
||||
### FieldTemplate
|
||||
|
||||
Custom field wrapper with connection handles:
|
||||
|
||||
```typescript
|
||||
const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
id, label, required, description, children, schema, formContext, uiSchema
|
||||
}) => {
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId } = formContext;
|
||||
|
||||
const fieldKey = generateHandleId(id);
|
||||
const isConnected = isInputConnected(nodeId, fieldKey);
|
||||
|
||||
return (
|
||||
<div className="mt-4 w-[400px] space-y-1">
|
||||
{label && schema.type && (
|
||||
<label htmlFor={id} className="flex items-center gap-1">
|
||||
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
|
||||
<Text variant="body">{label}</Text>
|
||||
<Text variant="small" className={colorClass}>({displayType})</Text>
|
||||
{required && <span style={{ color: "red" }}>*</span>}
|
||||
</label>
|
||||
)}
|
||||
{!isConnected && <div className="pl-2">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### ArrayFieldTemplate
|
||||
|
||||
Custom array editing interface:
|
||||
|
||||
```typescript
|
||||
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
const { items, canAdd, onAddClick, disabled, readonly, formContext, idSchema } = props;
|
||||
const { nodeId } = formContext;
|
||||
|
||||
return (
|
||||
<ArrayEditor
|
||||
items={items}
|
||||
nodeId={nodeId}
|
||||
canAdd={canAdd}
|
||||
onAddClick={onAddClick}
|
||||
disabled={disabled}
|
||||
readonly={readonly}
|
||||
id={idSchema.$id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Customization Guide
|
||||
|
||||
### Adding a Custom Widget
|
||||
|
||||
1. **Create the Widget Component**:
|
||||
|
||||
```typescript
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
|
||||
export const MyCustomWidget = (props: WidgetProps) => {
|
||||
const { value, onChange, schema, disabled, readonly } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled || readonly}
|
||||
placeholder={schema.placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
2. **Register the Widget**:
|
||||
|
||||
```typescript
|
||||
// In widgets/index.ts
|
||||
export const widgets: RegistryWidgetsType = {
|
||||
// ... existing widgets
|
||||
MyCustomWidget: MyCustomWidget,
|
||||
};
|
||||
```
|
||||
|
||||
3. **Use in Schema**:
|
||||
|
||||
```typescript
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
myField: {
|
||||
type: "string",
|
||||
"ui:widget": "MyCustomWidget",
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Adding a Custom Field
|
||||
|
||||
1. **Create the Field Component**:
|
||||
|
||||
```typescript
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
|
||||
export const MyCustomField = (props: FieldProps) => {
|
||||
const { schema, formData, onChange, name, idSchema, formContext } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Custom field implementation */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
2. **Register the Field**:
|
||||
|
||||
```typescript
|
||||
// In fields/index.ts
|
||||
export const fields: RegistryFieldsType = {
|
||||
// ... existing fields
|
||||
MyCustomField: MyCustomField,
|
||||
};
|
||||
```
|
||||
|
||||
3. **Use in Schema**:
|
||||
|
||||
```typescript
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
myField: {
|
||||
type: "string",
|
||||
"ui:field": "MyCustomField",
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Customizing Templates
|
||||
|
||||
1. **Create Custom Template**:
|
||||
|
||||
```typescript
|
||||
const MyCustomFieldTemplate: React.FC<FieldTemplateProps> = (props) => {
|
||||
return (
|
||||
<div className="my-custom-field">
|
||||
{/* Custom template implementation */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
2. **Register Template**:
|
||||
|
||||
```typescript
|
||||
// In templates/index.ts
|
||||
export const templates = {
|
||||
FieldTemplate: MyCustomFieldTemplate,
|
||||
// ... other templates
|
||||
};
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Connection State Management
|
||||
|
||||
The Form Creator integrates with the edge store to show/hide input fields based on connection state:
|
||||
|
||||
```typescript
|
||||
const FieldTemplate = ({ id, children, formContext }) => {
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId } = formContext;
|
||||
|
||||
const fieldKey = generateHandleId(id);
|
||||
const isConnected = isInputConnected(nodeId, fieldKey);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
|
||||
{!isConnected && children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Advanced Mode
|
||||
|
||||
Fields can be hidden/shown based on advanced mode:
|
||||
|
||||
```typescript
|
||||
const FieldTemplate = ({ schema, formContext }) => {
|
||||
const { nodeId } = formContext;
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[nodeId] || false
|
||||
);
|
||||
|
||||
if (!showAdvanced && schema.advanced === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>{/* field content */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Array Item Context
|
||||
|
||||
Array items have special context for connection handling:
|
||||
|
||||
```typescript
|
||||
const ArrayEditor = ({ items, nodeId }) => {
|
||||
return (
|
||||
<div>
|
||||
{items?.map((element) => {
|
||||
const fieldKey = generateHandleId(id, [element.index.toString()], HandleIdType.ARRAY);
|
||||
const isConnected = isInputConnected(nodeId, fieldKey);
|
||||
|
||||
return (
|
||||
<ArrayEditorContext.Provider
|
||||
value={{ isArrayItem: true, fieldKey, isConnected }}
|
||||
>
|
||||
{element.children}
|
||||
</ArrayEditorContext.Provider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Handle ID Generation
|
||||
|
||||
Handle IDs are generated based on field structure:
|
||||
|
||||
```typescript
|
||||
// Simple field
|
||||
generateHandleId("message"); // "message"
|
||||
|
||||
// Nested field
|
||||
generateHandleId("config", ["api_key"]); // "config.api_key"
|
||||
|
||||
// Array item
|
||||
generateHandleId("items", ["0"]); // "items_$_0"
|
||||
|
||||
// Key-value pair
|
||||
generateHandleId("headers", ["Authorization"]); // "headers_#_Authorization"
|
||||
```
|
||||
@@ -1,159 +0,0 @@
|
||||
# FlowEditor Component
|
||||
|
||||
The FlowEditor is a powerful visual flow builder component built on top of React Flow that allows users to create, connect, and manage nodes in a visual workflow. It provides a comprehensive form system with dynamic input handling, connection management, and advanced features.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Store Management](#store-management)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The FlowEditor follows a modular architecture with clear separation of concerns:
|
||||
|
||||
```
|
||||
FlowEditor/
|
||||
├── Flow.tsx # Main component
|
||||
├── nodes/ # Node-related components
|
||||
│ ├── CustomNode.tsx # Main node component
|
||||
│ ├── FormCreator.tsx # Dynamic form generator
|
||||
│ ├── fields/ # Custom field components
|
||||
│ ├── widgets/ # Custom input widgets
|
||||
│ ├── templates/ # RJSF templates
|
||||
│ └── helpers.ts # Utility functions
|
||||
├── edges/ # Edge-related components
|
||||
│ ├── CustomEdge.tsx # Custom edge component
|
||||
│ ├── useCustomEdge.ts # Edge management hook
|
||||
│ └── helpers.ts # Edge utilities
|
||||
├── handlers/ # Connection handles
|
||||
│ ├── NodeHandle.tsx # Connection handle component
|
||||
│ └── helpers.ts # Handle utilities
|
||||
├── components/ # Shared components
|
||||
│ ├── ArrayEditor/ # Array editing components
|
||||
│ └── ObjectEditor/ # Object editing components
|
||||
└── processors/ # Data processors
|
||||
└── input-schema-pre-processor.ts
|
||||
```
|
||||
|
||||
## Store Management
|
||||
|
||||
The FlowEditor uses Zustand for state management with two main stores:
|
||||
|
||||
### NodeStore (`useNodeStore`)
|
||||
|
||||
Manages all node-related state and operations.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Node CRUD operations
|
||||
- Advanced state management per node
|
||||
- Form data persistence
|
||||
- Node counter for unique IDs
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { useNodeStore } from "../stores/nodeStore";
|
||||
|
||||
// Get nodes
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
|
||||
// Add a new node
|
||||
const addNode = useNodeStore((state) => state.addNode);
|
||||
|
||||
// Update node data
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
|
||||
// Toggle advanced mode
|
||||
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
|
||||
```
|
||||
|
||||
**Store Methods:**
|
||||
|
||||
- `setNodes(nodes)` - Replace all nodes
|
||||
- `addNode(node)` - Add a single node
|
||||
- `addBlock(blockInfo)` - Add node from block info
|
||||
- `updateNodeData(nodeId, data)` - Update node data
|
||||
- `onNodesChange(changes)` - Handle node changes from React Flow
|
||||
- `setShowAdvanced(nodeId, show)` - Toggle advanced mode
|
||||
- `incrementNodeCounter()` - Get next node ID
|
||||
|
||||
### EdgeStore (`useEdgeStore`)
|
||||
|
||||
Manages all connection-related state and operations.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Connection CRUD operations
|
||||
- Connection validation
|
||||
- Backend link conversion
|
||||
- Connection state queries
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { useEdgeStore } from "../stores/edgeStore";
|
||||
|
||||
// Get connections
|
||||
const connections = useEdgeStore((state) => state.connections);
|
||||
|
||||
// Add connection
|
||||
const addConnection = useEdgeStore((state) => state.addConnection);
|
||||
|
||||
// Check if input is connected
|
||||
const isInputConnected = useEdgeStore((state) => state.isInputConnected);
|
||||
```
|
||||
|
||||
**Store Methods:**
|
||||
|
||||
- `setConnections(connections)` - Replace all connections
|
||||
- `addConnection(conn)` - Add a new connection
|
||||
- `removeConnection(edgeId)` - Remove connection by ID
|
||||
- `upsertMany(conns)` - Bulk update connections
|
||||
- `isInputConnected(nodeId, handle)` - Check input connection
|
||||
- `isOutputConnected(nodeId, handle)` - Check output connection
|
||||
- `getNodeConnections(nodeId)` - Get all connections for a node
|
||||
- `getBackendLinks()` - Convert to backend format
|
||||
|
||||
## Form Creator System
|
||||
|
||||
The FormCreator is a dynamic form generator built on React JSON Schema Form (RJSF) that automatically creates forms based on JSON schemas.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Schema Processing**: Input schemas are preprocessed to ensure all properties have types
|
||||
2. **Widget Mapping**: Schema types are mapped to appropriate input widgets
|
||||
3. **Field Rendering**: Custom fields handle complex data structures
|
||||
4. **State Management**: Form data is automatically synced with the node store
|
||||
|
||||
### Key Components
|
||||
|
||||
#### FormCreator
|
||||
|
||||
```typescript
|
||||
<FormCreator
|
||||
jsonSchema={preprocessedSchema}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Custom Widgets
|
||||
|
||||
- `TextInputWidget` - Text, number, password inputs
|
||||
- `SelectWidget` - Dropdown and multi-select
|
||||
- `SwitchWidget` - Boolean toggles
|
||||
- `FileWidget` - File upload
|
||||
- `DateInputWidget` - Date picker
|
||||
- `TimeInputWidget` - Time picker
|
||||
- `DateTimeInputWidget` - DateTime picker
|
||||
|
||||
#### Custom Fields
|
||||
|
||||
- `AnyOfField` - Union type handling
|
||||
- `ObjectField` - Free-form object editing
|
||||
- `CredentialsField` - API credential management
|
||||
|
||||
#### Templates
|
||||
|
||||
- `FieldTemplate` - Custom field wrapper with handles
|
||||
- `ArrayFieldTemplate` - Array editing interface
|
||||
@@ -2,7 +2,7 @@ import { beautifyString, cn } from "@/lib/utils";
|
||||
import { CustomNodeData } from "./CustomNode";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "../../processors/input-schema-pre-processor";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
@@ -57,7 +57,7 @@ export const StandardNodeBlock = ({
|
||||
</div>
|
||||
</div>
|
||||
{/* Input Handles */}
|
||||
<div className="bg-white pb-6 pr-6">
|
||||
<div className="bg-white pr-6">
|
||||
<FormCreator
|
||||
jsonSchema={preprocessInputSchema(data.inputSchema)}
|
||||
nodeId={nodeId}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "../../processors/input-schema-pre-processor";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { CustomNodeData } from "./CustomNode";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import Form from "@rjsf/core";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import React from "react";
|
||||
import { widgets } from "./widgets";
|
||||
import { fields } from "./fields";
|
||||
import { templates } from "./templates";
|
||||
import { uiSchema } from "./uiSchema";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { BlockUIType } from "../../types";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
|
||||
export const FormCreator = React.memo(
|
||||
({
|
||||
@@ -33,16 +29,17 @@ export const FormCreator = React.memo(
|
||||
const initialValues = getHardCodedValues(nodeId);
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={jsonSchema}
|
||||
validator={validator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={{ nodeId: nodeId, uiType: uiType }}
|
||||
onChange={handleChange}
|
||||
<FormRenderer
|
||||
jsonSchema={jsonSchema}
|
||||
handleChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
initialValues={initialValues}
|
||||
formContext={{
|
||||
nodeId: nodeId,
|
||||
uiType: uiType,
|
||||
showHandles: true,
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -3,9 +3,43 @@ import { create } from "zustand";
|
||||
interface GraphStore {
|
||||
isGraphRunning: boolean;
|
||||
setIsGraphRunning: (isGraphRunning: boolean) => void;
|
||||
|
||||
inputSchema: Record<string, any> | null;
|
||||
credentialsInputSchema: Record<string, any> | null;
|
||||
setGraphSchemas: (
|
||||
inputSchema: Record<string, any> | null,
|
||||
credentialsInputSchema: Record<string, any> | null,
|
||||
) => void;
|
||||
|
||||
hasInputs: () => boolean;
|
||||
hasCredentials: () => boolean;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useGraphStore = create<GraphStore>((set) => ({
|
||||
export const useGraphStore = create<GraphStore>((set, get) => ({
|
||||
isGraphRunning: false,
|
||||
inputSchema: null,
|
||||
credentialsInputSchema: null,
|
||||
|
||||
setIsGraphRunning: (isGraphRunning: boolean) => set({ isGraphRunning }),
|
||||
|
||||
setGraphSchemas: (inputSchema, credentialsInputSchema) =>
|
||||
set({ inputSchema, credentialsInputSchema }),
|
||||
|
||||
hasInputs: () => {
|
||||
const { inputSchema } = get();
|
||||
return Object.keys(inputSchema?.properties ?? {}).length > 0;
|
||||
},
|
||||
|
||||
hasCredentials: () => {
|
||||
const { credentialsInputSchema } = get();
|
||||
return Object.keys(credentialsInputSchema?.properties ?? {}).length > 0;
|
||||
},
|
||||
|
||||
reset: () =>
|
||||
set({
|
||||
isGraphRunning: false,
|
||||
inputSchema: null,
|
||||
credentialsInputSchema: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { BlockUIType } from "@/app/(platform)/build/components/types";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import Form from "@rjsf/core";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { fields } from "./fields";
|
||||
import { templates } from "./templates";
|
||||
import { widgets } from "./widgets";
|
||||
import { preprocessInputSchema } from "./utils/input-schema-pre-processor";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type FormContextType = {
|
||||
nodeId?: string;
|
||||
uiType?: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
};
|
||||
|
||||
type FormRendererProps = {
|
||||
jsonSchema: RJSFSchema;
|
||||
handleChange: (formData: any) => void;
|
||||
uiSchema: any;
|
||||
initialValues: any;
|
||||
formContext: FormContextType;
|
||||
};
|
||||
|
||||
export const FormRenderer = ({
|
||||
jsonSchema,
|
||||
handleChange,
|
||||
uiSchema,
|
||||
initialValues,
|
||||
formContext,
|
||||
}: FormRendererProps) => {
|
||||
const preprocessedSchema = useMemo(() => {
|
||||
return preprocessInputSchema(jsonSchema);
|
||||
}, [jsonSchema]);
|
||||
return (
|
||||
<div className={"mt-4"}>
|
||||
<Form
|
||||
schema={preprocessedSchema}
|
||||
validator={validator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={formContext}
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,14 +4,17 @@ import { FieldProps, RJSFSchema } from "@rjsf/utils";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import { InputType, mapJsonSchemaTypeToInputType } from "../../helpers";
|
||||
import {
|
||||
InputType,
|
||||
mapJsonSchemaTypeToInputType,
|
||||
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
|
||||
import { InfoIcon } from "@phosphor-icons/react";
|
||||
import { useAnyOfField } from "./useAnyOfField";
|
||||
import NodeHandle from "../../../handlers/NodeHandle";
|
||||
import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { generateHandleId } from "../../../handlers/helpers";
|
||||
import { getTypeDisplayInfo } from "../../helpers";
|
||||
import { generateHandleId } from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import merge from "lodash/merge";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TypeOption = {
|
||||
type: string;
|
||||
@@ -46,9 +50,9 @@ export const AnyOfField = ({
|
||||
const handleId = generateHandleId(idSchema.$id ?? "");
|
||||
const updatedFormContexrt = { ...formContext, fromAnyOf: true };
|
||||
|
||||
const { nodeId } = updatedFormContexrt;
|
||||
const { nodeId, showHandles = true } = updatedFormContexrt;
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const isConnected = isInputConnected(nodeId, handleId);
|
||||
const isConnected = showHandles ? isInputConnected(nodeId, handleId) : false;
|
||||
const {
|
||||
isNullableType,
|
||||
nonNull,
|
||||
@@ -124,15 +128,21 @@ export const AnyOfField = ({
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(nonNull);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-0 flex flex-col">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="-ml-2 flex items-center gap-1">
|
||||
<NodeHandle
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
side="left"
|
||||
/>
|
||||
<Text variant="body">
|
||||
<div
|
||||
className={cn("flex items-center gap-1", showHandles && "-ml-2")}
|
||||
>
|
||||
{showHandles && (
|
||||
<NodeHandle
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
variant={formContext.size === "small" ? "body" : "body-medium"}
|
||||
>
|
||||
{name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
</Text>
|
||||
<Text variant="small" className={colorClass}>
|
||||
@@ -147,16 +157,22 @@ export const AnyOfField = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isConnected && isEnabled && renderInput(nonNull)}
|
||||
<div>{!isConnected && isEnabled && renderInput(nonNull)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="-mb-3 -ml-2 flex items-center gap-1">
|
||||
<NodeHandle handleId={handleId} isConnected={isConnected} side="left" />
|
||||
<Text variant="body">
|
||||
<div className="mb-0 flex flex-col">
|
||||
<div className={cn("flex items-center gap-1", showHandles && "-ml-2")}>
|
||||
{showHandles && (
|
||||
<NodeHandle
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
<Text variant={formContext.size === "small" ? "body" : "body-medium"}>
|
||||
{name.charAt(0).toUpperCase() + name.slice(1)}
|
||||
</Text>
|
||||
{!isConnected && (
|
||||
@@ -193,6 +209,7 @@ export const AnyOfField = ({
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isConnected && currentTypeOption && renderInput(currentTypeOption)}
|
||||
</div>
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
import { getDefaultRegistry } from "@rjsf/core";
|
||||
import { generateHandleId } from "../../handlers/helpers";
|
||||
import { ObjectEditor } from "../../components/ObjectEditor/ObjectEditor";
|
||||
import { generateHandleId } from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
|
||||
import { ObjectEditor } from "../widgets/ObjectEditorWidget/ObjectEditorWidget";
|
||||
|
||||
export const ObjectField = (props: FieldProps) => {
|
||||
const {
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { ArrayFieldTemplateProps } from "@rjsf/utils";
|
||||
import { ArrayEditor } from "../../components/ArrayEditor/ArrayEditor";
|
||||
import { ArrayEditorWidget } from "../widgets/ArrayEditorWidget/ArrayEditorWidget";
|
||||
|
||||
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
const {
|
||||
@@ -15,7 +15,7 @@ function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
const { nodeId } = formContext;
|
||||
|
||||
return (
|
||||
<ArrayEditor
|
||||
<ArrayEditorWidget
|
||||
items={items}
|
||||
nodeId={nodeId}
|
||||
canAdd={canAdd}
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
import NodeHandle from "../../handlers/NodeHandle";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { generateHandleId } from "../../handlers/helpers";
|
||||
import { getTypeDisplayInfo } from "../helpers";
|
||||
import { ArrayEditorContext } from "../../components/ArrayEditor/ArrayEditorContext";
|
||||
import { generateHandleId } from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { ArrayEditorContext } from "../widgets/ArrayEditorWidget/ArrayEditorContext";
|
||||
import {
|
||||
isCredentialFieldSchema,
|
||||
toDisplayName,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
|
||||
import { BlockUIType } from "@/lib/autogpt-server-api";
|
||||
import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||
|
||||
const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
id: fieldId,
|
||||
@@ -35,7 +35,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
uiSchema,
|
||||
}) => {
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId } = formContext;
|
||||
const { nodeId, showHandles = true, size = "small" } = formContext;
|
||||
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[nodeId] ?? false,
|
||||
@@ -55,7 +55,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
handleId = arrayFieldHandleId;
|
||||
}
|
||||
|
||||
const isConnected = isInputConnected(nodeId, handleId);
|
||||
const isConnected = showHandles ? isInputConnected(nodeId, handleId) : false;
|
||||
|
||||
if (!showAdvanced && schema.advanced === true && !isConnected) {
|
||||
return null;
|
||||
@@ -78,11 +78,22 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
return <div className="w-full space-y-1">{children}</div>;
|
||||
}
|
||||
|
||||
// Size-based styling
|
||||
|
||||
const shouldShowHandle =
|
||||
showHandles && !suppressHandle && !fromAnyOf && !isCredential;
|
||||
|
||||
return (
|
||||
<div className="w-[350px] space-y-1 pt-4">
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 space-y-2",
|
||||
fromAnyOf && "mb-0",
|
||||
size === "small" ? "w-[350px]" : "w-full",
|
||||
)}
|
||||
>
|
||||
{label && schema.type && (
|
||||
<label htmlFor={fieldId} className="flex items-center gap-1">
|
||||
{!suppressHandle && !fromAnyOf && !isCredential && (
|
||||
{shouldShowHandle && (
|
||||
<NodeHandle
|
||||
handleId={handleId}
|
||||
isConnected={isConnected}
|
||||
@@ -92,7 +103,11 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
{!fromAnyOf && (
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn("line-clamp-1", isCredential && "ml-3")}
|
||||
className={cn(
|
||||
"line-clamp-1",
|
||||
isCredential && !shouldShowHandle && "ml-3",
|
||||
size == "large" && "ml-0",
|
||||
)}
|
||||
>
|
||||
{isCredential && credentialProvider
|
||||
? toDisplayName(credentialProvider) + " credentials"
|
||||
@@ -123,7 +138,9 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
{(isAnyOf || !isConnected) && <div className="pl-2">{children}</div>}{" "}
|
||||
{(isAnyOf || !isConnected) && (
|
||||
<div className={cn(size === "small" ? "pl-2" : "")}>{children}</div>
|
||||
)}{" "}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ArrayFieldTemplateItemType, RJSFSchema } from "@rjsf/utils";
|
||||
import { generateHandleId, HandleIdType } from "../../handlers/helpers";
|
||||
import { ArrayEditorContext } from "./ArrayEditorContext";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import {
|
||||
generateHandleId,
|
||||
HandleIdType,
|
||||
} from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
|
||||
|
||||
export interface ArrayEditorProps {
|
||||
items?: ArrayFieldTemplateItemType<any, RJSFSchema, any>[];
|
||||
@@ -15,7 +18,7 @@ export interface ArrayEditorProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const ArrayEditor = ({
|
||||
export const ArrayEditorWidget = ({
|
||||
items,
|
||||
nodeId,
|
||||
canAdd,
|
||||
@@ -29,7 +32,7 @@ export const ArrayEditor = ({
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="max-w-[345px] flex-1">
|
||||
{items?.map((element) => {
|
||||
const arrayFieldHandleId = generateHandleId(
|
||||
fieldId,
|
||||
@@ -59,7 +62,7 @@ export const ArrayEditor = ({
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="relative top-5"
|
||||
className="relative top-2 min-w-0"
|
||||
size="small"
|
||||
onClick={element.onDropIndexClick(element.index)}
|
||||
>
|
||||
@@ -3,12 +3,24 @@ import { WidgetProps } from "@rjsf/utils";
|
||||
import { DateInput } from "@/components/atoms/DateInput/DateInput";
|
||||
|
||||
export const DateInputWidget = (props: WidgetProps) => {
|
||||
const { value, onChange, disabled, readonly, placeholder, autofocus, id } =
|
||||
props;
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
readonly,
|
||||
placeholder,
|
||||
autofocus,
|
||||
id,
|
||||
formContext,
|
||||
} = props;
|
||||
const { size = "small" } = formContext || {};
|
||||
|
||||
// Determine input size based on context
|
||||
const inputSize = size === "large" ? "default" : "small";
|
||||
|
||||
return (
|
||||
<DateInput
|
||||
size="small"
|
||||
size={inputSize as any}
|
||||
id={id}
|
||||
hideLabel={true}
|
||||
label={""}
|
||||
@@ -2,11 +2,24 @@ import { WidgetProps } from "@rjsf/utils";
|
||||
import { DateTimeInput } from "@/components/atoms/DateTimeInput/DateTimeInput";
|
||||
|
||||
export const DateTimeInputWidget = (props: WidgetProps) => {
|
||||
const { value, onChange, disabled, readonly, placeholder, autofocus, id } =
|
||||
props;
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
readonly,
|
||||
placeholder,
|
||||
autofocus,
|
||||
id,
|
||||
formContext,
|
||||
} = props;
|
||||
const { size = "small" } = formContext || {};
|
||||
|
||||
// Determine input size based on context
|
||||
const inputSize = size === "large" ? "medium" : "small";
|
||||
|
||||
return (
|
||||
<DateTimeInput
|
||||
size="small"
|
||||
size={inputSize as any}
|
||||
id={id}
|
||||
hideLabel={true}
|
||||
label={""}
|
||||
@@ -5,13 +5,13 @@ import { Plus, X } from "lucide-react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import NodeHandle from "../../handlers/NodeHandle";
|
||||
import NodeHandle from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import {
|
||||
generateHandleId,
|
||||
HandleIdType,
|
||||
parseKeyValueHandleId,
|
||||
} from "../../handlers/helpers";
|
||||
} from "@/app/(platform)/build/components/FlowEditor/handlers/helpers";
|
||||
|
||||
export interface ObjectEditorProps {
|
||||
id: string;
|
||||
@@ -90,6 +90,10 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
}
|
||||
});
|
||||
|
||||
// Note: ObjectEditor is always used in node context, so showHandles is always true
|
||||
// If you need to use it in dialog context, you'll need to pass showHandles via props
|
||||
const showHandles = true;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -107,11 +111,13 @@ export const ObjectEditor = React.forwardRef<HTMLDivElement, ObjectEditorProps>(
|
||||
return (
|
||||
<div key={idx} className="flex flex-col gap-2">
|
||||
<div className="-ml-2 flex items-center gap-1">
|
||||
<NodeHandle
|
||||
isConnected={isDynamicPropertyConnected}
|
||||
handleId={handleId}
|
||||
side="left"
|
||||
/>
|
||||
{showHandles && (
|
||||
<NodeHandle
|
||||
isConnected={isDynamicPropertyConnected}
|
||||
handleId={handleId}
|
||||
side="left"
|
||||
/>
|
||||
)}
|
||||
<Text variant="small" className="!text-gray-500">
|
||||
#{key.trim() === "" ? "" : key}
|
||||
</Text>
|
||||
@@ -1,5 +1,8 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { InputType, mapJsonSchemaTypeToInputType } from "../helpers";
|
||||
import {
|
||||
InputType,
|
||||
mapJsonSchemaTypeToInputType,
|
||||
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import {
|
||||
MultiSelector,
|
||||
@@ -11,9 +14,14 @@ import {
|
||||
} from "@/components/__legacy__/ui/multiselect";
|
||||
|
||||
export const SelectWidget = (props: WidgetProps) => {
|
||||
const { options, value, onChange, disabled, readonly, id } = props;
|
||||
const { options, value, onChange, disabled, readonly, id, formContext } =
|
||||
props;
|
||||
const enumOptions = options.enumOptions || [];
|
||||
const type = mapJsonSchemaTypeToInputType(props.schema);
|
||||
const { size = "small" } = formContext || {};
|
||||
|
||||
// Determine select size based on context
|
||||
const selectSize = size === "large" ? "medium" : "small";
|
||||
|
||||
const renderInput = () => {
|
||||
if (type === InputType.MULTI_SELECT) {
|
||||
@@ -44,7 +52,7 @@ export const SelectWidget = (props: WidgetProps) => {
|
||||
id={id}
|
||||
hideLabel={true}
|
||||
disabled={disabled || readonly}
|
||||
size="small"
|
||||
size={selectSize as any}
|
||||
value={value ?? ""}
|
||||
onValueChange={onChange}
|
||||
options={
|
||||
@@ -1,11 +1,17 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { InputType, mapJsonSchemaTypeToInputType } from "../helpers";
|
||||
import {
|
||||
InputType,
|
||||
mapJsonSchemaTypeToInputType,
|
||||
} from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export const TextInputWidget = (props: WidgetProps) => {
|
||||
const { schema, formContext } = props;
|
||||
const { uiType } = formContext as { uiType: BlockUIType };
|
||||
const { uiType, size = "small" } = formContext as {
|
||||
uiType: BlockUIType;
|
||||
size?: string;
|
||||
};
|
||||
|
||||
const mapped = mapJsonSchemaTypeToInputType(schema);
|
||||
|
||||
@@ -53,6 +59,9 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
return props.onChange(config.handleChange(v));
|
||||
};
|
||||
|
||||
// Determine input size based on context
|
||||
const inputSize = size === "large" ? "medium" : "small";
|
||||
|
||||
if (uiType === BlockUIType.NOTE) {
|
||||
return (
|
||||
<Input
|
||||
@@ -78,7 +87,7 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
hideLabel={true}
|
||||
type={config.htmlType as any}
|
||||
label={""}
|
||||
size="small"
|
||||
size={inputSize as any}
|
||||
wrapperClassName="mb-0"
|
||||
value={props.value ?? ""}
|
||||
onChange={handleChange}
|
||||
@@ -2,7 +2,13 @@ import { WidgetProps } from "@rjsf/utils";
|
||||
import { TimeInput } from "@/components/atoms/TimeInput/TimeInput";
|
||||
|
||||
export const TimeInputWidget = (props: WidgetProps) => {
|
||||
const { value, onChange, disabled, readonly, placeholder, id } = props;
|
||||
const { value, onChange, disabled, readonly, placeholder, id, formContext } =
|
||||
props;
|
||||
const { size = "small" } = formContext || {};
|
||||
|
||||
// Determine input size based on context
|
||||
const inputSize = size === "large" ? "medium" : "small";
|
||||
|
||||
return (
|
||||
<TimeInput
|
||||
value={value}
|
||||
@@ -11,7 +17,7 @@ export const TimeInputWidget = (props: WidgetProps) => {
|
||||
label={""}
|
||||
id={id}
|
||||
hideLabel={true}
|
||||
size="small"
|
||||
size={inputSize as any}
|
||||
wrapperClassName="!mb-0 "
|
||||
disabled={disabled || readonly}
|
||||
placeholder={placeholder}
|
||||
Reference in New Issue
Block a user