mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-11 16:18:07 -05:00
Compare commits
9 Commits
add-llm-ma
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f482eb668b | ||
|
|
fc8434fb30 | ||
|
|
3ae08cd48e | ||
|
|
4db13837b9 | ||
|
|
df87867625 | ||
|
|
e503126170 | ||
|
|
7ee28197a3 | ||
|
|
818de26d24 | ||
|
|
4a7bc006a8 |
@@ -495,8 +495,14 @@ class SmartDecisionMakerBlock(Block):
|
||||
}
|
||||
|
||||
properties = {}
|
||||
field_mapping = {}
|
||||
|
||||
for link in links:
|
||||
field_name = link.sink_name
|
||||
|
||||
clean_field_name = SmartDecisionMakerBlock.cleanup(field_name)
|
||||
field_mapping[clean_field_name] = field_name
|
||||
|
||||
sink_block_input_schema = sink_node.input_default["input_schema"]
|
||||
sink_block_properties = sink_block_input_schema.get("properties", {}).get(
|
||||
link.sink_name, {}
|
||||
@@ -506,7 +512,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
if "description" in sink_block_properties
|
||||
else f"The {link.sink_name} of the tool"
|
||||
)
|
||||
properties[link.sink_name] = {
|
||||
properties[clean_field_name] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
"default": json.dumps(sink_block_properties.get("default", None)),
|
||||
@@ -519,7 +525,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
# Store node info for later use in output processing
|
||||
tool_function["_field_mapping"] = field_mapping
|
||||
tool_function["_sink_node_id"] = sink_node.id
|
||||
|
||||
return {"type": "function", "function": tool_function}
|
||||
@@ -1129,8 +1135,9 @@ class SmartDecisionMakerBlock(Block):
|
||||
original_field_name = field_mapping.get(clean_arg_name, clean_arg_name)
|
||||
arg_value = tool_args.get(clean_arg_name)
|
||||
|
||||
sanitized_arg_name = self.cleanup(original_field_name)
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{sanitized_arg_name}"
|
||||
# Use original_field_name directly (not sanitized) to match link sink_name
|
||||
# The field_mapping already translates from LLM's cleaned names to original names
|
||||
emit_key = f"tools_^_{sink_node_id}_~_{original_field_name}"
|
||||
|
||||
logger.debug(
|
||||
"[SmartDecisionMakerBlock|geid:%s|neid:%s] emit %s",
|
||||
|
||||
@@ -196,6 +196,15 @@ class TestXMLParserBlockSecurity:
|
||||
async for _ in block.run(XMLParserBlock.Input(input_xml=large_xml)):
|
||||
pass
|
||||
|
||||
async def test_rejects_text_outside_root(self):
|
||||
"""Ensure parser surfaces readable errors for invalid root text."""
|
||||
block = XMLParserBlock()
|
||||
invalid_xml = "<root><child>value</child></root> trailing"
|
||||
|
||||
with pytest.raises(ValueError, match="text outside the root element"):
|
||||
async for _ in block.run(XMLParserBlock.Input(input_xml=invalid_xml)):
|
||||
pass
|
||||
|
||||
|
||||
class TestStoreMediaFileSecurity:
|
||||
"""Test file storage security limits."""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from gravitasml.parser import Parser
|
||||
from gravitasml.token import tokenize
|
||||
from gravitasml.token import Token, tokenize
|
||||
|
||||
from backend.data.block import Block, BlockOutput, BlockSchemaInput, BlockSchemaOutput
|
||||
from backend.data.model import SchemaField
|
||||
@@ -25,6 +25,38 @@ class XMLParserBlock(Block):
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_tokens(tokens: list[Token]) -> None:
|
||||
"""Ensure the XML has a single root element and no stray text."""
|
||||
if not tokens:
|
||||
raise ValueError("XML input is empty.")
|
||||
|
||||
depth = 0
|
||||
root_seen = False
|
||||
|
||||
for token in tokens:
|
||||
if token.type == "TAG_OPEN":
|
||||
if depth == 0 and root_seen:
|
||||
raise ValueError("XML must have a single root element.")
|
||||
depth += 1
|
||||
if depth == 1:
|
||||
root_seen = True
|
||||
elif token.type == "TAG_CLOSE":
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
raise SyntaxError("Unexpected closing tag in XML input.")
|
||||
elif token.type in {"TEXT", "ESCAPE"}:
|
||||
if depth == 0 and token.value:
|
||||
raise ValueError(
|
||||
"XML contains text outside the root element; "
|
||||
"wrap content in a single root tag."
|
||||
)
|
||||
|
||||
if depth != 0:
|
||||
raise SyntaxError("Unclosed tag detected in XML input.")
|
||||
if not root_seen:
|
||||
raise ValueError("XML must include a root element.")
|
||||
|
||||
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||
# Security fix: Add size limits to prevent XML bomb attacks
|
||||
MAX_XML_SIZE = 10 * 1024 * 1024 # 10MB limit for XML input
|
||||
@@ -35,7 +67,9 @@ class XMLParserBlock(Block):
|
||||
)
|
||||
|
||||
try:
|
||||
tokens = tokenize(input_data.input_xml)
|
||||
tokens = list(tokenize(input_data.input_xml))
|
||||
self._validate_tokens(tokens)
|
||||
|
||||
parser = Parser(tokens)
|
||||
parsed_result = parser.parse()
|
||||
yield "parsed_xml", parsed_result
|
||||
|
||||
8
autogpt_platform/backend/poetry.lock
generated
8
autogpt_platform/backend/poetry.lock
generated
@@ -1924,14 +1924,14 @@ google = ["google-api-python-client (>=2.0.0)", "google-auth (>=2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "gravitasml"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "gravitasml-0.1.3-py3-none-any.whl", hash = "sha256:51ff98b4564b7a61f7796f18d5f2558b919d30b3722579296089645b7bc18b85"},
|
||||
{file = "gravitasml-0.1.3.tar.gz", hash = "sha256:04d240b9fa35878252d57a36032130b6516487468847fcdced1022c032a20f57"},
|
||||
{file = "gravitasml-0.1.4-py3-none-any.whl", hash = "sha256:671a18b11d3d8a0e270c6a80c72cd058458b18d5ef7560d00010e962ab1bca74"},
|
||||
{file = "gravitasml-0.1.4.tar.gz", hash = "sha256:35d0d9fec7431817482d53d9c976e375557c3e041d1eb6928e809324a8c866e3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -7295,4 +7295,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "b762806d5d58fcf811220890c4705a16dc62b33387af43e3a29399c62a641098"
|
||||
content-hash = "a93ba0cea3b465cb6ec3e3f258b383b09f84ea352ccfdbfa112902cde5653fc6"
|
||||
|
||||
@@ -27,7 +27,7 @@ google-api-python-client = "^2.177.0"
|
||||
google-auth-oauthlib = "^1.2.2"
|
||||
google-cloud-storage = "^3.2.0"
|
||||
googlemaps = "^4.10.0"
|
||||
gravitasml = "^0.1.3"
|
||||
gravitasml = "^0.1.4"
|
||||
groq = "^0.30.0"
|
||||
html2text = "^2024.2.26"
|
||||
jinja2 = "^3.1.6"
|
||||
|
||||
146
autogpt_platform/cloudflare_worker.js
Normal file
146
autogpt_platform/cloudflare_worker.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Cloudflare Workers Script for docs.agpt.co → agpt.co/docs migration
|
||||
*
|
||||
* Deploy this script to handle all redirects with a single JavaScript file.
|
||||
* No rule limits, easy to maintain, handles all edge cases.
|
||||
*/
|
||||
|
||||
// URL mapping for special cases that don't follow patterns
|
||||
const SPECIAL_MAPPINGS = {
|
||||
// Root page
|
||||
'/': '/docs/platform',
|
||||
|
||||
// Special cases that don't follow standard patterns
|
||||
'/platform/d_id/': '/docs/integrations/block-integrations/d-id',
|
||||
'/platform/blocks/blocks/': '/docs/integrations',
|
||||
'/platform/blocks/decoder_block/': '/docs/integrations/block-integrations/text-decoder',
|
||||
'/platform/blocks/http': '/docs/integrations/block-integrations/send-web-request',
|
||||
'/platform/blocks/llm/': '/docs/integrations/block-integrations/ai-and-llm',
|
||||
'/platform/blocks/time_blocks': '/docs/integrations/block-integrations/time-and-date',
|
||||
'/platform/blocks/text_to_speech_block': '/docs/integrations/block-integrations/text-to-speech',
|
||||
'/platform/blocks/ai_shortform_video_block': '/docs/integrations/block-integrations/ai-shortform-video',
|
||||
'/platform/blocks/replicate_flux_advanced': '/docs/integrations/block-integrations/replicate-flux-advanced',
|
||||
'/platform/blocks/flux_kontext': '/docs/integrations/block-integrations/flux-kontext',
|
||||
'/platform/blocks/ai_condition/': '/docs/integrations/block-integrations/ai-condition',
|
||||
'/platform/blocks/email_block': '/docs/integrations/block-integrations/email',
|
||||
'/platform/blocks/google_maps': '/docs/integrations/block-integrations/google-maps',
|
||||
'/platform/blocks/google/gmail': '/docs/integrations/block-integrations/gmail',
|
||||
'/platform/blocks/github/issues/': '/docs/integrations/block-integrations/github-issues',
|
||||
'/platform/blocks/github/repo/': '/docs/integrations/block-integrations/github-repo',
|
||||
'/platform/blocks/github/pull_requests': '/docs/integrations/block-integrations/github-pull-requests',
|
||||
'/platform/blocks/twitter/twitter': '/docs/integrations/block-integrations/twitter',
|
||||
'/classic/setup/': '/docs/classic/setup/setting-up-autogpt-classic',
|
||||
'/code-of-conduct/': '/docs/classic/help-us-improve-autogpt/code-of-conduct',
|
||||
'/contributing/': '/docs/classic/contributing',
|
||||
'/contribute/': '/docs/contribute',
|
||||
'/forge/components/introduction/': '/docs/classic/forge/introduction'
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform path by replacing underscores with hyphens and removing trailing slashes
|
||||
*/
|
||||
function transformPath(path) {
|
||||
return path.replace(/_/g, '-').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle docs.agpt.co redirects
|
||||
*/
|
||||
function handleDocsRedirect(url) {
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Check special mappings first
|
||||
if (SPECIAL_MAPPINGS[pathname]) {
|
||||
return `https://agpt.co${SPECIAL_MAPPINGS[pathname]}`;
|
||||
}
|
||||
|
||||
// Pattern-based redirects
|
||||
|
||||
// Platform blocks: /platform/blocks/* → /docs/integrations/block-integrations/*
|
||||
if (pathname.startsWith('/platform/blocks/')) {
|
||||
const blockName = pathname.substring('/platform/blocks/'.length);
|
||||
const transformedName = transformPath(blockName);
|
||||
return `https://agpt.co/docs/integrations/block-integrations/${transformedName}`;
|
||||
}
|
||||
|
||||
// Platform contributing: /platform/contributing/* → /docs/platform/contributing/*
|
||||
if (pathname.startsWith('/platform/contributing/')) {
|
||||
const subPath = pathname.substring('/platform/contributing/'.length);
|
||||
return `https://agpt.co/docs/platform/contributing/${subPath}`;
|
||||
}
|
||||
|
||||
// Platform general: /platform/* → /docs/platform/* (with underscore→hyphen)
|
||||
if (pathname.startsWith('/platform/')) {
|
||||
const subPath = pathname.substring('/platform/'.length);
|
||||
const transformedPath = transformPath(subPath);
|
||||
return `https://agpt.co/docs/platform/${transformedPath}`;
|
||||
}
|
||||
|
||||
// Forge components: /forge/components/* → /docs/classic/forge/introduction/*
|
||||
if (pathname.startsWith('/forge/components/')) {
|
||||
const subPath = pathname.substring('/forge/components/'.length);
|
||||
return `https://agpt.co/docs/classic/forge/introduction/${subPath}`;
|
||||
}
|
||||
|
||||
// Forge general: /forge/* → /docs/classic/forge/*
|
||||
if (pathname.startsWith('/forge/')) {
|
||||
const subPath = pathname.substring('/forge/'.length);
|
||||
return `https://agpt.co/docs/classic/forge/${subPath}`;
|
||||
}
|
||||
|
||||
// Classic: /classic/* → /docs/classic/*
|
||||
if (pathname.startsWith('/classic/')) {
|
||||
const subPath = pathname.substring('/classic/'.length);
|
||||
return `https://agpt.co/docs/classic/${subPath}`;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'https://agpt.co/docs/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Worker function
|
||||
*/
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Only handle docs.agpt.co requests
|
||||
if (url.hostname === 'docs.agpt.co') {
|
||||
const redirectUrl = handleDocsRedirect(url);
|
||||
|
||||
return new Response(null, {
|
||||
status: 301,
|
||||
headers: {
|
||||
'Location': redirectUrl,
|
||||
'Cache-Control': 'max-age=300' // Cache redirects for 5 minutes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For non-docs requests, pass through or return 404
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
};
|
||||
|
||||
// Test function for local development
|
||||
export function testRedirects() {
|
||||
const testCases = [
|
||||
'https://docs.agpt.co/',
|
||||
'https://docs.agpt.co/platform/getting-started/',
|
||||
'https://docs.agpt.co/platform/advanced_setup/',
|
||||
'https://docs.agpt.co/platform/blocks/basic/',
|
||||
'https://docs.agpt.co/platform/blocks/ai_condition/',
|
||||
'https://docs.agpt.co/classic/setup/',
|
||||
'https://docs.agpt.co/forge/components/agents/',
|
||||
'https://docs.agpt.co/contributing/',
|
||||
'https://docs.agpt.co/unknown-page'
|
||||
];
|
||||
|
||||
console.log('Testing redirects:');
|
||||
testCases.forEach(testUrl => {
|
||||
const url = new URL(testUrl);
|
||||
const result = handleDocsRedirect(url);
|
||||
console.log(`${testUrl} → ${result}`);
|
||||
});
|
||||
}
|
||||
@@ -46,14 +46,15 @@
|
||||
"@radix-ui/react-scroll-area": "1.2.10",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slider": "1.3.6",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@rjsf/core": "5.24.13",
|
||||
"@rjsf/utils": "5.24.13",
|
||||
"@rjsf/validator-ajv8": "5.24.13",
|
||||
"@rjsf/core": "6.1.2",
|
||||
"@rjsf/utils": "6.1.2",
|
||||
"@rjsf/validator-ajv8": "6.1.2",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@supabase/ssr": "0.7.0",
|
||||
"@supabase/supabase-js": "2.78.0",
|
||||
|
||||
3804
autogpt_platform/frontend/pnpm-lock.yaml
generated
3804
autogpt_platform/frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { OAuthPopupResultMessage } from "@/components/renderers/input-renderer/fields/CredentialField/models/OAuthCredentialModal/useOAuthCredentialModal";
|
||||
import { OAuthPopupResultMessage } from "./types";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// This route is intended to be used as the callback for integration OAuth flows,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export type OAuthPopupResultMessage = { message_type: "oauth_popup_result" } & (
|
||||
| {
|
||||
success: true;
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
);
|
||||
@@ -5,7 +5,7 @@ import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { ClockIcon, PlayIcon } from "@phosphor-icons/react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||
import { useRunInputDialog } from "./useRunInputDialog";
|
||||
import { CronSchedulerDialog } from "../CronSchedulerDialog/CronSchedulerDialog";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
|
||||
import { isCredentialFieldSchema } from "@/components/renderers/input-renderer/fields/CredentialField/helpers";
|
||||
import { isCredentialFieldSchema } from "@/components/renderers/InputRenderer/custom/CredentialField/helpers";
|
||||
|
||||
export const useRunInputDialog = ({
|
||||
setIsOpen,
|
||||
|
||||
@@ -12,16 +12,59 @@ import {
|
||||
import { useDraftRecoveryPopup } from "./useDraftRecoveryPopup";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { DraftDiff } from "@/lib/dexie/draft-utils";
|
||||
|
||||
interface DraftRecoveryPopupProps {
|
||||
isInitialLoadComplete: boolean;
|
||||
}
|
||||
|
||||
function formatDiffSummary(diff: DraftDiff | null): string {
|
||||
if (!diff) return "";
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
// Node changes
|
||||
const nodeChanges: string[] = [];
|
||||
if (diff.nodes.added > 0) nodeChanges.push(`+${diff.nodes.added}`);
|
||||
if (diff.nodes.removed > 0) nodeChanges.push(`-${diff.nodes.removed}`);
|
||||
if (diff.nodes.modified > 0) nodeChanges.push(`~${diff.nodes.modified}`);
|
||||
|
||||
if (nodeChanges.length > 0) {
|
||||
parts.push(
|
||||
`${nodeChanges.join("/")} block${diff.nodes.added + diff.nodes.removed + diff.nodes.modified !== 1 ? "s" : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Edge changes
|
||||
const edgeChanges: string[] = [];
|
||||
if (diff.edges.added > 0) edgeChanges.push(`+${diff.edges.added}`);
|
||||
if (diff.edges.removed > 0) edgeChanges.push(`-${diff.edges.removed}`);
|
||||
if (diff.edges.modified > 0) edgeChanges.push(`~${diff.edges.modified}`);
|
||||
|
||||
if (edgeChanges.length > 0) {
|
||||
parts.push(
|
||||
`${edgeChanges.join("/")} connection${diff.edges.added + diff.edges.removed + diff.edges.modified !== 1 ? "s" : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
export function DraftRecoveryPopup({
|
||||
isInitialLoadComplete,
|
||||
}: DraftRecoveryPopupProps) {
|
||||
const { isOpen, popupRef, nodeCount, edgeCount, savedAt, onLoad, onDiscard } =
|
||||
useDraftRecoveryPopup(isInitialLoadComplete);
|
||||
const {
|
||||
isOpen,
|
||||
popupRef,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
diff,
|
||||
savedAt,
|
||||
onLoad,
|
||||
onDiscard,
|
||||
} = useDraftRecoveryPopup(isInitialLoadComplete);
|
||||
|
||||
const diffSummary = formatDiffSummary(diff);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -72,10 +115,9 @@ export function DraftRecoveryPopup({
|
||||
variant="small"
|
||||
className="text-amber-700 dark:text-amber-400"
|
||||
>
|
||||
{nodeCount} block{nodeCount !== 1 ? "s" : ""}, {edgeCount}{" "}
|
||||
connection
|
||||
{edgeCount !== 1 ? "s" : ""} •{" "}
|
||||
{formatTimeAgo(new Date(savedAt).toISOString())}
|
||||
{diffSummary ||
|
||||
`${nodeCount} block${nodeCount !== 1 ? "s" : ""}, ${edgeCount} connection${edgeCount !== 1 ? "s" : ""}`}{" "}
|
||||
• {formatTimeAgo(new Date(savedAt).toISOString())}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useDraftRecoveryPopup = (isInitialLoadComplete: boolean) => {
|
||||
savedAt,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
diff,
|
||||
loadDraft: onLoad,
|
||||
discardDraft: onDiscard,
|
||||
} = useDraftManager(isInitialLoadComplete);
|
||||
@@ -54,6 +55,7 @@ export const useDraftRecoveryPopup = (isInitialLoadComplete: boolean) => {
|
||||
isOpen,
|
||||
nodeCount,
|
||||
edgeCount,
|
||||
diff,
|
||||
savedAt,
|
||||
onLoad,
|
||||
onDiscard,
|
||||
|
||||
@@ -48,8 +48,6 @@ export const resolveCollisions: CollisionAlgorithm = (
|
||||
const width = (node.width ?? node.measured?.width ?? 0) + margin * 2;
|
||||
const height = (node.height ?? node.measured?.height ?? 0) + margin * 2;
|
||||
|
||||
console.log("width", width);
|
||||
console.log("height", height);
|
||||
const x = node.position.x - margin;
|
||||
const y = node.position.y - margin;
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
DraftData,
|
||||
} from "@/services/builder-draft/draft-service";
|
||||
import { BuilderDraft } from "@/lib/dexie/db";
|
||||
import { cleanNodes, cleanEdges } from "@/lib/dexie/draft-utils";
|
||||
import {
|
||||
cleanNodes,
|
||||
cleanEdges,
|
||||
calculateDraftDiff,
|
||||
DraftDiff,
|
||||
} from "@/lib/dexie/draft-utils";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { useGraphStore } from "../../../stores/graphStore";
|
||||
@@ -19,6 +24,7 @@ const AUTO_SAVE_INTERVAL_MS = 15000; // 15 seconds
|
||||
interface DraftRecoveryState {
|
||||
isOpen: boolean;
|
||||
draft: BuilderDraft | null;
|
||||
diff: DraftDiff | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +37,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
const [state, setState] = useState<DraftRecoveryState>({
|
||||
isOpen: false,
|
||||
draft: null,
|
||||
diff: null,
|
||||
});
|
||||
|
||||
const [{ flowID, flowVersion }] = useQueryStates({
|
||||
@@ -207,9 +214,16 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
);
|
||||
|
||||
if (isDifferent && (draft.nodes.length > 0 || draft.edges.length > 0)) {
|
||||
const diff = calculateDraftDiff(
|
||||
draft.nodes,
|
||||
draft.edges,
|
||||
currentNodes,
|
||||
currentEdges,
|
||||
);
|
||||
setState({
|
||||
isOpen: true,
|
||||
draft,
|
||||
diff,
|
||||
});
|
||||
} else {
|
||||
await draftService.deleteDraft(effectiveFlowId);
|
||||
@@ -231,6 +245,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
setState({
|
||||
isOpen: false,
|
||||
draft: null,
|
||||
diff: null,
|
||||
});
|
||||
}, [flowID]);
|
||||
|
||||
@@ -242,8 +257,10 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
try {
|
||||
useNodeStore.getState().setNodes(draft.nodes);
|
||||
useEdgeStore.getState().setEdges(draft.edges);
|
||||
draft.nodes.forEach((node) => {
|
||||
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
|
||||
});
|
||||
|
||||
// Restore nodeCounter to prevent ID conflicts when adding new nodes
|
||||
if (draft.nodeCounter !== undefined) {
|
||||
useNodeStore.setState({ nodeCounter: draft.nodeCounter });
|
||||
}
|
||||
@@ -267,6 +284,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
setState({
|
||||
isOpen: false,
|
||||
draft: null,
|
||||
diff: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[DraftRecovery] Failed to load draft:", error);
|
||||
@@ -275,7 +293,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
|
||||
const discardDraft = useCallback(async () => {
|
||||
if (!state.draft) {
|
||||
setState({ isOpen: false, draft: null });
|
||||
setState({ isOpen: false, draft: null, diff: null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,7 +303,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
console.error("[DraftRecovery] Failed to discard draft:", error);
|
||||
}
|
||||
|
||||
setState({ isOpen: false, draft: null });
|
||||
setState({ isOpen: false, draft: null, diff: null });
|
||||
}, [state.draft]);
|
||||
|
||||
return {
|
||||
@@ -294,6 +312,7 @@ export function useDraftManager(isInitialLoadComplete: boolean) {
|
||||
savedAt: state.draft?.savedAt ?? 0,
|
||||
nodeCount: state.draft?.nodes.length ?? 0,
|
||||
edgeCount: state.draft?.edges.length ?? 0,
|
||||
diff: state.diff,
|
||||
loadDraft,
|
||||
discardDraft,
|
||||
};
|
||||
|
||||
@@ -121,6 +121,14 @@ export const useFlow = () => {
|
||||
if (customNodes.length > 0) {
|
||||
useNodeStore.getState().setNodes([]);
|
||||
addNodes(customNodes);
|
||||
|
||||
// Sync hardcoded values with handle IDs.
|
||||
// If a key–value field has a key without a value, the backend omits it from hardcoded values.
|
||||
// But if a handleId exists for that key, it causes inconsistency.
|
||||
// This ensures hardcoded values stay in sync with handle IDs.
|
||||
customNodes.forEach((node) => {
|
||||
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
|
||||
});
|
||||
}
|
||||
}, [customNodes, addNodes]);
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Connection as RFConnection, EdgeChange } from "@xyflow/react";
|
||||
import {
|
||||
Connection as RFConnection,
|
||||
EdgeChange,
|
||||
applyEdgeChanges,
|
||||
} from "@xyflow/react";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { useCallback } from "react";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
|
||||
export const useCustomEdge = () => {
|
||||
const edges = useEdgeStore((s) => s.edges);
|
||||
const addEdge = useEdgeStore((s) => s.addEdge);
|
||||
const removeEdge = useEdgeStore((s) => s.removeEdge);
|
||||
const setEdges = useEdgeStore((s) => s.setEdges);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(conn: RFConnection) => {
|
||||
@@ -45,14 +50,10 @@ export const useCustomEdge = () => {
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "remove") {
|
||||
removeEdge(change.id);
|
||||
}
|
||||
});
|
||||
(changes: EdgeChange<CustomEdge>[]) => {
|
||||
setEdges(applyEdgeChanges(changes, edges));
|
||||
},
|
||||
[removeEdge],
|
||||
[edges, setEdges],
|
||||
);
|
||||
|
||||
return { edges, onConnect, onEdgesChange };
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { CircleIcon } from "@phosphor-icons/react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NodeHandle = ({
|
||||
const InputNodeHandle = ({
|
||||
handleId,
|
||||
isConnected,
|
||||
side,
|
||||
nodeId,
|
||||
}: {
|
||||
handleId: string;
|
||||
isConnected: boolean;
|
||||
side: "left" | "right";
|
||||
nodeId: string;
|
||||
}) => {
|
||||
const cleanedHandleId = cleanUpHandleId(handleId);
|
||||
const isInputConnected = useEdgeStore((state) =>
|
||||
state.isInputConnected(nodeId ?? "", cleanedHandleId),
|
||||
);
|
||||
|
||||
return (
|
||||
<Handle
|
||||
type={side === "left" ? "target" : "source"}
|
||||
position={side === "left" ? Position.Left : Position.Right}
|
||||
id={handleId}
|
||||
className={side === "left" ? "-ml-4 mr-2" : "-mr-2 ml-2"}
|
||||
type={"target"}
|
||||
position={Position.Left}
|
||||
id={cleanedHandleId}
|
||||
className={"-ml-6 mr-2"}
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<CircleIcon
|
||||
size={16}
|
||||
weight={isConnected ? "fill" : "duotone"}
|
||||
weight={isInputConnected ? "fill" : "duotone"}
|
||||
className={"text-gray-400 opacity-100"}
|
||||
/>
|
||||
</div>
|
||||
@@ -28,4 +34,35 @@ const NodeHandle = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeHandle;
|
||||
const OutputNodeHandle = ({
|
||||
field_name,
|
||||
nodeId,
|
||||
hexColor,
|
||||
}: {
|
||||
field_name: string;
|
||||
nodeId: string;
|
||||
hexColor: string;
|
||||
}) => {
|
||||
const isOutputConnected = useEdgeStore((state) =>
|
||||
state.isOutputConnected(nodeId, field_name),
|
||||
);
|
||||
return (
|
||||
<Handle
|
||||
type={"source"}
|
||||
position={Position.Right}
|
||||
id={field_name}
|
||||
className={"-mr-2 ml-2"}
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<CircleIcon
|
||||
size={16}
|
||||
weight={"duotone"}
|
||||
color={isOutputConnected ? hexColor : "gray"}
|
||||
className={cn("text-gray-400 opacity-100")}
|
||||
/>
|
||||
</div>
|
||||
</Handle>
|
||||
);
|
||||
};
|
||||
|
||||
export { InputNodeHandle, OutputNodeHandle };
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
/**
|
||||
* Handle ID Types for different input structures
|
||||
*
|
||||
* Examples:
|
||||
* SIMPLE: "message"
|
||||
* NESTED: "config.api_key"
|
||||
* ARRAY: "items_$_0", "items_$_1"
|
||||
* KEY_VALUE: "headers_#_Authorization", "params_#_limit"
|
||||
*
|
||||
* Note: All handle IDs are sanitized to remove spaces and special characters.
|
||||
* Spaces become underscores, and special characters are removed.
|
||||
* Example: "user name" becomes "user_name", "email@domain.com" becomes "emaildomaincom"
|
||||
*/
|
||||
export enum HandleIdType {
|
||||
SIMPLE = "SIMPLE",
|
||||
NESTED = "NESTED",
|
||||
ARRAY = "ARRAY",
|
||||
KEY_VALUE = "KEY_VALUE",
|
||||
}
|
||||
|
||||
const fromRjsfId = (id: string): string => {
|
||||
if (!id) return "";
|
||||
const parts = id.split("_");
|
||||
const filtered = parts.filter(
|
||||
(p) => p !== "root" && p !== "properties" && p.length > 0,
|
||||
);
|
||||
return filtered.join("_") || "";
|
||||
};
|
||||
// Here we are handling single level of nesting, if need more in future then i will update it
|
||||
|
||||
const sanitizeForHandleId = (str: string): string => {
|
||||
if (!str) return "";
|
||||
@@ -38,51 +11,53 @@ const sanitizeForHandleId = (str: string): string => {
|
||||
.replace(/^_|_$/g, ""); // Remove leading/trailing underscores
|
||||
};
|
||||
|
||||
export const generateHandleId = (
|
||||
const cleanTitleId = (id: string): string => {
|
||||
if (!id) return "";
|
||||
|
||||
if (id.endsWith("_title")) {
|
||||
id = id.slice(0, -6);
|
||||
}
|
||||
const parts = id.split("_");
|
||||
const filtered = parts.filter(
|
||||
(p) => p !== "root" && p !== "properties" && p.length > 0,
|
||||
);
|
||||
const filtered_id = filtered.join("_") || "";
|
||||
return filtered_id;
|
||||
};
|
||||
|
||||
export const generateHandleIdFromTitleId = (
|
||||
fieldKey: string,
|
||||
nestedValues: string[] = [],
|
||||
type: HandleIdType = HandleIdType.SIMPLE,
|
||||
{
|
||||
isObjectProperty,
|
||||
isAdditionalProperty,
|
||||
isArrayItem,
|
||||
}: {
|
||||
isArrayItem?: boolean;
|
||||
isObjectProperty?: boolean;
|
||||
isAdditionalProperty?: boolean;
|
||||
} = {
|
||||
isArrayItem: false,
|
||||
isObjectProperty: false,
|
||||
isAdditionalProperty: false,
|
||||
},
|
||||
): string => {
|
||||
if (!fieldKey) return "";
|
||||
|
||||
fieldKey = fromRjsfId(fieldKey);
|
||||
fieldKey = sanitizeForHandleId(fieldKey);
|
||||
const filteredKey = cleanTitleId(fieldKey);
|
||||
if (isAdditionalProperty || isArrayItem) {
|
||||
return filteredKey;
|
||||
}
|
||||
const cleanedKey = sanitizeForHandleId(filteredKey);
|
||||
|
||||
if (type === HandleIdType.SIMPLE || nestedValues.length === 0) {
|
||||
return fieldKey;
|
||||
if (isObjectProperty) {
|
||||
// "config_api_key" -> "config.api_key"
|
||||
const parts = cleanedKey.split("_");
|
||||
if (parts.length >= 2) {
|
||||
const baseName = parts[0];
|
||||
const propertyName = parts.slice(1).join("_");
|
||||
return `${baseName}.${propertyName}`;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedNestedValues = nestedValues.map((value) =>
|
||||
sanitizeForHandleId(value),
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case HandleIdType.NESTED:
|
||||
return [fieldKey, ...sanitizedNestedValues].join(".");
|
||||
|
||||
case HandleIdType.ARRAY:
|
||||
return [fieldKey, ...sanitizedNestedValues].join("_$_");
|
||||
|
||||
case HandleIdType.KEY_VALUE:
|
||||
return [fieldKey, ...sanitizedNestedValues].join("_#_");
|
||||
|
||||
default:
|
||||
return fieldKey;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseKeyValueHandleId = (
|
||||
handleId: string,
|
||||
type: HandleIdType,
|
||||
): string => {
|
||||
if (type === HandleIdType.KEY_VALUE) {
|
||||
return handleId.split("_#_")[1];
|
||||
} else if (type === HandleIdType.ARRAY) {
|
||||
return handleId.split("_$_")[1];
|
||||
} else if (type === HandleIdType.NESTED) {
|
||||
return handleId.split(".")[1];
|
||||
} else if (type === HandleIdType.SIMPLE) {
|
||||
return handleId.split("_")[1];
|
||||
}
|
||||
return "";
|
||||
return cleanedKey;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutio
|
||||
import { NodeContainer } from "./components/NodeContainer";
|
||||
import { NodeHeader } from "./components/NodeHeader";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle";
|
||||
import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
||||
@@ -99,7 +99,7 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
nodeId={nodeId}
|
||||
uiType={data.uiType}
|
||||
className={cn(
|
||||
"bg-white pr-6",
|
||||
"bg-white px-4",
|
||||
isWebhook && "pointer-events-none opacity-50",
|
||||
)}
|
||||
showHandles={showHandles}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const NodeAdvancedToggle = ({ nodeId }: { nodeId: string }) => {
|
||||
);
|
||||
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 rounded-b-xlarge border-t border-slate-200/50 bg-white px-5 py-3.5">
|
||||
<div className="flex items-center justify-between gap-2 rounded-b-xlarge border-t border-zinc-200 bg-white px-5 py-3.5">
|
||||
<Text variant="body" className="font-medium text-slate-700">
|
||||
Advanced
|
||||
</Text>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const NodeContainer = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-12 max-w-[370px] rounded-xlarge ring-1 ring-slate-200/60",
|
||||
"z-12 w-[350px] rounded-xlarge ring-1 ring-slate-200/60",
|
||||
selected && "shadow-lg ring-2 ring-slate-200",
|
||||
status && nodeStyleBasedOnStatus[status],
|
||||
hasErrors ? nodeStyleBasedOnStatus[AgentExecutionStatus.FAILED] : "",
|
||||
|
||||
@@ -23,7 +23,9 @@ export const NodeHeader = ({
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
const title = (data.metadata?.customized_name as string) || data.title;
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editedTitle, setEditedTitle] = useState(title);
|
||||
const [editedTitle, setEditedTitle] = useState(
|
||||
beautifyString(title).replace("Block", "").trim(),
|
||||
);
|
||||
|
||||
const handleTitleEdit = () => {
|
||||
updateNodeData(nodeId, {
|
||||
@@ -41,7 +43,7 @@ export const NodeHeader = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-auto flex-col gap-1 rounded-xlarge border-b border-slate-200/50 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4 pt-3">
|
||||
<div className="flex h-auto flex-col gap-1 rounded-xlarge border-b border-zinc-200 bg-gradient-to-r from-slate-50/80 to-white/90 px-4 py-4 pt-3">
|
||||
{/* Title row with context menu */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
@@ -68,12 +70,12 @@ export const NodeHeader = ({
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Text variant="large-semibold" className="line-clamp-1">
|
||||
{beautifyString(title)}
|
||||
{beautifyString(title).replace("Block", "").trim()}
|
||||
</Text>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{beautifyString(title)}</p>
|
||||
<p>{beautifyString(title).replace("Block", "").trim()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-b-xl border-t border-slate-200/50 px-4 py-4">
|
||||
<div className="flex flex-col gap-3 rounded-b-xl border-t border-zinc-200 px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text variant="body-medium" className="!font-semibold text-slate-700">
|
||||
Node Output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormCreator } from "../../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/input-renderer/utils/input-schema-pre-processor";
|
||||
import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { uiSchema } from "./uiSchema";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { BlockUIType } from "../../types";
|
||||
import { FormRenderer } from "@/components/renderers/input-renderer/FormRenderer";
|
||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||
|
||||
export const FormCreator = React.memo(
|
||||
({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CaretDownIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
import NodeHandle from "../handlers/NodeHandle";
|
||||
import { OutputNodeHandle } from "../handlers/NodeHandle";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { getTypeDisplayInfo } from "./helpers";
|
||||
import { generateHandleId } from "../handlers/helpers";
|
||||
import { BlockUIType } from "../../types";
|
||||
|
||||
export const OutputHandler = ({
|
||||
@@ -29,8 +28,73 @@ export const OutputHandler = ({
|
||||
const properties = outputSchema?.properties || {};
|
||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||
|
||||
const showHandles = uiType !== BlockUIType.OUTPUT;
|
||||
|
||||
const renderOutputHandles = (
|
||||
schema: RJSFSchema,
|
||||
keyPrefix: string = "",
|
||||
titlePrefix: string = "",
|
||||
): React.ReactNode[] => {
|
||||
return Object.entries(schema).map(
|
||||
([key, fieldSchema]: [string, RJSFSchema]) => {
|
||||
const fullKey = keyPrefix ? `${keyPrefix}_#_${key}` : key;
|
||||
const fieldTitle = titlePrefix + (fieldSchema?.title || key);
|
||||
|
||||
const isConnected = isOutputConnected(nodeId, fullKey);
|
||||
const shouldShow = isConnected || isOutputVisible;
|
||||
const { displayType, colorClass, hexColor } =
|
||||
getTypeDisplayInfo(fieldSchema);
|
||||
|
||||
return shouldShow ? (
|
||||
<div key={fullKey} className="flex flex-col items-end gap-2">
|
||||
<div className="relative flex items-center gap-2">
|
||||
{fieldSchema?.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
style={{ marginLeft: 6, cursor: "pointer" }}
|
||||
aria-label="info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<InfoIcon />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{fieldSchema?.description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Text variant="body" className="text-slate-700">
|
||||
{fieldTitle}
|
||||
</Text>
|
||||
<Text variant="small" as="span" className={colorClass}>
|
||||
({displayType})
|
||||
</Text>
|
||||
|
||||
{showHandles && (
|
||||
<OutputNodeHandle
|
||||
field_name={fullKey}
|
||||
nodeId={nodeId}
|
||||
hexColor={hexColor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recursively render nested properties */}
|
||||
{fieldSchema?.properties &&
|
||||
renderOutputHandles(
|
||||
fieldSchema.properties,
|
||||
fullKey,
|
||||
`${fieldTitle}.`,
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xlarge border-t border-slate-200/50 bg-white py-3.5">
|
||||
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xlarge border-t border-zinc-200 bg-white py-3.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mr-4 h-fit min-w-0 p-0 hover:border-transparent hover:bg-transparent"
|
||||
@@ -49,50 +113,9 @@ export const OutputHandler = ({
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{Object.entries(properties).map(([key, property]: [string, any]) => {
|
||||
const isConnected = isOutputConnected(nodeId, key);
|
||||
const shouldShow = isConnected || isOutputVisible;
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(property);
|
||||
|
||||
return shouldShow ? (
|
||||
<div key={key} className="relative flex items-center gap-2">
|
||||
{property?.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
style={{ marginLeft: 6, cursor: "pointer" }}
|
||||
aria-label="info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<InfoIcon />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{property?.description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Text variant="body" className="text-slate-700">
|
||||
{property?.title || key}{" "}
|
||||
</Text>
|
||||
<Text variant="small" as="span" className={colorClass}>
|
||||
({displayType})
|
||||
</Text>
|
||||
|
||||
<NodeHandle
|
||||
handleId={
|
||||
uiType === BlockUIType.AGENT ? key : generateHandleId(key)
|
||||
}
|
||||
isConnected={isConnected}
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{renderOutputHandles(properties)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -92,14 +92,38 @@ export const getTypeDisplayInfo = (schema: any) => {
|
||||
if (schema?.type === "string" && schema?.format) {
|
||||
const formatMap: Record<
|
||||
string,
|
||||
{ displayType: string; colorClass: string }
|
||||
{ displayType: string; colorClass: string; hexColor: string }
|
||||
> = {
|
||||
file: { displayType: "file", colorClass: "!text-green-500" },
|
||||
date: { displayType: "date", colorClass: "!text-blue-500" },
|
||||
time: { displayType: "time", colorClass: "!text-blue-500" },
|
||||
"date-time": { displayType: "datetime", colorClass: "!text-blue-500" },
|
||||
"long-text": { displayType: "text", colorClass: "!text-green-500" },
|
||||
"short-text": { displayType: "text", colorClass: "!text-green-500" },
|
||||
file: {
|
||||
displayType: "file",
|
||||
colorClass: "!text-green-500",
|
||||
hexColor: "#22c55e",
|
||||
},
|
||||
date: {
|
||||
displayType: "date",
|
||||
colorClass: "!text-blue-500",
|
||||
hexColor: "#3b82f6",
|
||||
},
|
||||
time: {
|
||||
displayType: "time",
|
||||
colorClass: "!text-blue-500",
|
||||
hexColor: "#3b82f6",
|
||||
},
|
||||
"date-time": {
|
||||
displayType: "datetime",
|
||||
colorClass: "!text-blue-500",
|
||||
hexColor: "#3b82f6",
|
||||
},
|
||||
"long-text": {
|
||||
displayType: "text",
|
||||
colorClass: "!text-green-500",
|
||||
hexColor: "#22c55e",
|
||||
},
|
||||
"short-text": {
|
||||
displayType: "text",
|
||||
colorClass: "!text-green-500",
|
||||
hexColor: "#22c55e",
|
||||
},
|
||||
};
|
||||
|
||||
const formatInfo = formatMap[schema.format];
|
||||
@@ -131,10 +155,23 @@ export const getTypeDisplayInfo = (schema: any) => {
|
||||
any: "!text-gray-500",
|
||||
};
|
||||
|
||||
const hexColorMap: Record<string, string> = {
|
||||
string: "#22c55e",
|
||||
number: "#3b82f6",
|
||||
integer: "#3b82f6",
|
||||
boolean: "#eab308",
|
||||
object: "#a855f7",
|
||||
array: "#6366f1",
|
||||
null: "#6b7280",
|
||||
any: "#6b7280",
|
||||
};
|
||||
|
||||
const colorClass = colorMap[schema?.type] || "!text-gray-500";
|
||||
const hexColor = hexColorMap[schema?.type] || "#6b7280";
|
||||
|
||||
return {
|
||||
displayType,
|
||||
colorClass,
|
||||
hexColor,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CustomEdge } from "../components/FlowEditor/edges/CustomEdge";
|
||||
import { customEdgeToLink, linkToCustomEdge } from "../components/helper";
|
||||
import { MarkerType } from "@xyflow/react";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
|
||||
|
||||
type EdgeStore = {
|
||||
edges: CustomEdge[];
|
||||
@@ -13,6 +14,8 @@ type EdgeStore = {
|
||||
removeEdge: (edgeId: string) => void;
|
||||
upsertMany: (edges: CustomEdge[]) => void;
|
||||
|
||||
removeEdgesByHandlePrefix: (nodeId: string, handlePrefix: string) => void;
|
||||
|
||||
getNodeEdges: (nodeId: string) => CustomEdge[];
|
||||
isInputConnected: (nodeId: string, handle: string) => boolean;
|
||||
isOutputConnected: (nodeId: string, handle: string) => boolean;
|
||||
@@ -79,11 +82,27 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
return { edges: Array.from(byKey.values()) };
|
||||
}),
|
||||
|
||||
removeEdgesByHandlePrefix: (nodeId, handlePrefix) =>
|
||||
set((state) => ({
|
||||
edges: state.edges.filter(
|
||||
(e) =>
|
||||
!(
|
||||
e.target === nodeId &&
|
||||
e.targetHandle &&
|
||||
e.targetHandle.startsWith(handlePrefix)
|
||||
),
|
||||
),
|
||||
})),
|
||||
|
||||
getNodeEdges: (nodeId) =>
|
||||
get().edges.filter((e) => e.source === nodeId || e.target === nodeId),
|
||||
|
||||
isInputConnected: (nodeId, handle) =>
|
||||
get().edges.some((e) => e.target === nodeId && e.targetHandle === handle),
|
||||
isInputConnected: (nodeId, handle) => {
|
||||
const cleanedHandle = cleanUpHandleId(handle);
|
||||
return get().edges.some(
|
||||
(e) => e.target === nodeId && e.targetHandle === cleanedHandle,
|
||||
);
|
||||
},
|
||||
|
||||
isOutputConnected: (nodeId, handle) =>
|
||||
get().edges.some((e) => e.source === nodeId && e.sourceHandle === handle),
|
||||
@@ -105,15 +124,15 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
targetNodeId: string,
|
||||
executionResult: NodeExecutionResult,
|
||||
) => {
|
||||
set((state) => ({
|
||||
edges: state.edges.map((edge) => {
|
||||
set((state) => {
|
||||
let hasChanges = false;
|
||||
|
||||
const newEdges = state.edges.map((edge) => {
|
||||
if (edge.target !== targetNodeId) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
const beadData =
|
||||
edge.data?.beadData ??
|
||||
new Map<string, NodeExecutionResult["status"]>();
|
||||
const beadData = new Map(edge.data?.beadData ?? new Map());
|
||||
|
||||
const inputValue = edge.targetHandle
|
||||
? executionResult.input_data[edge.targetHandle]
|
||||
@@ -137,6 +156,11 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
beadUp = beadDown + 1;
|
||||
}
|
||||
|
||||
if (edge.data?.beadUp === beadUp && edge.data?.beadDown === beadDown) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
hasChanges = true;
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
@@ -146,8 +170,10 @@ export const useEdgeStore = create<EdgeStore>((set, get) => ({
|
||||
beadData,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
return hasChanges ? { edges: newEdges } : state;
|
||||
});
|
||||
},
|
||||
|
||||
resetEdgeBeads: () => {
|
||||
|
||||
@@ -13,6 +13,10 @@ import { useHistoryStore } from "./historyStore";
|
||||
import { useEdgeStore } from "./edgeStore";
|
||||
import { BlockUIType } from "../components/types";
|
||||
import { pruneEmptyValues } from "@/lib/utils";
|
||||
import {
|
||||
ensurePathExists,
|
||||
parseHandleIdToPath,
|
||||
} from "@/components/renderers/InputRenderer/helpers";
|
||||
|
||||
// Minimum movement (in pixels) required before logging position change to history
|
||||
// Prevents spamming history with small movements when clicking on inputs inside blocks
|
||||
@@ -62,6 +66,8 @@ type NodeStore = {
|
||||
errors: { [key: string]: string },
|
||||
) => void;
|
||||
clearAllNodeErrors: () => void; // Add this
|
||||
|
||||
syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
|
||||
};
|
||||
|
||||
export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
@@ -305,4 +311,35 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
syncHardcodedValuesWithHandleIds: (nodeId: string) => {
|
||||
const node = get().nodes.find((n) => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const handleIds = useEdgeStore.getState().getAllHandleIdsOfANode(nodeId);
|
||||
const additionalHandles = handleIds.filter((h) => h.includes("_#_"));
|
||||
|
||||
if (additionalHandles.length === 0) return;
|
||||
|
||||
const hardcodedValues = JSON.parse(
|
||||
JSON.stringify(node.data.hardcodedValues || {}),
|
||||
);
|
||||
|
||||
let modified = false;
|
||||
|
||||
additionalHandles.forEach((handleId) => {
|
||||
const segments = parseHandleIdToPath(handleId);
|
||||
if (ensurePathExists(hardcodedValues, segments)) {
|
||||
modified = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, hardcodedValues } } : n,
|
||||
),
|
||||
}));
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -143,6 +143,7 @@ export function CredentialsInput({
|
||||
size="small"
|
||||
onClick={handleActionButtonClick}
|
||||
className="w-fit"
|
||||
type="button"
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
@@ -155,6 +156,7 @@ export function CredentialsInput({
|
||||
size="small"
|
||||
onClick={handleActionButtonClick}
|
||||
className="w-fit"
|
||||
type="button"
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { providerIcons } from "@/components/renderers/input-renderer/fields/CredentialField/helpers";
|
||||
import { providerIcons } from "@/components/renderers/InputRenderer/custom/CredentialField/helpers";
|
||||
import { CredentialsProviderName } from "@/lib/autogpt-server-api";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||
|
||||
@@ -49,6 +49,7 @@ export function GoogleDrivePicker(props: Props) {
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={handleOpenPicker}
|
||||
disabled={props.disabled || isLoading || isAuthInProgress}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import React, { useCallback } from "react";
|
||||
import { GoogleDrivePicker } from "./GoogleDrivePicker";
|
||||
import { isValidFile } from "./helpers";
|
||||
|
||||
export interface Props {
|
||||
config: GoogleDrivePickerConfig;
|
||||
@@ -27,13 +28,15 @@ export function GoogleDrivePickerInput({
|
||||
const hasAutoCredentials = !!config.auto_credentials;
|
||||
|
||||
// Strip _credentials_id from value for display purposes
|
||||
const currentFiles = isMultiSelect
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: []
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
// Only show files section when there are valid file objects
|
||||
const currentFiles = React.useMemo(() => {
|
||||
if (isMultiSelect) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter(isValidFile);
|
||||
}
|
||||
if (!value || !isValidFile(value)) return [];
|
||||
return [value];
|
||||
}, [value, isMultiSelect]);
|
||||
|
||||
const handlePicked = useCallback(
|
||||
(files: any[], credentialId?: string) => {
|
||||
@@ -85,23 +88,27 @@ export function GoogleDrivePickerInput({
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
{/* Picker Button */}
|
||||
<GoogleDrivePicker
|
||||
multiselect={config.multiselect || false}
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={config.scopes || ["https://www.googleapis.com/auth/drive.file"]}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
{/* Picker Button */}
|
||||
<GoogleDrivePicker
|
||||
multiselect={config.multiselect || false}
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={
|
||||
config.scopes || ["https://www.googleapis.com/auth/drive.file"]
|
||||
}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
}}
|
||||
onError={handleError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Selected Files */}
|
||||
{currentFiles.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="mb-8 space-y-1">
|
||||
{currentFiles.map((file: any, idx: number) => (
|
||||
<div
|
||||
key={file.id || idx}
|
||||
|
||||
@@ -119,3 +119,14 @@ export function getCredentialsSchema(scopes: string[]) {
|
||||
secret: true,
|
||||
} satisfies BlockIOCredentialsSubSchema;
|
||||
}
|
||||
|
||||
export function isValidFile(
|
||||
file: unknown,
|
||||
): file is { id?: string; name?: string } {
|
||||
return (
|
||||
typeof file === "object" &&
|
||||
file !== null &&
|
||||
(typeof (file as { id?: unknown }).id === "string" ||
|
||||
typeof (file as { name?: unknown }).name === "string")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { BlockUIType } from "@/app/(platform)/build/components/types";
|
||||
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";
|
||||
import { customValidator } from "./utils/custom-validator";
|
||||
|
||||
type FormContextType = {
|
||||
nodeId?: string;
|
||||
uiType?: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
};
|
||||
import Form from "./registry";
|
||||
import { ExtendedFormContextType } from "./types";
|
||||
|
||||
type FormRendererProps = {
|
||||
jsonSchema: RJSFSchema;
|
||||
handleChange: (formData: any) => void;
|
||||
uiSchema: any;
|
||||
initialValues: any;
|
||||
formContext: FormContextType;
|
||||
formContext: ExtendedFormContextType;
|
||||
};
|
||||
|
||||
export const FormRenderer = ({
|
||||
@@ -33,19 +23,18 @@ export const FormRenderer = ({
|
||||
const preprocessedSchema = useMemo(() => {
|
||||
return preprocessInputSchema(jsonSchema);
|
||||
}, [jsonSchema]);
|
||||
|
||||
return (
|
||||
<div className={"mt-4"}>
|
||||
<div className={"mb-6 mt-4"}>
|
||||
<Form
|
||||
formContext={formContext}
|
||||
idPrefix="agpt"
|
||||
idSeparator="_%_"
|
||||
schema={preprocessedSchema}
|
||||
validator={customValidator}
|
||||
fields={fields}
|
||||
templates={templates}
|
||||
widgets={widgets}
|
||||
formContext={formContext}
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
noValidate={true}
|
||||
liveValidate={false}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { FieldProps, getUiOptions, getWidget } from "@rjsf/utils";
|
||||
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
|
||||
import { isEmpty } from "lodash";
|
||||
import { useAnyOfField } from "./useAnyOfField";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { ANY_OF_FLAG } from "../../constants";
|
||||
|
||||
export const AnyOfField = (props: FieldProps) => {
|
||||
const { registry, schema } = props;
|
||||
const { fields } = registry;
|
||||
const { SchemaField: _SchemaField } = fields;
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
|
||||
const uiOptions = getUiOptions(props.uiSchema, props.globalUiOptions);
|
||||
|
||||
const Widget = getWidget({ type: "string" }, "select", registry.widgets);
|
||||
|
||||
const {
|
||||
handleOptionChange,
|
||||
enumOptions,
|
||||
selectedOption,
|
||||
optionSchema,
|
||||
field_id,
|
||||
} = useAnyOfField(props);
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: field_id + ANY_OF_FLAG,
|
||||
schema: schema,
|
||||
});
|
||||
|
||||
const updatedUiSchema = updateUiOption(props.uiSchema, {
|
||||
handleId: handleId,
|
||||
label: false,
|
||||
fromAnyOf: true,
|
||||
});
|
||||
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
const optionsSchemaField =
|
||||
(optionSchema && optionSchema.type !== "null" && (
|
||||
<_SchemaField
|
||||
{...props}
|
||||
schema={optionSchema}
|
||||
uiSchema={updatedUiSchema}
|
||||
/>
|
||||
)) ||
|
||||
null;
|
||||
|
||||
const selector = (
|
||||
<Widget
|
||||
id={field_id}
|
||||
name={`${props.name}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`}
|
||||
schema={{ type: "number", default: 0 }}
|
||||
onChange={handleOptionChange}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
disabled={props.disabled || isEmpty(enumOptions)}
|
||||
multiple={false}
|
||||
value={selectedOption >= 0 ? selectedOption : undefined}
|
||||
options={{ enumOptions }}
|
||||
registry={registry}
|
||||
placeholder={props.placeholder}
|
||||
autocomplete={props.autocomplete}
|
||||
className="-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium"
|
||||
autofocus={props.autofocus}
|
||||
label=""
|
||||
hideLabel={true}
|
||||
readonly={props.readonly}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnyOfFieldTitle
|
||||
{...props}
|
||||
selector={selector}
|
||||
uiSchema={updatedUiSchema}
|
||||
/>
|
||||
{!isHandleConnected && optionsSchemaField}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
descriptionId,
|
||||
FieldProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
import { shouldShowTypeSelector } from "../helpers";
|
||||
import { useIsArrayItem } from "../../array/context/array-item-context";
|
||||
import { cleanUpHandleId } from "../../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { isOptionalType } from "../../../utils/schema-utils";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface customFieldProps extends FieldProps {
|
||||
selector: JSX.Element;
|
||||
}
|
||||
|
||||
export const AnyOfFieldTitle = (props: customFieldProps) => {
|
||||
const { uiSchema, schema, required, name, registry, fieldPathId, selector } =
|
||||
props;
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const title_id = titleId(fieldPathId ?? "");
|
||||
const description_id = descriptionId(fieldPathId ?? "");
|
||||
|
||||
const isArrayItem = useIsArrayItem();
|
||||
|
||||
const handleId = cleanUpHandleId(uiOptions.handleId);
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
const { isOptional, type } = isOptionalType(schema); // If we have something like int | null = we will treat it as optional int
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(type);
|
||||
|
||||
const shouldShowSelector =
|
||||
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
|
||||
const shoudlShowType = isHandleConnected || (isOptional && type);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TitleFieldTemplate
|
||||
id={title_id}
|
||||
title={schema.title || name || ""}
|
||||
required={required}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
{shoudlShowType && (
|
||||
<Text variant="small" className={cn("text-zinc-700", colorClass)}>
|
||||
{isOptional ? `(${displayType})` : "(any)"}
|
||||
</Text>
|
||||
)}
|
||||
{shouldShowSelector && selector}
|
||||
<DescriptionFieldTemplate
|
||||
id={description_id}
|
||||
description={schema.description || ""}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { RJSFSchema, StrictRJSFSchema } from "@rjsf/utils";
|
||||
|
||||
const TYPE_PRIORITY = [
|
||||
"string",
|
||||
"number",
|
||||
"integer",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
] as const;
|
||||
|
||||
export function getDefaultTypeIndex(options: StrictRJSFSchema[]): number {
|
||||
for (const preferredType of TYPE_PRIORITY) {
|
||||
const index = options.findIndex((opt) => opt.type === preferredType);
|
||||
if (index >= 0) return index;
|
||||
}
|
||||
|
||||
const nonNullIndex = options.findIndex((opt) => opt.type !== "null");
|
||||
return nonNullIndex >= 0 ? nonNullIndex : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a type selector should be shown for an anyOf schema
|
||||
* Returns false for simple optional types (type | null)
|
||||
* Returns true for complex anyOf (3+ types or multiple non-null types)
|
||||
*/
|
||||
export function shouldShowTypeSelector(
|
||||
schema: RJSFSchema | undefined,
|
||||
): boolean {
|
||||
const anyOf = schema?.anyOf;
|
||||
if (!anyOf || !Array.isArray(anyOf) || anyOf.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anyOf.length === 2 && anyOf.some((opt: any) => opt.type === "null")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return anyOf.length >= 3;
|
||||
}
|
||||
|
||||
export function isSimpleOptional(schema: RJSFSchema | undefined): boolean {
|
||||
const anyOf = schema?.anyOf;
|
||||
return (
|
||||
Array.isArray(anyOf) &&
|
||||
anyOf.length === 2 &&
|
||||
anyOf.some((opt: any) => opt.type === "null")
|
||||
);
|
||||
}
|
||||
|
||||
export function getOptionalType(
|
||||
schema: RJSFSchema | undefined,
|
||||
): string | undefined {
|
||||
if (!isSimpleOptional(schema)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const anyOf = schema?.anyOf;
|
||||
const nonNullOption = anyOf?.find((opt: any) => opt.type !== "null");
|
||||
return nonNullOption ? (nonNullOption as any).type : undefined;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { FieldProps, getFirstMatchingOption, mergeSchemas } from "@rjsf/utils";
|
||||
import { useRef, useState } from "react";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { getDefaultTypeIndex } from "./helpers";
|
||||
import { cleanUpHandleId } from "../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
|
||||
export const useAnyOfField = (props: FieldProps) => {
|
||||
const { registry, schema, options, onChange, formData } = props;
|
||||
const { schemaUtils } = registry;
|
||||
|
||||
const getInitialOption = () => {
|
||||
if (formData !== undefined && formData !== null) {
|
||||
const option = getFirstMatchingOption(
|
||||
validator,
|
||||
formData,
|
||||
options,
|
||||
schema,
|
||||
);
|
||||
return option !== undefined ? option : getDefaultTypeIndex(options);
|
||||
}
|
||||
return getDefaultTypeIndex(options);
|
||||
};
|
||||
|
||||
const [selectedOption, setSelectedOption] =
|
||||
useState<number>(getInitialOption());
|
||||
const retrievedOptions = useRef<any[]>(
|
||||
options.map((opt: any) => schemaUtils.retrieveSchema(opt, formData)),
|
||||
);
|
||||
|
||||
const option =
|
||||
selectedOption >= 0
|
||||
? retrievedOptions.current[selectedOption] || null
|
||||
: null;
|
||||
let optionSchema: any | undefined | null;
|
||||
|
||||
// adding top level required to each option schema
|
||||
if (option) {
|
||||
const { required } = schema;
|
||||
optionSchema = required
|
||||
? (mergeSchemas({ required }, option) as any)
|
||||
: option;
|
||||
}
|
||||
|
||||
const field_id = props.fieldPathId.$id;
|
||||
|
||||
const handleOptionChange = (option?: string) => {
|
||||
const intOption = option !== undefined ? parseInt(option, 10) : -1;
|
||||
if (intOption === selectedOption) return;
|
||||
|
||||
const newOption =
|
||||
intOption >= 0 ? retrievedOptions.current[intOption] : undefined;
|
||||
const oldOption =
|
||||
selectedOption >= 0
|
||||
? retrievedOptions.current[selectedOption]
|
||||
: undefined;
|
||||
|
||||
// When we change the option, we need to clean the form data
|
||||
let newFormData = schemaUtils.sanitizeDataForNewSchema(
|
||||
newOption,
|
||||
oldOption,
|
||||
formData,
|
||||
);
|
||||
|
||||
const handlePrefix = cleanUpHandleId(field_id);
|
||||
console.log("handlePrefix", handlePrefix);
|
||||
useEdgeStore
|
||||
.getState()
|
||||
.removeEdgesByHandlePrefix(registry.formContext.nodeId, handlePrefix);
|
||||
|
||||
// We have cleaned the form data, now we need to get the default form state of new selected option
|
||||
if (newOption) {
|
||||
newFormData = schemaUtils.getDefaultFormState(
|
||||
newOption,
|
||||
newFormData,
|
||||
"excludeObjectChildren",
|
||||
) as any;
|
||||
}
|
||||
|
||||
setSelectedOption(intOption);
|
||||
onChange(newFormData, props.fieldPathId.path, undefined, field_id);
|
||||
};
|
||||
|
||||
const enumOptions = retrievedOptions.current.map((option, index) => ({
|
||||
value: index,
|
||||
label: option.type,
|
||||
}));
|
||||
|
||||
return {
|
||||
handleOptionChange,
|
||||
enumOptions,
|
||||
selectedOption,
|
||||
optionSchema,
|
||||
field_id,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
ArrayFieldItemTemplateProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
export default function ArrayFieldItemTemplate(
|
||||
props: ArrayFieldItemTemplateProps,
|
||||
) {
|
||||
const { children, buttonsProps, hasToolbar, uiSchema, registry } = props;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const ArrayFieldItemButtonsTemplate = getTemplate(
|
||||
"ArrayFieldItemButtonsTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex flex-row flex-wrap items-center">
|
||||
<div className="shrink grow">
|
||||
<div className="shrink grow">{children}</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
{hasToolbar && (
|
||||
<div className="-mt-4 mb-2 flex gap-2">
|
||||
<ArrayFieldItemButtonsTemplate {...buttonsProps} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
ArrayFieldTemplateProps,
|
||||
buttonId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
} from "@rjsf/utils";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
|
||||
export default function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
const {
|
||||
canAdd,
|
||||
disabled,
|
||||
fieldPathId,
|
||||
uiSchema,
|
||||
items,
|
||||
optionalDataControl,
|
||||
onAddClick,
|
||||
readonly,
|
||||
registry,
|
||||
required,
|
||||
schema,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const ArrayFieldDescriptionTemplate = getTemplate(
|
||||
"ArrayFieldDescriptionTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const ArrayFieldTitleTemplate = getTemplate(
|
||||
"ArrayFieldTitleTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const showOptionalDataControlInTitle = !readonly && !disabled;
|
||||
|
||||
const {
|
||||
ButtonTemplates: { AddButton },
|
||||
} = registry.templates;
|
||||
|
||||
const { fromAnyOf } = uiOptions;
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldPathId.$id,
|
||||
schema: schema,
|
||||
});
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="m-0 flex p-0">
|
||||
<div className="m-0 w-full space-y-4 p-0">
|
||||
{!fromAnyOf && (
|
||||
<div className="flex items-center">
|
||||
<ArrayFieldTitleTemplate
|
||||
fieldPathId={fieldPathId}
|
||||
title={uiOptions.title || title}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
required={required}
|
||||
registry={registry}
|
||||
optionalDataControl={
|
||||
showOptionalDataControlInTitle
|
||||
? optionalDataControl
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ArrayFieldDescriptionTemplate
|
||||
fieldPathId={fieldPathId}
|
||||
description={uiOptions.description || schema.description}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
key={`array-item-list-${fieldPathId.$id}`}
|
||||
className="m-0 mb-2 w-full p-0"
|
||||
>
|
||||
{!showOptionalDataControlInTitle ? optionalDataControl : undefined}
|
||||
{items}
|
||||
{canAdd && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<AddButton
|
||||
id={buttonId(fieldPathId, "add")}
|
||||
className="rjsf-array-item-add"
|
||||
onClick={onAddClick}
|
||||
disabled={disabled || readonly}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FieldProps, getUiOptions } from "@rjsf/utils";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import { ARRAY_ITEM_FLAG } from "../../constants";
|
||||
|
||||
const ArraySchemaField = (props: FieldProps) => {
|
||||
const { index, registry, fieldPathId } = props;
|
||||
const { SchemaField } = registry.fields;
|
||||
|
||||
const uiOptions = getUiOptions(props.uiSchema);
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldPathId.$id,
|
||||
schema: props.schema,
|
||||
});
|
||||
const updatedUiSchema = updateUiOption(props.uiSchema, {
|
||||
handleId: handleId + ARRAY_ITEM_FLAG,
|
||||
});
|
||||
|
||||
return (
|
||||
<SchemaField
|
||||
{...props}
|
||||
uiSchema={updatedUiSchema}
|
||||
title={"_item-" + index.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArraySchemaField;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
interface ArrayItemContextValue {
|
||||
isArrayItem: boolean;
|
||||
arrayItemHandleId: string;
|
||||
}
|
||||
|
||||
const ArrayItemContext = createContext<ArrayItemContextValue>({
|
||||
isArrayItem: false,
|
||||
arrayItemHandleId: "",
|
||||
});
|
||||
|
||||
export const ArrayItemProvider: React.FC<{
|
||||
children: React.ReactNode;
|
||||
arrayItemHandleId: string;
|
||||
}> = ({ children, arrayItemHandleId }) => {
|
||||
return (
|
||||
<ArrayItemContext.Provider value={{ isArrayItem: true, arrayItemHandleId }}>
|
||||
{children}
|
||||
</ArrayItemContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useIsArrayItem = (): boolean => {
|
||||
// here this will be true if field is inside an array
|
||||
const context = useContext(ArrayItemContext);
|
||||
return context.isArrayItem;
|
||||
};
|
||||
|
||||
export const useArrayItemHandleId = (): string => {
|
||||
const context = useContext(ArrayItemContext);
|
||||
return context.arrayItemHandleId;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const generateArrayItemHandleId = (id: string) => {
|
||||
return `array-item-${id}`;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as ArrayFieldTemplate } from "./ArrayFieldTemplate";
|
||||
export { default as ArrayFieldItemTemplate } from "./ArrayFieldItemTemplate";
|
||||
export { default as ArraySchemaField } from "./ArraySchemaField";
|
||||
export {
|
||||
ArrayItemProvider,
|
||||
useIsArrayItem,
|
||||
} from "./context/array-item-context";
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
RegistryFieldsType,
|
||||
RegistryWidgetsType,
|
||||
TemplatesType,
|
||||
} from "@rjsf/utils";
|
||||
import { AnyOfField } from "./anyof/AnyOfField";
|
||||
import {
|
||||
ArrayFieldItemTemplate,
|
||||
ArrayFieldTemplate,
|
||||
ArraySchemaField,
|
||||
} from "./array";
|
||||
import {
|
||||
ObjectFieldTemplate,
|
||||
OptionalDataControlsTemplate,
|
||||
WrapIfAdditionalTemplate,
|
||||
} from "./object";
|
||||
import { DescriptionField, FieldTemplate, TitleField } from "./standard";
|
||||
import { AddButton, CopyButton, RemoveButton } from "./standard/buttons";
|
||||
import {
|
||||
CheckboxWidget,
|
||||
DateTimeWidget,
|
||||
DateWidget,
|
||||
FileWidget,
|
||||
GoogleDrivePickerWidget,
|
||||
SelectWidget,
|
||||
TextWidget,
|
||||
TimeWidget,
|
||||
} from "./standard/widgets";
|
||||
|
||||
const NoButton = () => null;
|
||||
|
||||
export function generateBaseFields(): RegistryFieldsType {
|
||||
return {
|
||||
AnyOfField,
|
||||
ArraySchemaField,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBaseTemplates(): Partial<TemplatesType> {
|
||||
return {
|
||||
ArrayFieldItemTemplate,
|
||||
ArrayFieldTemplate,
|
||||
ButtonTemplates: {
|
||||
AddButton,
|
||||
CopyButton,
|
||||
MoveDownButton: NoButton,
|
||||
MoveUpButton: NoButton,
|
||||
RemoveButton,
|
||||
SubmitButton: NoButton,
|
||||
},
|
||||
DescriptionFieldTemplate: DescriptionField,
|
||||
FieldTemplate,
|
||||
ObjectFieldTemplate,
|
||||
OptionalDataControlsTemplate,
|
||||
TitleFieldTemplate: TitleField,
|
||||
WrapIfAdditionalTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBaseWidgets(): RegistryWidgetsType {
|
||||
return {
|
||||
TextWidget,
|
||||
SelectWidget,
|
||||
CheckboxWidget,
|
||||
FileWidget,
|
||||
DateWidget,
|
||||
TimeWidget,
|
||||
DateTimeWidget,
|
||||
GoogleDrivePickerWidget,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./array";
|
||||
export * from "./object";
|
||||
export * from "./standard";
|
||||
export * from "./standard/widgets";
|
||||
export * from "./standard/buttons";
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
buttonId,
|
||||
canExpand,
|
||||
descriptionId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
ObjectFieldTemplateProps,
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
import { getHandleId, updateUiOption } from "../../helpers";
|
||||
import React from "react";
|
||||
|
||||
export default function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
const {
|
||||
description,
|
||||
title,
|
||||
properties,
|
||||
required,
|
||||
uiSchema,
|
||||
fieldPathId,
|
||||
schema,
|
||||
formData,
|
||||
optionalDataControl,
|
||||
onAddProperty,
|
||||
disabled,
|
||||
readonly,
|
||||
registry,
|
||||
} = props;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const showOptionalDataControlInTitle = !readonly && !disabled;
|
||||
|
||||
const {
|
||||
ButtonTemplates: { AddButton },
|
||||
} = registry.templates;
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: fieldPathId.$id,
|
||||
schema,
|
||||
});
|
||||
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
{title && !additional && (
|
||||
<TitleFieldTemplate
|
||||
id={titleId(fieldPathId)}
|
||||
title={title}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
optionalDataControl={true ? optionalDataControl : undefined}
|
||||
/>
|
||||
)}
|
||||
{description && (
|
||||
<DescriptionFieldTemplate
|
||||
id={descriptionId(fieldPathId)}
|
||||
description={description}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{!showOptionalDataControlInTitle ? optionalDataControl : undefined}
|
||||
|
||||
{/* I have cloned it - so i could pass updated uiSchema to the nested children */}
|
||||
{properties.map((element: any, index: number) => {
|
||||
const clonedContent = React.cloneElement(element.content, {
|
||||
...element.content.props,
|
||||
uiSchema: updateUiOption(element.content.props.uiSchema, {
|
||||
handleId: handleId,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`${element.hidden ? "hidden" : ""} flex`}
|
||||
>
|
||||
<div className="w-full">{clonedContent}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{canExpand(schema, uiSchema, formData) ? (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<AddButton
|
||||
id={buttonId(fieldPathId, "add")}
|
||||
onClick={onAddProperty}
|
||||
disabled={disabled || readonly}
|
||||
className="rjsf-object-property-expand"
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { OptionalDataControlsTemplateProps } from "@rjsf/utils";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
|
||||
import { IconButton, RemoveButton } from "../standard/buttons";
|
||||
|
||||
export default function OptionalDataControlsTemplate(
|
||||
props: OptionalDataControlsTemplateProps,
|
||||
) {
|
||||
const { id, registry, label, onAddClick, onRemoveClick } = props;
|
||||
if (onAddClick) {
|
||||
return (
|
||||
<IconButton
|
||||
id={id}
|
||||
registry={registry}
|
||||
className="rjsf-add-optional-data"
|
||||
onClick={onAddClick}
|
||||
title={label}
|
||||
icon={<PlusCircle />}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
} else if (onRemoveClick) {
|
||||
return (
|
||||
<RemoveButton
|
||||
id={id}
|
||||
registry={registry}
|
||||
className="rjsf-remove-optional-data"
|
||||
onClick={onRemoveClick}
|
||||
title={label}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <em id={id}>{label}</em>;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
buttonId,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId,
|
||||
WrapIfAdditionalTemplateProps,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
|
||||
export default function WrapIfAdditionalTemplate(
|
||||
props: WrapIfAdditionalTemplateProps,
|
||||
) {
|
||||
const {
|
||||
classNames,
|
||||
style,
|
||||
children,
|
||||
disabled,
|
||||
id,
|
||||
label,
|
||||
onRemoveProperty,
|
||||
onKeyRenameBlur,
|
||||
readonly,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
registry,
|
||||
} = props;
|
||||
const { templates, formContext } = registry;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
// Button templates are not overridden in the uiSchema
|
||||
const { RemoveButton } = templates.ButtonTemplates;
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
const { nodeId } = formContext;
|
||||
const handleId = uiOptions.handleId;
|
||||
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
if (!additional) {
|
||||
return (
|
||||
<div className={classNames} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const keyId = `${id}-key`;
|
||||
const generateObjectPropertyTitleId = (id: string, label: string) => {
|
||||
return id.replace(`_${label}`, `_#_${label}`);
|
||||
};
|
||||
const title_id = generateObjectPropertyTitleId(id, label);
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (e.target.value == "") {
|
||||
onRemoveProperty();
|
||||
} else {
|
||||
onKeyRenameBlur(e);
|
||||
}
|
||||
};
|
||||
|
||||
const isHandleConnected = isInputConnected(nodeId, handleId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`mb-4 flex flex-col gap-1`} style={style}>
|
||||
<TitleFieldTemplate
|
||||
id={titleId(title_id)}
|
||||
title={`#${label}`}
|
||||
required={required}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
{!isHandleConnected && (
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<Input
|
||||
label={""}
|
||||
hideLabel={true}
|
||||
required={required}
|
||||
defaultValue={label}
|
||||
disabled={disabled || readonly}
|
||||
id={keyId}
|
||||
wrapperClassName="mb-2 w-30"
|
||||
name={keyId}
|
||||
onBlur={!readonly ? handleBlur : undefined}
|
||||
type="text"
|
||||
size="small"
|
||||
/>
|
||||
<div className="mt-2"> {children}</div>
|
||||
</div>
|
||||
)}
|
||||
{!isHandleConnected && (
|
||||
<div className="-mt-4">
|
||||
<RemoveButton
|
||||
id={buttonId(id, "remove")}
|
||||
disabled={disabled || readonly}
|
||||
onClick={onRemoveProperty}
|
||||
uiSchema={uiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as ObjectFieldTemplate } from "./ObjectFieldTemplate";
|
||||
export { default as WrapIfAdditionalTemplate } from "./WrapIfAdditionalTemplate";
|
||||
export { default as OptionalDataControlsTemplate } from "./OptionalDataControlsTemplate";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DescriptionFieldProps } from "@rjsf/utils";
|
||||
import { RichDescription } from "@rjsf/core";
|
||||
import { InfoIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
|
||||
export default function DescriptionField(props: DescriptionFieldProps) {
|
||||
const { id, description, registry, uiSchema } = props;
|
||||
if (!description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={id} className="0 inline w-fit">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon size={16} className="cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<RichDescription
|
||||
description={description}
|
||||
registry={registry}
|
||||
uiSchema={uiSchema}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export const FieldError = ({
|
||||
nodeId,
|
||||
fieldId,
|
||||
}: {
|
||||
nodeId: string;
|
||||
fieldId: string;
|
||||
}) => {
|
||||
const nodeErrors = useNodeStore((state) => {
|
||||
const node = state.nodes.find((n) => n.id === nodeId);
|
||||
return node?.data?.errors;
|
||||
});
|
||||
const fieldError =
|
||||
nodeErrors?.[fieldId] || nodeErrors?.[fieldId.replace(/_%_/g, ".")] || null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fieldError && (
|
||||
<Text variant="small" className="mt-1 pl-4 !text-red-600">
|
||||
{fieldError}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
FieldTemplateProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { isAnyOfChild, isAnyOfSchema } from "../../utils/schema-utils";
|
||||
import {
|
||||
cleanUpHandleId,
|
||||
getHandleId,
|
||||
isPartOfAnyOf,
|
||||
updateUiOption,
|
||||
} from "../../helpers";
|
||||
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { FieldError } from "./FieldError";
|
||||
|
||||
export default function FieldTemplate(props: FieldTemplateProps) {
|
||||
const {
|
||||
id,
|
||||
children,
|
||||
displayLabel,
|
||||
description,
|
||||
rawDescription,
|
||||
label,
|
||||
hidden,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
registry,
|
||||
classNames,
|
||||
style,
|
||||
disabled,
|
||||
onKeyRename,
|
||||
onKeyRenameBlur,
|
||||
onRemoveProperty,
|
||||
readonly,
|
||||
} = props;
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const { isInputConnected } = useEdgeStore();
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[registry.formContext.nodeId ?? ""],
|
||||
);
|
||||
|
||||
if (hidden) {
|
||||
return <div className="hidden">{children}</div>;
|
||||
}
|
||||
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const WrapIfAdditionalTemplate = getTemplate(
|
||||
"WrapIfAdditionalTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: id,
|
||||
schema: schema,
|
||||
});
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
handleId: handleId,
|
||||
});
|
||||
const isHandleConnected = isInputConnected(nodeId, cleanUpHandleId(handleId));
|
||||
|
||||
const shouldDisplayLabel =
|
||||
displayLabel ||
|
||||
(schema.type === "boolean" && !isAnyOfChild(uiSchema as any));
|
||||
const shouldShowTitleSection = !isAnyOfSchema(schema) && !additional;
|
||||
const shouldShowChildren = isAnyOfSchema(schema) || !isHandleConnected;
|
||||
|
||||
const isAdvancedField = (schema as any).advanced === true;
|
||||
if (!showAdvanced && isAdvancedField && !isHandleConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marginBottom =
|
||||
isPartOfAnyOf({ uiOptions }) || isAnyOfSchema(schema) ? 0 : 16;
|
||||
|
||||
return (
|
||||
<WrapIfAdditionalTemplate
|
||||
classNames={classNames}
|
||||
style={style}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
label={label}
|
||||
displayLabel={displayLabel}
|
||||
onKeyRename={onKeyRename}
|
||||
onKeyRenameBlur={onKeyRenameBlur}
|
||||
onRemoveProperty={onRemoveProperty}
|
||||
rawDescription={rawDescription}
|
||||
readonly={readonly}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
>
|
||||
<div className="flex flex-col gap-2" style={{ marginBottom }}>
|
||||
{shouldShowTitleSection && (
|
||||
<div className="flex items-center gap-2">
|
||||
{shouldDisplayLabel && (
|
||||
<TitleFieldTemplate
|
||||
id={titleId(id)}
|
||||
title={label}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={updatedUiSchema}
|
||||
registry={registry}
|
||||
/>
|
||||
)}
|
||||
{shouldDisplayLabel && rawDescription && <span>{description}</span>}
|
||||
</div>
|
||||
)}
|
||||
{shouldShowChildren && children}
|
||||
|
||||
<FieldError nodeId={nodeId} fieldId={cleanUpHandleId(id)} />
|
||||
</div>
|
||||
</WrapIfAdditionalTemplate>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
descriptionId,
|
||||
getUiOptions,
|
||||
TitleFieldProps,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { isAnyOfSchema } from "../../utils/schema-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isArrayItem } from "../../helpers";
|
||||
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||
|
||||
export default function TitleField(props: TitleFieldProps) {
|
||||
const { id, title, required, schema, registry, uiSchema } = props;
|
||||
const { nodeId, showHandles } = registry.formContext;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const isAnyOf = isAnyOfSchema(schema);
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(schema);
|
||||
const description_id = descriptionId(id);
|
||||
|
||||
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
const isArrayItemFlag = isArrayItem({ uiOptions });
|
||||
const smallText = isArrayItemFlag || additional;
|
||||
|
||||
const showHandle = uiOptions.showHandles ?? showHandles;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{showHandle !== false && (
|
||||
<InputNodeHandle handleId={uiOptions.handleId} nodeId={nodeId} />
|
||||
)}
|
||||
<Text
|
||||
variant={isArrayItemFlag ? "small" : "body"}
|
||||
id={id}
|
||||
className={cn("line-clamp-1", smallText && "text-sm text-zinc-700")}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text variant="small" className={"mr-1 text-red-500"}>
|
||||
{required ? "*" : null}
|
||||
</Text>
|
||||
{!isAnyOf && (
|
||||
<Text
|
||||
variant="small"
|
||||
className={cn("ml-2", colorClass)}
|
||||
id={description_id}
|
||||
>
|
||||
({displayType})
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconButtonProps, TranslatableString } from "@rjsf/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
|
||||
export default function AddButton({
|
||||
registry,
|
||||
className,
|
||||
uiSchema: _uiSchema,
|
||||
...props
|
||||
}: IconButtonProps) {
|
||||
const { translateString } = registry;
|
||||
return (
|
||||
<div className="m-0 w-full p-0">
|
||||
<Button
|
||||
{...props}
|
||||
size="small"
|
||||
className={cn("w-full gap-4", className)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon size={16} weight="bold" />
|
||||
{translateString(TranslatableString.AddItemButton)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
FormContextType,
|
||||
IconButtonProps,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
TranslatableString,
|
||||
} from "@rjsf/utils";
|
||||
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { extendedButtonVariants } from "@/components/atoms/Button/helpers";
|
||||
import { TrashIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
export type AutogptIconButtonProps<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
> = IconButtonProps<T, S, F> & VariantProps<typeof extendedButtonVariants>;
|
||||
|
||||
export default function IconButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
icon,
|
||||
className,
|
||||
uiSchema: _uiSchema,
|
||||
registry: _registry,
|
||||
iconType: _iconType,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className={cn(className, "w-fit border border-zinc-200 p-1.5 px-4")}
|
||||
{...otherProps}
|
||||
type="button"
|
||||
>
|
||||
{icon}
|
||||
<Text variant="body" className="ml-2">
|
||||
{" "}
|
||||
Remove Item{" "}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.CopyButton)}
|
||||
{...props}
|
||||
icon={<Copy className="h-4 w-4" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveDownButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.MoveDownButton)}
|
||||
{...props}
|
||||
icon={<ChevronDown className="h-4 w-4" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveUpButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.MoveUpButton)}
|
||||
{...props}
|
||||
icon={<ChevronUp className="h-4 w-4" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RemoveButton(props: AutogptIconButtonProps) {
|
||||
const {
|
||||
registry: { translateString },
|
||||
} = props;
|
||||
return (
|
||||
<IconButton
|
||||
title={translateString(TranslatableString.RemoveButton)}
|
||||
{...props}
|
||||
className={"border-destructive"}
|
||||
icon={<TrashIcon size={16} className="!text-zinc-800" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as AddButton } from "./AddButton";
|
||||
export {
|
||||
default as IconButton,
|
||||
CopyButton,
|
||||
RemoveButton,
|
||||
MoveUpButton,
|
||||
MoveDownButton,
|
||||
} from "./IconButton";
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ErrorListProps, TranslatableString } from "@rjsf/utils";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/molecules/Alert/Alert";
|
||||
|
||||
export default function ErrorList(props: ErrorListProps) {
|
||||
const { errors, registry } = props;
|
||||
const { translateString } = registry;
|
||||
return (
|
||||
<Alert variant="error" className="mb-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>{translateString(TranslatableString.ErrorsLabel)}</AlertTitle>
|
||||
<AlertDescription className="flex flex-col gap-1">
|
||||
{errors.map((error, i: number) => {
|
||||
return <span key={i}>• {error.stack}</span>;
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ErrorList } from "./ErrorList";
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
|
||||
export function parseFieldPath(
|
||||
rootSchema: RJSFSchema,
|
||||
id: string,
|
||||
additional: boolean,
|
||||
idSeparator: string = "_%_",
|
||||
): { path: string[]; typeHints: string[] } {
|
||||
const segments = id.split(idSeparator).filter(Boolean);
|
||||
const typeHints: string[] = [];
|
||||
|
||||
let currentSchema = rootSchema;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
const isNumeric = /^\d+$/.test(segment);
|
||||
|
||||
if (isNumeric) {
|
||||
typeHints.push("array");
|
||||
} else {
|
||||
if (additional) {
|
||||
typeHints.push("object-key");
|
||||
} else {
|
||||
typeHints.push("object-property");
|
||||
}
|
||||
currentSchema = (currentSchema.properties?.[segment] as RJSFSchema) || {};
|
||||
}
|
||||
}
|
||||
|
||||
return { path: segments, typeHints };
|
||||
}
|
||||
|
||||
// This helper work is simple - it just help us to convert rjsf id to our backend compatible id
|
||||
// Example : List[dict] = agpt_%_List_0_dict__title -> List_$_0_#_dict
|
||||
// We remove the prefix and suffix and then we split id by our custom delimiter (_%_)
|
||||
// then add _$_ delimiter for array and _#_ delimiter for object-key
|
||||
// and for normal property we add . delimiter
|
||||
|
||||
export function getHandleId(
|
||||
rootSchema: RJSFSchema,
|
||||
id: string,
|
||||
additional: boolean,
|
||||
idSeparator: string = "_%_",
|
||||
): string {
|
||||
const idPrefix = "agpt_%_";
|
||||
const idSuffix = "__title";
|
||||
|
||||
if (id.startsWith(idPrefix)) {
|
||||
id = id.slice(idPrefix.length);
|
||||
}
|
||||
if (id.endsWith(idSuffix)) {
|
||||
id = id.slice(0, -idSuffix.length);
|
||||
}
|
||||
|
||||
const { path, typeHints } = parseFieldPath(
|
||||
rootSchema,
|
||||
id,
|
||||
additional,
|
||||
idSeparator,
|
||||
);
|
||||
|
||||
return path
|
||||
.map((seg, i) => {
|
||||
const type = typeHints[i];
|
||||
if (type === "array") {
|
||||
return `_$_${seg}`;
|
||||
}
|
||||
if (type === "object-key") {
|
||||
return `_${seg}`; // we haven't added _#_ delimiter for object-key because it's already added in the id - check WrapIfAdditionalTemplate.tsx
|
||||
}
|
||||
|
||||
return `.${seg}`;
|
||||
})
|
||||
.join("")
|
||||
.slice(1);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as FieldTemplate } from "./FieldTemplate";
|
||||
export { default as TitleField } from "./TitleField";
|
||||
export { default as DescriptionField } from "./DescriptionField";
|
||||
@@ -1,8 +1,9 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { Switch } from "@/components/atoms/Switch/Switch";
|
||||
|
||||
export function SwitchWidget(props: WidgetProps) {
|
||||
export function CheckboxWidget(props: WidgetProps) {
|
||||
const { value = false, onChange, disabled, readonly, autofocus, id } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id={id}
|
||||
@@ -0,0 +1 @@
|
||||
export { CheckboxWidget } from "./CheckBoxWidget";
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { DateInput } from "@/components/atoms/DateInput/DateInput";
|
||||
|
||||
export const DateInputWidget = (props: WidgetProps) => {
|
||||
export const DateWidget = (props: WidgetProps) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
@@ -0,0 +1 @@
|
||||
export { DateWidget } from "./DateWidget";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { DateTimeInput } from "@/components/atoms/DateTimeInput/DateTimeInput";
|
||||
|
||||
export const DateTimeInputWidget = (props: WidgetProps) => {
|
||||
export const DateTimeWidget = (props: WidgetProps) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
@@ -0,0 +1 @@
|
||||
export { DateTimeWidget } from "./DateTimeWidget";
|
||||
@@ -0,0 +1 @@
|
||||
export { FileWidget } from "./FileWidget";
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
|
||||
import { getFieldErrorKey } from "@/components/renderers/InputRenderer/utils/helpers";
|
||||
import type { GoogleDrivePickerConfig } from "@/lib/autogpt-server-api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
|
||||
function hasGoogleDrivePickerConfig(
|
||||
schema: unknown,
|
||||
): schema is { google_drive_picker_config?: GoogleDrivePickerConfig } {
|
||||
return (
|
||||
typeof schema === "object" &&
|
||||
schema !== null &&
|
||||
"google_drive_picker_config" in schema
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleDrivePickerWidget(props: WidgetProps) {
|
||||
const { onChange, disabled, readonly, value, schema, id, formContext } =
|
||||
props;
|
||||
const { nodeId } = formContext || {};
|
||||
|
||||
const nodeErrors = useNodeStore((state) => {
|
||||
const node = state.nodes.find((n) => n.id === nodeId);
|
||||
return node?.data?.errors;
|
||||
});
|
||||
|
||||
const fieldErrorKey = getFieldErrorKey(id ?? "");
|
||||
const fieldError =
|
||||
nodeErrors?.[fieldErrorKey] ||
|
||||
nodeErrors?.[fieldErrorKey.replace(/_/g, ".")] ||
|
||||
nodeErrors?.[fieldErrorKey.replace(/\./g, "_")] ||
|
||||
undefined;
|
||||
|
||||
const config: GoogleDrivePickerConfig = hasGoogleDrivePickerConfig(schema)
|
||||
? schema.google_drive_picker_config || {}
|
||||
: {};
|
||||
|
||||
function handleChange(newValue: unknown) {
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<GoogleDrivePickerInput
|
||||
config={config}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
error={fieldError}
|
||||
className={cn(
|
||||
disabled || readonly ? "pointer-events-none opacity-50" : undefined,
|
||||
)}
|
||||
showRemoveButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GoogleDrivePickerWidget } from "./GoogleDrivePicketWidget";
|
||||
@@ -14,8 +14,16 @@ import {
|
||||
} from "@/components/__legacy__/ui/multiselect";
|
||||
|
||||
export const SelectWidget = (props: WidgetProps) => {
|
||||
const { options, value, onChange, disabled, readonly, id, formContext } =
|
||||
props;
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
readonly,
|
||||
className,
|
||||
id,
|
||||
formContext,
|
||||
} = props;
|
||||
const enumOptions = options.enumOptions || [];
|
||||
const type = mapJsonSchemaTypeToInputType(props.schema);
|
||||
const { size = "small" } = formContext || {};
|
||||
@@ -36,7 +44,7 @@ export const SelectWidget = (props: WidgetProps) => {
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{enumOptions?.map((option) => (
|
||||
{enumOptions?.map((option: any) => (
|
||||
<MultiSelectorItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MultiSelectorItem>
|
||||
@@ -56,12 +64,13 @@ export const SelectWidget = (props: WidgetProps) => {
|
||||
value={value ?? ""}
|
||||
onValueChange={onChange}
|
||||
options={
|
||||
enumOptions?.map((option) => ({
|
||||
enumOptions?.map((option: any) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
})) || []
|
||||
}
|
||||
wrapperClassName="!mb-0 "
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SelectWidget } from "./SelectWidget";
|
||||
@@ -14,15 +14,12 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
import { InputExpanderModal } from "./InputExpanderModal";
|
||||
import { ArrowsOutIcon } from "@phosphor-icons/react";
|
||||
import { InputExpanderModal } from "./TextInputExpanderModal";
|
||||
|
||||
export const TextInputWidget = (props: WidgetProps) => {
|
||||
const { schema, formContext } = props;
|
||||
const { uiType, size = "small" } = formContext as {
|
||||
uiType: BlockUIType;
|
||||
size?: string;
|
||||
};
|
||||
export default function TextWidget(props: WidgetProps) {
|
||||
const { schema, placeholder, registry } = props;
|
||||
const { size, uiType } = registry.formContext;
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
@@ -51,7 +48,7 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
handleChange: (v: string) => (v === "" ? undefined : Number(v)),
|
||||
},
|
||||
[InputType.INTEGER]: {
|
||||
htmlType: "number",
|
||||
htmlType: "account",
|
||||
placeholder: "Enter integer value...",
|
||||
handleChange: (v: string) => (v === "" ? undefined : Number(v)),
|
||||
},
|
||||
@@ -122,7 +119,7 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
wrapperClassName="mb-0 flex-1"
|
||||
value={props.value ?? ""}
|
||||
onChange={handleChange}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
placeholder={placeholder || config.placeholder}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
className={showExpandButton ? "pr-8" : ""}
|
||||
@@ -152,8 +149,8 @@ export const TextInputWidget = (props: WidgetProps) => {
|
||||
title={schema.title || "Edit value"}
|
||||
description={schema.description || ""}
|
||||
defaultValue={props.value ?? ""}
|
||||
placeholder={schema.placeholder || config.placeholder}
|
||||
placeholder={placeholder || config.placeholder}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./TextWidget";
|
||||
export { InputExpanderModal } from "./TextInputExpanderModal";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { TimeInput } from "@/components/atoms/TimeInput/TimeInput";
|
||||
|
||||
export const TimeInputWidget = (props: WidgetProps) => {
|
||||
export const TimeWidget = (props: WidgetProps) => {
|
||||
const { value, onChange, disabled, readonly, placeholder, id, formContext } =
|
||||
props;
|
||||
const { size = "small" } = formContext || {};
|
||||
@@ -0,0 +1 @@
|
||||
export { TimeWidget } from "./TimeWidget";
|
||||
@@ -0,0 +1,8 @@
|
||||
export { CheckboxWidget } from "./CheckboxInput";
|
||||
export { DateWidget } from "./DateInput";
|
||||
export { DateTimeWidget } from "./DateTimeInput";
|
||||
export { FileWidget } from "./FileInput";
|
||||
export { GoogleDrivePickerWidget } from "./GoogleDrivePicker";
|
||||
export { SelectWidget } from "./SelectInput";
|
||||
export { default as TextWidget } from "./TextInput";
|
||||
export { TimeWidget } from "./TimeInput";
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ANY_OF_FLAG = "__anyOf";
|
||||
export const ARRAY_FLAG = "__array";
|
||||
export const OBJECT_FLAG = "__object";
|
||||
export const KEY_PAIR_FLAG = "__keyPair";
|
||||
export const TITLE_FLAG = "__title";
|
||||
export const ARRAY_ITEM_FLAG = "__arrayItem";
|
||||
export const ID_PREFIX = "agpt_@_";
|
||||
export const ID_PREFIX_ARRAY = "agpt_%_";
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { FieldProps, getUiOptions } from "@rjsf/utils";
|
||||
import {
|
||||
BlockIOCredentialsSubSchema,
|
||||
CredentialsMetaInput,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { CredentialFieldTitle } from "./components/CredentialFieldTitle";
|
||||
|
||||
export const CredentialsField = (props: FieldProps) => {
|
||||
const { formData, onChange, schema, registry, fieldPathId } = props;
|
||||
|
||||
const formContext = registry.formContext;
|
||||
const uiOptions = getUiOptions(props.uiSchema);
|
||||
const nodeId = formContext?.nodeId;
|
||||
|
||||
// Get sibling inputs (hardcoded values) from the node store
|
||||
const hardcodedValues = useNodeStore(
|
||||
useShallow((state) => (nodeId ? state.getHardCodedValues(nodeId) : {})),
|
||||
);
|
||||
|
||||
const handleChange = (newValue: any) => {
|
||||
onChange(newValue, fieldPathId?.path);
|
||||
};
|
||||
|
||||
const handleSelectCredentials = (credentialsMeta?: CredentialsMetaInput) => {
|
||||
if (credentialsMeta) {
|
||||
handleChange({
|
||||
id: credentialsMeta.id,
|
||||
provider: credentialsMeta.provider,
|
||||
title: credentialsMeta.title,
|
||||
type: credentialsMeta.type,
|
||||
});
|
||||
} else {
|
||||
handleChange(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Convert formData to CredentialsMetaInput format
|
||||
const selectedCredentials: CredentialsMetaInput | undefined = useMemo(
|
||||
() =>
|
||||
formData?.id
|
||||
? {
|
||||
id: formData.id,
|
||||
provider: formData.provider,
|
||||
title: formData.title,
|
||||
type: formData.type,
|
||||
}
|
||||
: undefined,
|
||||
[formData?.id, formData?.provider, formData?.title, formData?.type],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<CredentialFieldTitle
|
||||
fieldPathId={fieldPathId}
|
||||
registry={registry}
|
||||
uiOptions={uiOptions}
|
||||
schema={schema}
|
||||
/>
|
||||
<CredentialsInput
|
||||
schema={schema as BlockIOCredentialsSubSchema}
|
||||
selectedCredentials={selectedCredentials}
|
||||
onSelectCredentials={handleSelectCredentials}
|
||||
siblingInputs={hardcodedValues}
|
||||
showTitle={false}
|
||||
readOnly={formContext?.readOnly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
getTemplate,
|
||||
UiSchema,
|
||||
Registry,
|
||||
RJSFSchema,
|
||||
FieldPathId,
|
||||
titleId,
|
||||
descriptionId,
|
||||
} from "@rjsf/utils";
|
||||
import { getCredentialProviderFromSchema, toDisplayName } from "../helpers";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
|
||||
import { updateUiOption } from "../../../helpers";
|
||||
import { uiSchema } from "@/app/(platform)/build/components/FlowEditor/nodes/uiSchema";
|
||||
|
||||
export const CredentialFieldTitle = (props: {
|
||||
registry: Registry;
|
||||
uiOptions: UiSchema;
|
||||
schema: RJSFSchema;
|
||||
fieldPathId: FieldPathId;
|
||||
}) => {
|
||||
const { registry, uiOptions, schema, fieldPathId } = props;
|
||||
const { nodeId } = registry.formContext;
|
||||
|
||||
const TitleFieldTemplate = getTemplate(
|
||||
"TitleFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
|
||||
const credentialProvider = toDisplayName(
|
||||
getCredentialProviderFromSchema(
|
||||
useNodeStore.getState().getHardCodedValues(nodeId),
|
||||
schema as BlockIOCredentialsSubSchema,
|
||||
) ?? "",
|
||||
);
|
||||
|
||||
const updatedUiSchema = updateUiOption(uiSchema, {
|
||||
showHandles: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TitleFieldTemplate
|
||||
id={titleId(fieldPathId ?? "")}
|
||||
title={credentialProvider ?? ""}
|
||||
required={true}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
uiSchema={updatedUiSchema}
|
||||
/>
|
||||
<DescriptionFieldTemplate
|
||||
id={descriptionId(fieldPathId ?? "")}
|
||||
description={schema.description || ""}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
|
||||
import { GoogleDrivePickerConfig } from "@/lib/autogpt-server-api";
|
||||
import { FieldProps, getUiOptions } from "@rjsf/utils";
|
||||
|
||||
export const GoogleDrivePickerField = (props: FieldProps) => {
|
||||
const { schema, uiSchema, onChange, fieldPathId, formData } = props;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const config: GoogleDrivePickerConfig = schema.google_drive_picker_config;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GoogleDrivePickerInput
|
||||
config={config}
|
||||
value={formData}
|
||||
onChange={(value) => onChange(value, fieldPathId.path)}
|
||||
className={uiOptions.className}
|
||||
showRemoveButton={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { FieldProps, RJSFSchema, RegistryFieldsType } from "@rjsf/utils";
|
||||
import { CredentialsField } from "./CredentialField/CredentialField";
|
||||
import { GoogleDrivePickerField } from "./GoogleDrivePickerField/GoogleDrivePickerField";
|
||||
|
||||
export interface CustomFieldDefinition {
|
||||
id: string;
|
||||
matcher: (schema: any) => boolean;
|
||||
component: (props: FieldProps<any, RJSFSchema, any>) => JSX.Element | null;
|
||||
}
|
||||
|
||||
export const CUSTOM_FIELDS: CustomFieldDefinition[] = [
|
||||
{
|
||||
id: "custom/credential_field",
|
||||
matcher: (schema: any) => {
|
||||
return (
|
||||
typeof schema === "object" &&
|
||||
schema !== null &&
|
||||
"credentials_provider" in schema
|
||||
);
|
||||
},
|
||||
component: CredentialsField,
|
||||
},
|
||||
{
|
||||
id: "custom/google_drive_picker_field",
|
||||
matcher: (schema: any) => {
|
||||
return (
|
||||
"google_drive_picker_config" in schema ||
|
||||
("format" in schema && schema.format === "google-drive-picker")
|
||||
);
|
||||
},
|
||||
component: GoogleDrivePickerField,
|
||||
},
|
||||
];
|
||||
|
||||
export function findCustomFieldId(schema: any): string | null {
|
||||
for (const field of CUSTOM_FIELDS) {
|
||||
if (field.matcher(schema)) {
|
||||
return field.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function generateCustomFields(): RegistryFieldsType {
|
||||
return CUSTOM_FIELDS.reduce(
|
||||
(acc, field) => {
|
||||
acc[field.id] = field.component;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
# Input Renderer 2 - Hierarchy
|
||||
|
||||
## Flow Overview
|
||||
|
||||
```
|
||||
FormRenderer2 → Form (RJSF) → ObjectFieldTemplate → FieldTemplate → Widget/Field
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Layers
|
||||
|
||||
### 1. Root (FormRenderer2)
|
||||
|
||||
- Entry point
|
||||
- Preprocesses schema
|
||||
- Passes to RJSF Form
|
||||
|
||||
### 2. Form (registry/Form.tsx)
|
||||
|
||||
- RJSF themed form
|
||||
- Combines: templates + widgets + fields
|
||||
|
||||
### 3. Templates (decide layout/structure)
|
||||
|
||||
| Template | When Used |
|
||||
| -------------------------- | ------------------------------------------- |
|
||||
| `ObjectFieldTemplate` | `type: "object"` |
|
||||
| `ArrayFieldTemplate` | `type: "array"` |
|
||||
| `FieldTemplate` | Wraps every field (title, errors, children) |
|
||||
| `ArrayFieldItemTemplate` | Each array item |
|
||||
| `WrapIfAdditionalTemplate` | Additional properties in objects |
|
||||
|
||||
### 4. Fields (custom rendering logic)
|
||||
|
||||
| Field | When Used |
|
||||
| ------------------ | ---------------------------- |
|
||||
| `AnyOfField` | `anyOf` or `oneOf` in schema |
|
||||
| `ArraySchemaField` | Array type handling |
|
||||
|
||||
### 5. Widgets (actual input elements)
|
||||
|
||||
| Widget | Input Type |
|
||||
| ---------------- | ----------------------- |
|
||||
| `TextWidget` | string, number, integer |
|
||||
| `SelectWidget` | enum, anyOf selector |
|
||||
| `CheckboxWidget` | boolean |
|
||||
| `FileWidget` | file upload |
|
||||
| `DateWidget` | date |
|
||||
| `TimeWidget` | time |
|
||||
| `DateTimeWidget` | datetime |
|
||||
|
||||
---
|
||||
|
||||
## Your Schema Hierarchy
|
||||
|
||||
```
|
||||
Root (type: object)
|
||||
└── ObjectFieldTemplate
|
||||
│
|
||||
├── name (string, required)
|
||||
│ └── FieldTemplate → TextWidget
|
||||
│
|
||||
├── value (anyOf)
|
||||
│ └── FieldTemplate → AnyOfField
|
||||
│ └── Selector dropdown + selected type:
|
||||
│ ├── String → TextWidget
|
||||
│ ├── Number → TextWidget
|
||||
│ ├── Integer → TextWidget
|
||||
│ ├── Boolean → CheckboxWidget
|
||||
│ ├── Array → ArrayFieldTemplate → items
|
||||
│ ├── Object → ObjectFieldTemplate
|
||||
│ └── Null → nothing
|
||||
│
|
||||
├── title (anyOf: string | null)
|
||||
│ └── FieldTemplate → AnyOfField
|
||||
│ └── String → TextWidget OR Null → nothing
|
||||
│
|
||||
├── description (anyOf: string | null)
|
||||
│ └── FieldTemplate → AnyOfField
|
||||
│ └── String → TextWidget OR Null → nothing
|
||||
│
|
||||
├── placeholder_values (array of strings)
|
||||
│ └── FieldTemplate → ArrayFieldTemplate
|
||||
│ └── ArrayFieldItemTemplate (per item)
|
||||
│ └── TextWidget
|
||||
│
|
||||
├── advanced (boolean)
|
||||
│ └── FieldTemplate → CheckboxWidget
|
||||
│
|
||||
└── secret (boolean)
|
||||
└── FieldTemplate → CheckboxWidget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nested Examples (up to 3 levels)
|
||||
|
||||
### Simple Array (strings)
|
||||
|
||||
```json
|
||||
{ "tags": { "type": "array", "items": { "type": "string" } } }
|
||||
```
|
||||
|
||||
```
|
||||
Level 1: ObjectFieldTemplate (root)
|
||||
└── Level 2: FieldTemplate → ArrayFieldTemplate
|
||||
└── Level 3: ArrayFieldItemTemplate → TextWidget
|
||||
```
|
||||
|
||||
### Array of Objects
|
||||
|
||||
```json
|
||||
{
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"age": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Level 1: ObjectFieldTemplate (root)
|
||||
└── Level 2: FieldTemplate → ArrayFieldTemplate
|
||||
└── Level 3: ArrayFieldItemTemplate → ObjectFieldTemplate
|
||||
├── FieldTemplate → TextWidget (name)
|
||||
└── FieldTemplate → TextWidget (age)
|
||||
```
|
||||
|
||||
### Nested Object (3 levels)
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"database": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": { "type": "string" },
|
||||
"port": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Level 1: ObjectFieldTemplate (root)
|
||||
└── config
|
||||
└── Level 2: FieldTemplate → ObjectFieldTemplate
|
||||
└── database
|
||||
└── Level 3: FieldTemplate → ObjectFieldTemplate
|
||||
├── FieldTemplate → TextWidget (host)
|
||||
└── FieldTemplate → TextWidget (port)
|
||||
```
|
||||
|
||||
### Array of Arrays (nested array)
|
||||
|
||||
```json
|
||||
{
|
||||
"matrix": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Level 1: ObjectFieldTemplate (root)
|
||||
└── Level 2: FieldTemplate → ArrayFieldTemplate
|
||||
└── Level 3: ArrayFieldItemTemplate → ArrayFieldTemplate
|
||||
└── ArrayFieldItemTemplate → TextWidget
|
||||
```
|
||||
|
||||
### Complex: Object → Array → Object
|
||||
|
||||
```json
|
||||
{
|
||||
"company": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"departments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"budget": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Level 1: ObjectFieldTemplate (root)
|
||||
└── company
|
||||
└── Level 2: FieldTemplate → ObjectFieldTemplate
|
||||
└── departments
|
||||
└── Level 3: FieldTemplate → ArrayFieldTemplate
|
||||
└── ArrayFieldItemTemplate → ObjectFieldTemplate
|
||||
├── FieldTemplate → TextWidget (name)
|
||||
└── FieldTemplate → TextWidget (budget)
|
||||
```
|
||||
|
||||
### anyOf inside Array
|
||||
|
||||
```json
|
||||
{
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "object", "properties": { "id": { "type": "string" } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
Level 1: ObjectFieldTemplate (root)
|
||||
└── Level 2: FieldTemplate → ArrayFieldTemplate
|
||||
└── Level 3: ArrayFieldItemTemplate → AnyOfField
|
||||
└── Selector + selected:
|
||||
├── String → TextWidget
|
||||
└── Object → ObjectFieldTemplate
|
||||
└── FieldTemplate → TextWidget (id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nesting Pattern Summary
|
||||
|
||||
| Parent Type | Child Wrapper |
|
||||
| ----------- | ----------------------------------------------- |
|
||||
| object | `ObjectFieldTemplate` → `FieldTemplate` |
|
||||
| array | `ArrayFieldTemplate` → `ArrayFieldItemTemplate` |
|
||||
| anyOf | `AnyOfField` → selected schema's template |
|
||||
| primitive | `Widget` (leaf - no children) |
|
||||
|
||||
**Pattern:** Each level adds FieldTemplate wrapper except array items (use ArrayFieldItemTemplate)
|
||||
|
||||
---
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **FieldTemplate wraps everything** - handles title, description, errors
|
||||
2. **anyOf = AnyOfField** - shows dropdown to pick type, then renders selected schema
|
||||
3. **ObjectFieldTemplate loops properties** - each property gets FieldTemplate
|
||||
4. **ArrayFieldTemplate loops items** - each item gets ArrayFieldItemTemplate
|
||||
5. **Widgets are leaf nodes** - actual input controls user interacts with
|
||||
6. **Nesting repeats the pattern** - object/array/anyOf can contain object/array/anyOf recursively
|
||||
|
||||
---
|
||||
|
||||
## Decision Flow
|
||||
|
||||
```
|
||||
Schema Type?
|
||||
├── object → ObjectFieldTemplate → loop properties
|
||||
├── array → ArrayFieldTemplate → loop items
|
||||
├── anyOf/oneOf → AnyOfField → selector + selected schema
|
||||
└── primitive (string/number/boolean) → Widget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Wrapping Order
|
||||
|
||||
```
|
||||
ObjectFieldTemplate (root)
|
||||
└── FieldTemplate (per property)
|
||||
└── WrapIfAdditionalTemplate (if additionalProperties)
|
||||
└── TitleField + DescriptionField + children
|
||||
└── Widget OR nested Template/Field
|
||||
```
|
||||
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
RJSFSchema,
|
||||
UIOptionsType,
|
||||
StrictRJSFSchema,
|
||||
FormContextType,
|
||||
ADDITIONAL_PROPERTY_FLAG,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import {
|
||||
ANY_OF_FLAG,
|
||||
ARRAY_ITEM_FLAG,
|
||||
ID_PREFIX,
|
||||
ID_PREFIX_ARRAY,
|
||||
KEY_PAIR_FLAG,
|
||||
OBJECT_FLAG,
|
||||
} from "./constants";
|
||||
import { PathSegment } from "./types";
|
||||
|
||||
export function updateUiOption<T extends Record<string, any>>(
|
||||
uiSchema: T | undefined,
|
||||
options: Record<string, any>,
|
||||
): T & { "ui:options": Record<string, any> } {
|
||||
return {
|
||||
...(uiSchema || {}),
|
||||
"ui:options": {
|
||||
...uiSchema?.["ui:options"],
|
||||
...options,
|
||||
},
|
||||
} as T & { "ui:options": Record<string, any> };
|
||||
}
|
||||
|
||||
export const cleanUpHandleId = (handleId: string) => {
|
||||
let newHandleId = handleId;
|
||||
if (handleId.includes(ANY_OF_FLAG)) {
|
||||
newHandleId = newHandleId.replace(ANY_OF_FLAG, "");
|
||||
}
|
||||
if (handleId.includes(ARRAY_ITEM_FLAG)) {
|
||||
newHandleId = newHandleId.replace(ARRAY_ITEM_FLAG, "");
|
||||
}
|
||||
if (handleId.includes(KEY_PAIR_FLAG)) {
|
||||
newHandleId = newHandleId.replace(KEY_PAIR_FLAG, "");
|
||||
}
|
||||
if (handleId.includes(OBJECT_FLAG)) {
|
||||
newHandleId = newHandleId.replace(OBJECT_FLAG, "");
|
||||
}
|
||||
if (handleId.includes(ID_PREFIX_ARRAY)) {
|
||||
newHandleId = newHandleId.replace(ID_PREFIX_ARRAY, "");
|
||||
}
|
||||
if (handleId.includes(ID_PREFIX)) {
|
||||
newHandleId = newHandleId.replace(ID_PREFIX, "");
|
||||
}
|
||||
return newHandleId;
|
||||
};
|
||||
|
||||
export const isArrayItem = <
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
uiOptions,
|
||||
}: {
|
||||
uiOptions: UIOptionsType<T, S, F>;
|
||||
}) => {
|
||||
return uiOptions.handleId?.endsWith(ARRAY_ITEM_FLAG);
|
||||
};
|
||||
|
||||
export const isKeyValuePair = ({ schema }: { schema: RJSFSchema }) => {
|
||||
return ADDITIONAL_PROPERTY_FLAG in schema;
|
||||
};
|
||||
|
||||
export const isNormal = <
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
uiOptions,
|
||||
}: {
|
||||
uiOptions: UIOptionsType<T, S, F>;
|
||||
}) => {
|
||||
return uiOptions.handleId === undefined;
|
||||
};
|
||||
|
||||
export const isPartOfAnyOf = <
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
uiOptions,
|
||||
}: {
|
||||
uiOptions: UIOptionsType<T, S, F>;
|
||||
}) => {
|
||||
return uiOptions.handleId?.endsWith(ANY_OF_FLAG);
|
||||
};
|
||||
export const isObjectProperty = <
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
uiOptions,
|
||||
schema,
|
||||
}: {
|
||||
uiOptions: UIOptionsType<T, S, F>;
|
||||
schema: RJSFSchema;
|
||||
}) => {
|
||||
return (
|
||||
!isArrayItem({ uiOptions }) &&
|
||||
!isKeyValuePair({ schema }) &&
|
||||
!isNormal({ uiOptions }) &&
|
||||
!isPartOfAnyOf({ uiOptions })
|
||||
);
|
||||
};
|
||||
|
||||
export const getHandleId = <
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({
|
||||
id,
|
||||
schema,
|
||||
uiOptions,
|
||||
}: {
|
||||
id: string;
|
||||
schema: RJSFSchema;
|
||||
uiOptions: UIOptionsType<T, S, F>;
|
||||
}) => {
|
||||
const parentHandleId = uiOptions.handleId;
|
||||
|
||||
if (isNormal({ uiOptions })) {
|
||||
return id;
|
||||
}
|
||||
|
||||
if (isPartOfAnyOf({ uiOptions })) {
|
||||
return parentHandleId + ANY_OF_FLAG;
|
||||
}
|
||||
|
||||
if (isKeyValuePair({ schema })) {
|
||||
const key = id.split("_%_").at(-1);
|
||||
let prefix = "";
|
||||
if (parentHandleId) {
|
||||
prefix = parentHandleId;
|
||||
} else {
|
||||
prefix = id.split("_%_").slice(0, -1).join("_%_");
|
||||
}
|
||||
|
||||
const handleId = `${prefix}_#_${key}`;
|
||||
return handleId + KEY_PAIR_FLAG;
|
||||
}
|
||||
|
||||
if (isArrayItem({ uiOptions })) {
|
||||
const index = id.split("_%_").at(-1);
|
||||
const prefix = id.split("_%_").slice(0, -1).join("_%_");
|
||||
const handleId = `${prefix}_$_${index}`;
|
||||
return handleId + ARRAY_ITEM_FLAG;
|
||||
}
|
||||
|
||||
if (isObjectProperty({ uiOptions, schema })) {
|
||||
const key = id.split("_%_").at(-1);
|
||||
const prefix = id.split("_%_").slice(0, -1).join("_%_");
|
||||
const handleId = `${prefix}_@_${key}`;
|
||||
return handleId + OBJECT_FLAG;
|
||||
}
|
||||
return parentHandleId;
|
||||
};
|
||||
|
||||
export function isCredentialFieldSchema(schema: any): boolean {
|
||||
return (
|
||||
typeof schema === "object" &&
|
||||
schema !== null &&
|
||||
"credentials_provider" in schema
|
||||
);
|
||||
}
|
||||
|
||||
export function parseHandleIdToPath(handleId: string): PathSegment[] {
|
||||
const cleanedId = cleanUpHandleId(handleId);
|
||||
const segments: PathSegment[] = [];
|
||||
const parts = cleanedId.split(/(_#_|_@_|_\$_|\.)/);
|
||||
|
||||
let currentType: "property" | "item" | "additional" | "normal" = "normal";
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (part === "_#_") {
|
||||
currentType = "additional";
|
||||
} else if (part === "_@_") {
|
||||
currentType = "property";
|
||||
} else if (part === "_$_") {
|
||||
currentType = "item";
|
||||
} else if (part === ".") {
|
||||
currentType = "normal";
|
||||
} else if (part) {
|
||||
const isNumeric = /^\d+$/.test(part);
|
||||
if (currentType === "item" && isNumeric) {
|
||||
segments.push({
|
||||
key: part,
|
||||
type: "item",
|
||||
index: parseInt(part, 10),
|
||||
});
|
||||
} else {
|
||||
segments.push({
|
||||
key: part,
|
||||
type: currentType,
|
||||
});
|
||||
}
|
||||
currentType = "normal";
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a path exists in an object, creating intermediate objects/arrays as needed
|
||||
* Returns true if any modifications were made
|
||||
*/
|
||||
export function ensurePathExists(
|
||||
obj: Record<string, any>,
|
||||
segments: PathSegment[],
|
||||
): boolean {
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
let current = obj;
|
||||
let modified = false;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
const isLast = i === segments.length - 1;
|
||||
const nextSegment = segments[i + 1];
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (isLast) {
|
||||
return "";
|
||||
}
|
||||
if (nextSegment?.type === "item") {
|
||||
return [];
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (segment.type === "item" && segment.index !== undefined) {
|
||||
if (!Array.isArray(current)) {
|
||||
return modified;
|
||||
}
|
||||
|
||||
while (current.length <= segment.index) {
|
||||
current.push(isLast ? "" : {});
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!isLast) {
|
||||
if (
|
||||
current[segment.index] === undefined ||
|
||||
current[segment.index] === null
|
||||
) {
|
||||
current[segment.index] = getDefaultValue();
|
||||
modified = true;
|
||||
}
|
||||
current = current[segment.index];
|
||||
}
|
||||
} else {
|
||||
if (!(segment.key in current)) {
|
||||
current[segment.key] = getDefaultValue();
|
||||
modified = true;
|
||||
} else if (!isLast && current[segment.key] === undefined) {
|
||||
current[segment.key] = getDefaultValue();
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!isLast) {
|
||||
current = current[segment.key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { FormRenderer } from "./FormRenderer";
|
||||
export { default as Form } from "./registry";
|
||||
export type { ExtendedFormContextType } from "./types";
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentType } from "react";
|
||||
import { FormProps, withTheme, ThemeProps } from "@rjsf/core";
|
||||
import {
|
||||
generateBaseFields,
|
||||
generateBaseTemplates,
|
||||
generateBaseWidgets,
|
||||
} from "../base/base-registry";
|
||||
import { generateCustomFields } from "../custom/custom-registry";
|
||||
|
||||
export function generateForm(): ComponentType<FormProps> {
|
||||
const theme: ThemeProps = {
|
||||
templates: generateBaseTemplates(),
|
||||
widgets: generateBaseWidgets(),
|
||||
fields: {
|
||||
...generateBaseFields(),
|
||||
...generateCustomFields(),
|
||||
},
|
||||
};
|
||||
|
||||
return withTheme(theme);
|
||||
}
|
||||
|
||||
export default generateForm();
|
||||
@@ -0,0 +1,10 @@
|
||||
export { default, generateForm } from "./Form";
|
||||
export {
|
||||
generateBaseFields,
|
||||
generateBaseTemplates,
|
||||
generateBaseWidgets,
|
||||
} from "../base/base-registry";
|
||||
export {
|
||||
generateCustomFields,
|
||||
findCustomFieldId,
|
||||
} from "../custom/custom-registry";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BlockUIType } from "@/app/(platform)/build/components/types";
|
||||
|
||||
export type ExtraContext = {
|
||||
nodeId?: string;
|
||||
uiType?: BlockUIType;
|
||||
size?: "small" | "medium" | "large";
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { BlockUIType } from "@/lib/autogpt-server-api/types";
|
||||
import { FormContextType } from "@rjsf/utils";
|
||||
|
||||
export interface ExtendedFormContextType extends FormContextType {
|
||||
nodeId?: string;
|
||||
uiType?: BlockUIType;
|
||||
showHandles?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export type PathSegment = {
|
||||
key: string;
|
||||
type: "property" | "item" | "additional" | "normal";
|
||||
index?: number;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { findCustomFieldId } from "../custom/custom-registry";
|
||||
|
||||
/**
|
||||
* Pre-processes the input schema to ensure all properties have a type defined.
|
||||
* If a property doesn't have a type, it assigns a union of all supported JSON Schema types.
|
||||
*/
|
||||
|
||||
export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return schema;
|
||||
@@ -19,6 +21,12 @@ export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema {
|
||||
if (property && typeof property === "object") {
|
||||
const processedProperty = { ...property };
|
||||
|
||||
// adding $id for custom field
|
||||
const customFieldId = findCustomFieldId(processedProperty);
|
||||
if (customFieldId) {
|
||||
processedProperty.$id = customFieldId;
|
||||
}
|
||||
|
||||
// Only add type if no type is defined AND no anyOf/oneOf/allOf is present
|
||||
if (
|
||||
!processedProperty.type &&
|
||||
@@ -32,7 +40,7 @@ export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema {
|
||||
{ type: "integer" },
|
||||
{ type: "boolean" },
|
||||
{ type: "array", items: { type: "string" } },
|
||||
{ type: "object" },
|
||||
{ type: "object", title: "Object", additionalProperties: true },
|
||||
{ type: "null" },
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getUiOptions, RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
|
||||
export function isAnyOfSchema(schema: RJSFSchema | undefined): boolean {
|
||||
return Array.isArray(schema?.anyOf) && schema!.anyOf.length > 0;
|
||||
}
|
||||
|
||||
export const isAnyOfChild = (
|
||||
uiSchema: UiSchema<any, RJSFSchema, any> | undefined,
|
||||
): boolean => {
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
return uiOptions.label === false;
|
||||
};
|
||||
|
||||
export function isOptionalType(schema: RJSFSchema | undefined): {
|
||||
isOptional: boolean;
|
||||
type?: any;
|
||||
} {
|
||||
if (
|
||||
!Array.isArray(schema?.anyOf) ||
|
||||
schema!.anyOf.length !== 2 ||
|
||||
!schema!.anyOf.some((opt: any) => opt.type === "null")
|
||||
) {
|
||||
return { isOptional: false };
|
||||
}
|
||||
|
||||
const nonNullType = schema!.anyOf?.find((opt: any) => opt.type !== "null");
|
||||
|
||||
return {
|
||||
isOptional: true,
|
||||
type: nonNullType,
|
||||
};
|
||||
}
|
||||
export function isAnyOfSelector(name: string) {
|
||||
return name.includes("anyof_select");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user