From 2ff0936f83502b98102fdd40ee0b8525f3be9bbb Mon Sep 17 00:00:00 2001 From: Handrey Cunha Date: Thu, 10 Sep 2026 19:00:29 -0400 Subject: [PATCH 1/4] Add dynamic task form generation driven by JSON schema Signed-off-by: Handrey Cunha # Conflicts: # packages/open-workflow-diagram-editor/src/i18n/locales/en.ts # Conflicts: # packages/open-workflow-diagram-editor/src/side-panel/EditableProperties.tsx --- .changeset/form-generation.md | 5 + .../src/components/ui/combobox.tsx | 68 +- .../src/components/ui/input-group.tsx | 2 +- .../src/components/ui/input.tsx | 2 +- .../src/components/ui/select.tsx | 39 + .../src/components/ui/shadcn.css | 72 ++ .../src/components/ui/textarea.tsx | 2 +- .../src/core/index.ts | 3 +- .../src/core/schemaWalker.ts | 80 ++ .../src/core/taskDetails.ts | 124 --- .../src/core/taskDraft.ts | 194 ++-- .../src/i18n/locales/en.ts | 11 + .../src/side-panel/EditFormFooter.tsx | 140 +-- .../src/side-panel/EditSession.tsx | 30 +- .../src/side-panel/EditableProperties.tsx | 134 --- .../src/side-panel/FieldControls.tsx | 76 -- .../src/side-panel/Fields.tsx | 53 -- .../src/side-panel/NodeDetailsView.tsx | 27 +- .../src/side-panel/SidePanel.css | 360 ++++++++ .../src/side-panel/forms/FieldControl.tsx | 59 ++ .../src/side-panel/forms/FormField.tsx | 344 ++++++++ .../src/side-panel/forms/TaskForm.tsx | 137 +++ .../forms/customFields/BooleanControl.tsx | 52 ++ .../forms/customFields/ChildTaskListField.tsx | 57 ++ .../forms/customFields/DurationField.tsx | 70 ++ .../forms/customFields/EnumControl.tsx | 82 ++ .../forms/customFields/KeyValueMapField.tsx | 282 ++++++ .../forms/customFields/NumberControl.tsx | 84 ++ .../customFields/ScrollableTextField.tsx | 64 ++ .../forms/customFields/StringControl.tsx | 66 ++ .../forms/customFields/ThenField.tsx | 98 ++ .../forms/customFields/fieldHelpers.tsx | 54 ++ .../side-panel/forms/customFields/index.ts | 26 + .../side-panel/forms/schemaToFormFields.ts | 835 ++++++++++++++++++ .../src/side-panel/forms/taskFormContext.ts | 128 +++ .../side-panel/forms/useSiblingTaskNames.ts | 111 +++ .../validation/index.ts} | 16 +- .../validation/useWorkflowErrorsForForm.ts | 67 ++ .../tests/core/taskDetails.test.ts | 353 -------- .../tests/core/taskDraft.test.ts | 169 ++-- .../tests/fixtures/workflows.ts | 17 + .../tests/side-panel/EditFormFooter.test.tsx | 182 ++-- .../side-panel/EditableProperties.test.tsx | 146 --- .../tests/side-panel/FieldControls.test.tsx | 107 --- .../tests/side-panel/NodeDetailsView.test.tsx | 152 ++-- .../side-panel/ReadOnlyProperties.test.tsx | 97 -- .../tests/side-panel/SidePanel.test.tsx | 85 +- .../side-panel/forms/DurationField.test.ts | 67 ++ .../forms/KeyValueMapField.test.tsx | 137 +++ .../side-panel/forms/NumberControl.test.tsx | 115 +++ .../side-panel/forms/fieldHelpers.test.tsx | 83 ++ .../side-panel/forms/flattenTask.test.ts | 135 +++ .../forms/schemaToFormFields.test.ts | 67 ++ .../side-panel/forms/taskFormContext.test.ts | 252 ++++++ .../forms/useSiblingTaskNames.test.ts | 137 +++ 55 files changed, 4720 insertions(+), 1635 deletions(-) create mode 100644 .changeset/form-generation.md create mode 100644 packages/open-workflow-diagram-editor/src/components/ui/select.tsx create mode 100644 packages/open-workflow-diagram-editor/src/core/schemaWalker.ts delete mode 100644 packages/open-workflow-diagram-editor/src/core/taskDetails.ts delete mode 100644 packages/open-workflow-diagram-editor/src/side-panel/EditableProperties.tsx delete mode 100644 packages/open-workflow-diagram-editor/src/side-panel/FieldControls.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/FieldControl.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/FormField.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/TaskForm.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/BooleanControl.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/ChildTaskListField.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/DurationField.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/EnumControl.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/KeyValueMapField.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/NumberControl.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/ScrollableTextField.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/StringControl.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/ThenField.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/fieldHelpers.tsx create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/index.ts create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/schemaToFormFields.ts create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/taskFormContext.ts create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/useSiblingTaskNames.ts rename packages/open-workflow-diagram-editor/src/side-panel/{ReadOnlyProperties.tsx => forms/validation/index.ts} (62%) create mode 100644 packages/open-workflow-diagram-editor/src/side-panel/forms/validation/useWorkflowErrorsForForm.ts delete mode 100644 packages/open-workflow-diagram-editor/tests/core/taskDetails.test.ts delete mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/EditableProperties.test.tsx delete mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/FieldControls.test.tsx delete mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/ReadOnlyProperties.test.tsx create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/DurationField.test.ts create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/KeyValueMapField.test.tsx create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/NumberControl.test.tsx create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/fieldHelpers.test.tsx create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/flattenTask.test.ts create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/schemaToFormFields.test.ts create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/taskFormContext.test.ts create mode 100644 packages/open-workflow-diagram-editor/tests/side-panel/forms/useSiblingTaskNames.test.ts diff --git a/.changeset/form-generation.md b/.changeset/form-generation.md new file mode 100644 index 00000000..65d29697 --- /dev/null +++ b/.changeset/form-generation.md @@ -0,0 +1,5 @@ +--- +"@openworkflowspec/diagram-editor": minor +--- + +Add dynamic task form generation driven by JSON schema. diff --git a/packages/open-workflow-diagram-editor/src/components/ui/combobox.tsx b/packages/open-workflow-diagram-editor/src/components/ui/combobox.tsx index 6e8ecb4e..487e5efa 100644 --- a/packages/open-workflow-diagram-editor/src/components/ui/combobox.tsx +++ b/packages/open-workflow-diagram-editor/src/components/ui/combobox.tsx @@ -22,12 +22,6 @@ import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import { - InputGroup, - InputGroupAddon, - InputGroupButton, - InputGroupInput, -} from "@/components/ui/input-group"; const Combobox = ComboboxPrimitive.Root; @@ -45,7 +39,7 @@ function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Tr {children} ); @@ -55,11 +49,13 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { return ( } - className={cn(className)} + className={cn( + "dec:inline-flex dec:items-center dec:justify-center dec:size-6 dec:rounded-md dec:hover:bg-transparent dec:dark:hover:bg-transparent", + className, + )} {...props} > - + ); } @@ -76,32 +72,36 @@ function ComboboxInput({ showClear?: boolean; }) { return ( - - } {...props} /> - - {showTrigger && ( - - - - )} - {showClear && } - + + + {showClear && } + {showTrigger && ( + + )} {children} - + ); } function ComboboxContent({ className, side = "bottom", - sideOffset = 6, + sideOffset = 4, align = "start", alignOffset = 0, anchor, @@ -111,8 +111,12 @@ function ComboboxContent({ ComboboxPrimitive.Positioner.Props, "side" | "align" | "sideOffset" | "alignOffset" | "anchor" >) { + const [container, setContainer] = React.useState(null); + React.useEffect(() => { + setContainer(document.querySelector(".dec-root")); + }, []); return ( - + ) ) { type={type} data-slot="input" className={cn( - "dec:h-9 dec:w-full dec:min-w-0 dec:rounded-md dec:border dec:border-input dec:bg-transparent dec:px-3 dec:py-1 dec:text-base dec:shadow-xs dec:transition-[color,box-shadow] dec:outline-none dec:selection:bg-primary dec:selection:text-primary-foreground dec:file:inline-flex dec:file:h-7 dec:file:border-0 dec:file:bg-transparent dec:file:text-sm dec:file:font-medium dec:file:text-foreground dec:placeholder:text-muted-foreground dec:disabled:pointer-events-none dec:disabled:cursor-not-allowed dec:disabled:opacity-50 dec:md:text-sm dec:dark:bg-input/30", + "dec:h-7 dec:w-full dec:min-w-0 dec:rounded-md dec:border dec:border-input dec:bg-transparent dec:px-2 dec:py-0.5 dec:text-xs dec:shadow-xs dec:transition-[color,box-shadow] dec:outline-none dec:selection:bg-primary dec:selection:text-primary-foreground dec:file:inline-flex dec:file:h-6 dec:file:border-0 dec:file:bg-transparent dec:file:text-xs dec:file:font-medium dec:file:text-foreground dec:placeholder:text-muted-foreground dec:disabled:pointer-events-none dec:disabled:cursor-not-allowed dec:disabled:opacity-50 dec:dark:bg-input/30", "dec:focus-visible:border-ring dec:focus-visible:ring-[3px] dec:focus-visible:ring-ring/50", "dec:aria-invalid:border-destructive dec:aria-invalid:ring-destructive/20 dec:dark:aria-invalid:ring-destructive/40", className, diff --git a/packages/open-workflow-diagram-editor/src/components/ui/select.tsx b/packages/open-workflow-diagram-editor/src/components/ui/select.tsx new file mode 100644 index 00000000..b8f5e78d --- /dev/null +++ b/packages/open-workflow-diagram-editor/src/components/ui/select.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +/** + * Styled native ` + ); +} + +export { Select }; diff --git a/packages/open-workflow-diagram-editor/src/components/ui/shadcn.css b/packages/open-workflow-diagram-editor/src/components/ui/shadcn.css index 1fc2d5b5..eaf53025 100644 --- a/packages/open-workflow-diagram-editor/src/components/ui/shadcn.css +++ b/packages/open-workflow-diagram-editor/src/components/ui/shadcn.css @@ -31,10 +31,34 @@ --background: hsl(0 0% 100%); + --foreground: hsl(222.2 84% 4.9%); + --border: hsl(220 13% 91%); --input: hsl(220 13% 91%); + --ring: #2563eb; + + --muted: hsl(210 40% 96.1%); + + --muted-foreground: hsl(215.4 16.3% 46.9%); + + --destructive: hsl(0 84.2% 60.2%); + + --destructive-foreground: hsl(210 40% 98%); + + --secondary: hsl(210 40% 96.1%); + + --secondary-foreground: hsl(222.2 47.4% 11.2%); + + --card: hsl(0 0% 100%); + + --card-foreground: hsl(222.2 84% 4.9%); + + --popover: hsl(0 0% 100%); + + --popover-foreground: hsl(222.2 84% 4.9%); + --accent: hsl(240 4.8% 95.9%); --accent-foreground: hsl(240 5.9% 10%); @@ -67,10 +91,34 @@ --background: var(--dec-surface-dark); + --foreground: hsl(210 40% 98%); + --border: var(--dec-surface-dark-line); --input: var(--dec-surface-dark-line); + --ring: #3b82f6; + + --muted: var(--dec-surface-dark-elevated); + + --muted-foreground: hsl(215 20.2% 65.1%); + + --destructive: hsl(0 62.8% 30.6%); + + --destructive-foreground: hsl(210 40% 98%); + + --secondary: var(--dec-surface-dark-elevated); + + --secondary-foreground: hsl(210 40% 98%); + + --card: var(--dec-surface-dark-raised); + + --card-foreground: hsl(210 40% 98%); + + --popover: var(--dec-surface-dark-raised); + + --popover-foreground: hsl(210 40% 98%); + --accent: var(--dec-surface-dark-line); --accent-foreground: hsl(0 0% 98%); @@ -103,10 +151,34 @@ --color-background: var(--background); + --color-foreground: var(--foreground); + --color-border: var(--border); --color-input: var(--input); + --color-ring: var(--ring); + + --color-muted: var(--muted); + + --color-muted-foreground: var(--muted-foreground); + + --color-destructive: var(--destructive); + + --color-destructive-foreground: var(--destructive-foreground); + + --color-secondary: var(--secondary); + + --color-secondary-foreground: var(--secondary-foreground); + + --color-card: var(--card); + + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + + --color-popover-foreground: var(--popover-foreground); + --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); diff --git a/packages/open-workflow-diagram-editor/src/components/ui/textarea.tsx b/packages/open-workflow-diagram-editor/src/components/ui/textarea.tsx index ce505105..749205b1 100644 --- a/packages/open-workflow-diagram-editor/src/components/ui/textarea.tsx +++ b/packages/open-workflow-diagram-editor/src/components/ui/textarea.tsx @@ -23,7 +23,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {