diff --git a/packages/open-workflow-diagram-editor/src/core/schemaToFormFields.ts b/packages/open-workflow-diagram-editor/src/core/schemaToFormFields.ts index 8c089b1c..7ab078b3 100644 --- a/packages/open-workflow-diagram-editor/src/core/schemaToFormFields.ts +++ b/packages/open-workflow-diagram-editor/src/core/schemaToFormFields.ts @@ -15,6 +15,7 @@ */ import type { DereferencedSchema } from "./schemaFilter"; +import type { ContentFormat } from "./workflowSdk"; /** * A single form field descriptor produced by walking a task's JSON Schema. @@ -107,7 +108,7 @@ export interface MapField extends FieldBase { export interface JsonField extends FieldBase { kind: "json"; - format: "json" | "yaml"; + format: ContentFormat; } export interface OneOfField extends FieldBase { @@ -313,7 +314,7 @@ export function schemaToFormFields( defs?: Record, requiredSet?: Set, path = "", - format: "json" | "yaml" = "yaml", + format: ContentFormat = "yaml", ): FormFieldDescriptor[] { const fields: FormFieldDescriptor[] = []; @@ -636,7 +637,7 @@ function buildOneOfVariants( candidates: unknown[], defs: Record | undefined, parentPath: string, - format: "json" | "yaml" = "yaml", + format: ContentFormat = "yaml", ): OneOfVariant[] { const leafPath = parentPath || "__leaf__"; diff --git a/packages/open-workflow-diagram-editor/src/core/schemaWalker.ts b/packages/open-workflow-diagram-editor/src/core/schemaWalker.ts index 5a78968c..d299560b 100644 --- a/packages/open-workflow-diagram-editor/src/core/schemaWalker.ts +++ b/packages/open-workflow-diagram-editor/src/core/schemaWalker.ts @@ -17,6 +17,7 @@ import { GraphNodeType } from "@openworkflowspec/sdk"; import { getSchemaForDefinition } from "./schemaFilter"; import { schemaToFormFields, FormFieldDescriptor } from "./schemaToFormFields"; +import type { ContentFormat } from "./workflowSdk"; // --------------------------------------------------------------------------- // Node-type → schema definition-name mapping @@ -60,7 +61,7 @@ const _fieldCache = new Map(); */ export function getFormFieldsForNodeType( nodeType: string, - format: "json" | "yaml" = "yaml", + format: ContentFormat = "yaml", ): FormFieldDescriptor[] { const cacheKey = `${nodeType}:${format}`; const cached = _fieldCache.get(cacheKey); diff --git a/packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/StringControl.tsx b/packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/StringControl.tsx index a0a27ab7..febc13d0 100644 --- a/packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/StringControl.tsx +++ b/packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/StringControl.tsx @@ -15,7 +15,7 @@ */ import * as React from "react"; -import { Controller, useFormContext, useFormState } from "react-hook-form"; +import { Controller } from "react-hook-form"; import { Input } from "../ui/input"; import type { StringField } from "../../../core/schemaToFormFields"; import { useTaskFormContext } from "../taskFormContext"; @@ -41,70 +41,32 @@ export function StringControl({ field, id }: StringControlProps) { } function SingleLineStringControl({ field, id }: StringControlProps) { - const { control, getValues, getFieldState } = useFormContext>(); const { isReadOnly } = useTaskFormContext(); const errorMessage = useFieldError(field.path); - const { defaultValues } = useFormState({ control }); const placeholder = field.placeholder ?? (field.isRuntimeExpression ? "${...}" : undefined); - // Compute the initial display value. - const [inputValue, setInputValue] = React.useState(() => { - const live = getValues(field.path as never) as unknown; - const wasDirtied = getFieldState(field.path as never).isDirty; - // Stale defaultValues restoration after a kind-boundary switch - if (typeof live === "string" && !wasDirtied) { - const isRe = /^\s*\$\{.+\}\s*$/.test(live); - if (isRe !== field.isRuntimeExpression) { - return ""; - } - } - // Show the live string value, or empty if not a string. - return typeof live === "string" ? live : ""; - }); - - // Reset when the task changes (defaultValues identity) or path/isRuntimeExpression changes. - const prevDefaultValuesRef = React.useRef(defaultValues); - const prevPathRef = React.useRef(field.path); - const prevIsReRef = React.useRef(field.isRuntimeExpression); - React.useEffect(() => { - const pathOrKindChanged = - field.path !== prevPathRef.current || field.isRuntimeExpression !== prevIsReRef.current; - if (defaultValues === prevDefaultValuesRef.current && !pathOrKindChanged) return; - prevDefaultValuesRef.current = defaultValues; - prevPathRef.current = field.path; - prevIsReRef.current = field.isRuntimeExpression; - // Re-derive from the new task state — same logic as the useState initialiser. - const live = getValues(field.path as never) as unknown; - const wasDirtied = getFieldState(field.path as never).isDirty; - if (typeof live === "string" && !wasDirtied) { - const isRe = /^\s*\$\{.+\}\s*$/.test(live); - if (isRe !== field.isRuntimeExpression) { - setInputValue(""); - return; - } - } - setInputValue(typeof live === "string" ? live : ""); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValues, field.path, field.isRuntimeExpression]); - return ( { - const handleChange = (e: React.ChangeEvent) => { - const val = e.target.value; - setInputValue(val); - rhfField.onChange(val); - }; + render={({ field: rhfField, fieldState }) => { + const live = rhfField.value as unknown; + let inputValue = typeof live === "string" ? live : ""; + + if (typeof live === "string" && !fieldState.isDirty) { + const isRuntimeExpression = /^\s*\$\{.+\}\s*$/.test(live); + + if (isRuntimeExpression !== field.isRuntimeExpression) { + inputValue = ""; + } + } return (