mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(frontend/builder): handle discriminated unions and improve node layout (#12354)
## Summary - **Discriminated union support (oneOf)**: Added a new `OneOfField` component that properly renders Pydantic discriminated unions. Hides the unusable parent object handle, auto-populates the discriminator value, shows a dropdown with variant titles (e.g., "Username" / "UserId"), and filters out the internal discriminator field from the form. Non-discriminated `oneOf` schemas fall back to existing `AnyOfField` behavior. - **Collapsible object outputs**: Object-type outputs with nested keys (e.g., `PersonLookupResponse.Url`, `PersonLookupResponse.profile`) are now collapsed by default behind a caret toggle. Nested keys show short names instead of the full `Parent.Key` prefix. - **Node layout cleanup**: Removed excessive bottom margin (`mb-6`) from `FormRenderer`, hide the Advanced toggle when no advanced fields exist, and add rounded bottom corners on OUTPUT-type blocks. <img width="440" height="427" alt="Screenshot 2026-03-10 at 11 31 55 AM" src="https://github.com/user-attachments/assets/06cc5414-4e02-4371-bdeb-1695e7cb2c97" /> <img width="371" height="320" alt="Screenshot 2026-03-10 at 11 36 52 AM" src="https://github.com/user-attachments/assets/1a55f87a-c602-4f4d-b91b-6e49f810e5d5" /> ## Test plan - [x] Add a Twitter Get User block — verify "Identifier" shows a dropdown (Username/UserId) with no unusable parent handle, discriminator field is hidden, and the block can run without staying INCOMPLETE - [x] Add any block with object outputs (e.g., PersonLookupResponse) — verify nested keys are collapsed by default and expand on click with short labels - [x] Verify blocks without advanced fields don't show the Advanced toggle - [x] Verify existing `anyOf` schemas (optional types, 3+ variant unions) still render correctly - [x] Check OUTPUT-type blocks have rounded bottom corners --------- Co-authored-by: Reinier van der Leer <pwuts@agpt.co> Co-authored-by: eureka928 <meobius123@gmail.com>
This commit is contained in:
@@ -23,6 +23,12 @@ import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||
import { SubAgentUpdateFeature } from "./components/SubAgentUpdate/SubAgentUpdateFeature";
|
||||
import { useCustomNode } from "./useCustomNode";
|
||||
|
||||
function hasAdvancedFields(schema: RJSFSchema): boolean {
|
||||
const properties = schema?.properties;
|
||||
if (!properties) return false;
|
||||
return Object.values(properties).some((prop: any) => prop.advanced === true);
|
||||
}
|
||||
|
||||
export type CustomNodeData = {
|
||||
hardcodedValues: {
|
||||
[key: string]: any;
|
||||
@@ -108,7 +114,11 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
)}
|
||||
showHandles={showHandles}
|
||||
/>
|
||||
<NodeAdvancedToggle nodeId={nodeId} />
|
||||
<NodeAdvancedToggle
|
||||
nodeId={nodeId}
|
||||
isLastSection={data.uiType === BlockUIType.OUTPUT}
|
||||
hasAdvancedFields={hasAdvancedFields(inputSchema)}
|
||||
/>
|
||||
{data.uiType != BlockUIType.OUTPUT && (
|
||||
<OutputHandler
|
||||
uiType={data.uiType}
|
||||
|
||||
@@ -2,18 +2,33 @@ import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
isLastSection?: boolean;
|
||||
hasAdvancedFields?: boolean;
|
||||
};
|
||||
|
||||
export function NodeAdvancedToggle({ nodeId }: Props) {
|
||||
export function NodeAdvancedToggle({
|
||||
nodeId,
|
||||
isLastSection,
|
||||
hasAdvancedFields = true,
|
||||
}: Props) {
|
||||
const showAdvanced = useNodeStore(
|
||||
(state) => state.nodeAdvancedStates[nodeId] || false,
|
||||
);
|
||||
const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced);
|
||||
|
||||
if (!hasAdvancedFields) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-start gap-2 bg-white px-5 pb-3.5">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-2 bg-white px-5 pb-3.5",
|
||||
isLastSection && "rounded-b-xlarge",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-fit min-w-0 p-0 hover:border-transparent hover:bg-transparent"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CaretDownIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { CaretDownIcon, CaretRightIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -30,13 +30,41 @@ export const OutputHandler = ({
|
||||
const properties = outputSchema?.properties || {};
|
||||
const [isOutputVisible, setIsOutputVisible] = useState(true);
|
||||
const brokenOutputs = useBrokenOutputs(nodeId);
|
||||
const [expandedObjects, setExpandedObjects] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const showHandles = uiType !== BlockUIType.OUTPUT;
|
||||
|
||||
function toggleObjectExpanded(key: string) {
|
||||
setExpandedObjects((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
}
|
||||
|
||||
function hasConnectedOrBrokenDescendant(
|
||||
schema: RJSFSchema,
|
||||
keyPrefix: string,
|
||||
): boolean {
|
||||
if (!schema) return false;
|
||||
return Object.entries(schema).some(
|
||||
([key, fieldSchema]: [string, RJSFSchema]) => {
|
||||
const fullKey = keyPrefix ? `${keyPrefix}_#_${key}` : key;
|
||||
if (isOutputConnected(nodeId, fullKey) || brokenOutputs.has(fullKey))
|
||||
return true;
|
||||
if (fieldSchema?.properties)
|
||||
return hasConnectedOrBrokenDescendant(
|
||||
fieldSchema.properties,
|
||||
fullKey,
|
||||
);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const renderOutputHandles = (
|
||||
schema: RJSFSchema,
|
||||
keyPrefix: string = "",
|
||||
titlePrefix: string = "",
|
||||
connectedOnly: boolean = false,
|
||||
): React.ReactNode[] => {
|
||||
return Object.entries(schema).map(
|
||||
([key, fieldSchema]: [string, RJSFSchema]) => {
|
||||
@@ -44,10 +72,23 @@ export const OutputHandler = ({
|
||||
const fieldTitle = titlePrefix + (fieldSchema?.title || key);
|
||||
|
||||
const isConnected = isOutputConnected(nodeId, fullKey);
|
||||
const shouldShow = isConnected || isOutputVisible;
|
||||
const isBroken = brokenOutputs.has(fullKey);
|
||||
const hasNestedProperties = !!fieldSchema?.properties;
|
||||
const selfIsRelevant = isConnected || isBroken;
|
||||
const descendantIsRelevant =
|
||||
hasNestedProperties &&
|
||||
hasConnectedOrBrokenDescendant(fieldSchema.properties!, fullKey);
|
||||
|
||||
const shouldShow = connectedOnly
|
||||
? selfIsRelevant || descendantIsRelevant
|
||||
: isOutputVisible || selfIsRelevant || descendantIsRelevant;
|
||||
|
||||
const { displayType, colorClass, hexColor } =
|
||||
getTypeDisplayInfo(fieldSchema);
|
||||
const isBroken = brokenOutputs.has(fullKey);
|
||||
const isExpanded = expandedObjects[fullKey] ?? false;
|
||||
|
||||
// User expanded → show all children; auto-expanded → filter to connected only
|
||||
const shouldRenderChildren = isExpanded || descendantIsRelevant;
|
||||
|
||||
return shouldShow ? (
|
||||
<div
|
||||
@@ -56,6 +97,19 @@ export const OutputHandler = ({
|
||||
data-tutorial-id={`output-handler-${nodeId}-${fieldTitle}`}
|
||||
>
|
||||
<div className="relative flex items-center gap-2">
|
||||
{hasNestedProperties && (
|
||||
<button
|
||||
onClick={() => toggleObjectExpanded(fullKey)}
|
||||
className="flex items-center text-slate-500 hover:text-slate-700"
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<CaretDownIcon size={12} weight="bold" />
|
||||
) : (
|
||||
<CaretRightIcon size={12} weight="bold" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{fieldSchema?.description && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -102,12 +156,14 @@ export const OutputHandler = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recursively render nested properties */}
|
||||
{fieldSchema?.properties &&
|
||||
{/* Nested properties */}
|
||||
{hasNestedProperties &&
|
||||
shouldRenderChildren &&
|
||||
renderOutputHandles(
|
||||
fieldSchema.properties,
|
||||
fieldSchema.properties!,
|
||||
fullKey,
|
||||
`${fieldTitle}.`,
|
||||
"",
|
||||
!isExpanded,
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
@@ -136,7 +192,7 @@ export const OutputHandler = ({
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{renderOutputHandles(properties)}
|
||||
{renderOutputHandles(properties, "", "", !isOutputVisible)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,10 +34,7 @@ export function FormRenderer({
|
||||
}, [preprocessedSchema, uiSchema]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("mb-6 mt-4", className)}
|
||||
data-tutorial-id="input-handles"
|
||||
>
|
||||
<div className={cn("mt-4", className)} data-tutorial-id="input-handles">
|
||||
<Form
|
||||
formContext={formContext}
|
||||
idPrefix="agpt"
|
||||
|
||||
@@ -63,7 +63,6 @@ export const useAnyOfField = (props: FieldProps) => {
|
||||
);
|
||||
|
||||
const handlePrefix = cleanUpHandleId(field_id);
|
||||
console.log("handlePrefix", handlePrefix);
|
||||
useEdgeStore
|
||||
.getState()
|
||||
.removeEdgesByHandlePrefix(registry.formContext.nodeId, handlePrefix);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
TemplatesType,
|
||||
} from "@rjsf/utils";
|
||||
import { AnyOfField } from "./anyof/AnyOfField";
|
||||
import { OneOfField } from "./oneof/OneOfField";
|
||||
import {
|
||||
ArrayFieldItemTemplate,
|
||||
ArrayFieldTemplate,
|
||||
@@ -32,6 +33,7 @@ const NoButton = () => null;
|
||||
export function generateBaseFields(): RegistryFieldsType {
|
||||
return {
|
||||
AnyOfField,
|
||||
OneOfField,
|
||||
ArraySchemaField,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import {
|
||||
descriptionId,
|
||||
FieldProps,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
getWidget,
|
||||
} from "@rjsf/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnyOfField } from "../anyof/AnyOfField";
|
||||
import { cleanUpHandleId, getHandleId, updateUiOption } from "../../helpers";
|
||||
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
|
||||
import { ANY_OF_FLAG } from "../../constants";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function getDiscriminatorPropName(schema: any): string | undefined {
|
||||
if (!schema?.discriminator) return undefined;
|
||||
if (typeof schema.discriminator === "string") return schema.discriminator;
|
||||
return schema.discriminator.propertyName;
|
||||
}
|
||||
|
||||
export function OneOfField(props: FieldProps) {
|
||||
const { schema } = props;
|
||||
|
||||
const discriminatorProp = getDiscriminatorPropName(schema);
|
||||
if (!discriminatorProp) {
|
||||
return <AnyOfField {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DiscriminatedUnionField {...props} discriminatorProp={discriminatorProp} />
|
||||
);
|
||||
}
|
||||
|
||||
interface DiscriminatedUnionFieldProps extends FieldProps {
|
||||
discriminatorProp: string;
|
||||
}
|
||||
|
||||
function DiscriminatedUnionField({
|
||||
discriminatorProp,
|
||||
...props
|
||||
}: DiscriminatedUnionFieldProps) {
|
||||
const { schema, registry, formData, onChange, name } = props;
|
||||
const { fields, schemaUtils, formContext } = registry;
|
||||
const { SchemaField } = fields;
|
||||
const { nodeId } = formContext;
|
||||
|
||||
const field_id = props.fieldPathId.$id;
|
||||
|
||||
// Resolve variant schemas from $refs
|
||||
const variants = useRef(
|
||||
(schema.oneOf || []).map((opt: any) =>
|
||||
schemaUtils.retrieveSchema(opt, formData),
|
||||
),
|
||||
);
|
||||
|
||||
// Build dropdown options from variant titles and discriminator const values
|
||||
const enumOptions = variants.current.map((variant: any, index: number) => {
|
||||
const discValue = (variant.properties?.[discriminatorProp] as any)?.const;
|
||||
return {
|
||||
value: index,
|
||||
label: variant.title || discValue || `Option ${index + 1}`,
|
||||
discriminatorValue: discValue,
|
||||
};
|
||||
});
|
||||
|
||||
// Determine initial selected index from formData
|
||||
function getInitialIndex() {
|
||||
const currentDisc = formData?.[discriminatorProp];
|
||||
if (currentDisc) {
|
||||
const idx = enumOptions.findIndex(
|
||||
(o) => o.discriminatorValue === currentDisc,
|
||||
);
|
||||
if (idx >= 0) return idx;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(getInitialIndex);
|
||||
|
||||
// Generate handleId for sub-fields (same convention as AnyOfField)
|
||||
const uiOptions = getUiOptions(props.uiSchema, props.globalUiOptions);
|
||||
const handleId = getHandleId({
|
||||
uiOptions,
|
||||
id: field_id + ANY_OF_FLAG,
|
||||
schema,
|
||||
});
|
||||
|
||||
const childUiSchema = updateUiOption(props.uiSchema, {
|
||||
handleId,
|
||||
label: false,
|
||||
fromAnyOf: true,
|
||||
});
|
||||
|
||||
// Get selected variant schema with discriminator property filtered out
|
||||
// and sub-fields inheriting the parent's advanced value
|
||||
const selectedVariant = variants.current[selectedIndex];
|
||||
const parentAdvanced = (schema as any).advanced;
|
||||
|
||||
function getFilteredSchema() {
|
||||
if (!selectedVariant?.properties) return selectedVariant;
|
||||
const filteredProperties: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(selectedVariant.properties)) {
|
||||
if (key === discriminatorProp) continue;
|
||||
filteredProperties[key] =
|
||||
parentAdvanced !== undefined
|
||||
? { ...(value as any), advanced: parentAdvanced }
|
||||
: value;
|
||||
}
|
||||
return {
|
||||
...selectedVariant,
|
||||
properties: filteredProperties,
|
||||
required: (selectedVariant.required || []).filter(
|
||||
(r: string) => r !== discriminatorProp,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const filteredSchema = getFilteredSchema();
|
||||
|
||||
// Handle variant change
|
||||
function handleVariantChange(option?: string) {
|
||||
const newIndex = option !== undefined ? parseInt(option, 10) : -1;
|
||||
if (newIndex === selectedIndex || newIndex < 0) return;
|
||||
|
||||
const newVariant = variants.current[newIndex];
|
||||
const oldVariant = variants.current[selectedIndex];
|
||||
const discValue = (newVariant.properties?.[discriminatorProp] as any)
|
||||
?.const;
|
||||
|
||||
// Clean edges for this field
|
||||
const handlePrefix = cleanUpHandleId(field_id);
|
||||
useEdgeStore.getState().removeEdgesByHandlePrefix(nodeId, handlePrefix);
|
||||
|
||||
// Sanitize current data against old→new schema to preserve shared fields
|
||||
let newFormData = schemaUtils.sanitizeDataForNewSchema(
|
||||
newVariant,
|
||||
oldVariant,
|
||||
formData,
|
||||
);
|
||||
|
||||
// Fill in defaults for the new variant
|
||||
newFormData = schemaUtils.getDefaultFormState(
|
||||
newVariant,
|
||||
newFormData,
|
||||
"excludeObjectChildren",
|
||||
) as any;
|
||||
newFormData = { ...newFormData, [discriminatorProp]: discValue };
|
||||
|
||||
setSelectedIndex(newIndex);
|
||||
onChange(newFormData, props.fieldPathId.path, undefined, field_id);
|
||||
}
|
||||
|
||||
// Sync selectedIndex when formData discriminator changes externally
|
||||
// (e.g. undo/redo, loading saved state)
|
||||
const currentDiscValue = formData?.[discriminatorProp];
|
||||
useEffect(() => {
|
||||
const idx = currentDiscValue
|
||||
? enumOptions.findIndex((o) => o.discriminatorValue === currentDiscValue)
|
||||
: -1;
|
||||
|
||||
if (idx >= 0) {
|
||||
if (idx !== selectedIndex) setSelectedIndex(idx);
|
||||
} else if (enumOptions.length > 0 && selectedIndex !== 0) {
|
||||
// Unknown or cleared discriminator — full reset via same cleanup path
|
||||
handleVariantChange("0");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentDiscValue]);
|
||||
|
||||
// Auto-set discriminator on initial render if missing
|
||||
useEffect(() => {
|
||||
const discValue = enumOptions[selectedIndex]?.discriminatorValue;
|
||||
if (discValue && formData?.[discriminatorProp] !== discValue) {
|
||||
onChange(
|
||||
{ ...formData, [discriminatorProp]: discValue },
|
||||
props.fieldPathId.path,
|
||||
undefined,
|
||||
field_id,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const Widget = getWidget({ type: "string" }, "select", registry.widgets);
|
||||
|
||||
const selector = (
|
||||
<Widget
|
||||
id={field_id}
|
||||
name={`${name}__oneof_select`}
|
||||
schema={{ type: "number", default: 0 }}
|
||||
onChange={handleVariantChange}
|
||||
onBlur={props.onBlur}
|
||||
onFocus={props.onFocus}
|
||||
disabled={props.disabled || enumOptions.length === 0}
|
||||
multiple={false}
|
||||
value={selectedIndex}
|
||||
options={{ enumOptions }}
|
||||
registry={registry}
|
||||
placeholder={props.placeholder}
|
||||
autocomplete={props.autocomplete}
|
||||
className={cn("-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}
|
||||
/>
|
||||
);
|
||||
|
||||
const DescriptionFieldTemplate = getTemplate(
|
||||
"DescriptionFieldTemplate",
|
||||
registry,
|
||||
uiOptions,
|
||||
);
|
||||
const description_id = descriptionId(props.fieldPathId ?? "");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Text variant="body" className="line-clamp-1">
|
||||
{schema.title || name}
|
||||
</Text>
|
||||
<Text variant="small" className="mr-1 text-red-500">
|
||||
{props.required ? "*" : null}
|
||||
</Text>
|
||||
{selector}
|
||||
<DescriptionFieldTemplate
|
||||
id={description_id}
|
||||
description={schema.description || ""}
|
||||
schema={schema}
|
||||
registry={registry}
|
||||
/>
|
||||
</div>
|
||||
{filteredSchema && filteredSchema.type !== "null" && (
|
||||
<SchemaField
|
||||
{...props}
|
||||
schema={filteredSchema}
|
||||
uiSchema={childUiSchema}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
titleId,
|
||||
} from "@rjsf/utils";
|
||||
|
||||
import { isAnyOfChild, isAnyOfSchema } from "../../utils/schema-utils";
|
||||
import {
|
||||
isAnyOfChild,
|
||||
isAnyOfSchema,
|
||||
isOneOfSchema,
|
||||
} from "../../utils/schema-utils";
|
||||
import {
|
||||
cleanUpHandleId,
|
||||
getHandleId,
|
||||
@@ -82,12 +86,13 @@ export default function FieldTemplate(props: FieldTemplateProps) {
|
||||
const shouldDisplayLabel =
|
||||
displayLabel ||
|
||||
(schema.type === "boolean" && !isAnyOfChild(uiSchema as any));
|
||||
const shouldShowTitleSection = !isAnyOfSchema(schema) && !additional;
|
||||
const isUnionSchema = isAnyOfSchema(schema) || isOneOfSchema(schema);
|
||||
const shouldShowTitleSection = !isUnionSchema && !additional;
|
||||
|
||||
const shouldShowChildren =
|
||||
schema.type === "object" ||
|
||||
schema.type === "array" ||
|
||||
isAnyOfSchema(schema) ||
|
||||
isUnionSchema ||
|
||||
!isHandleConnected;
|
||||
|
||||
const isAdvancedField = (schema as any).advanced === true;
|
||||
@@ -95,8 +100,7 @@ export default function FieldTemplate(props: FieldTemplateProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marginBottom =
|
||||
isPartOfAnyOf({ uiOptions }) || isAnyOfSchema(schema) ? 0 : 16;
|
||||
const marginBottom = isPartOfAnyOf({ uiOptions }) || isUnionSchema ? 0 : 16;
|
||||
|
||||
return (
|
||||
<WrapIfAdditionalTemplate
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
|
||||
import { isAnyOfSchema } from "../../utils/schema-utils";
|
||||
import { isAnyOfSchema, isOneOfSchema } from "../../utils/schema-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cleanUpHandleId, isArrayItem } from "../../helpers";
|
||||
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
|
||||
@@ -18,7 +18,7 @@ export default function TitleField(props: TitleFieldProps) {
|
||||
const { nodeId, showHandles } = registry.formContext;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
|
||||
const isAnyOf = isAnyOfSchema(schema);
|
||||
const isAnyOf = isAnyOfSchema(schema) || isOneOfSchema(schema);
|
||||
const { displayType, colorClass } = getTypeDisplayInfo(schema);
|
||||
const description_id = descriptionId(id);
|
||||
|
||||
|
||||
@@ -8,6 +8,14 @@ export function isAnyOfSchema(schema: RJSFSchema | undefined): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isOneOfSchema(schema: RJSFSchema | undefined): boolean {
|
||||
return (
|
||||
Array.isArray(schema?.oneOf) &&
|
||||
schema!.oneOf.length > 0 &&
|
||||
schema?.enum === undefined
|
||||
);
|
||||
}
|
||||
|
||||
export const isAnyOfChild = (
|
||||
uiSchema: UiSchema<any, RJSFSchema, any> | undefined,
|
||||
): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user