Skip to content

feat: Generate task form from schema - #414

Open
handreyrc wants to merge 3 commits into
open-workflow-specification:feature/node-editingfrom
handreyrc:form-generation
Open

feat: Generate task form from schema#414
handreyrc wants to merge 3 commits into
open-workflow-specification:feature/node-editingfrom
handreyrc:form-generation

Conversation

@handreyrc

Copy link
Copy Markdown
Contributor

Closes #384

Summary

This PR introduces dynamic task form generation driven by the schema exposed from the SDK. It replaces the base implementation — which relied on the task instance (taskDetails) to generate the form — with an engine that builds the form directly from a filtered task definition in the schema. This work targets the setTask definition only; other task types will be covered in subsequent issues.

Not Addressed in This PR

  • Layered validation (field-level and task-level). TODOs have been added to the code.
  • Full task support — only setTask is fully functional.

Changes

  • Added schemaWalker to navigate the task definition and extract the field structure from the JSON schema.
  • Replaced taskDetails and the base form implementation with a schema-based form generator (schemaToFormFields) and a TaskForm engine.
  • Added custom field components to handle complex types and sub-levels: DurationField, KeyValueMapField, ThenField, ChildTaskListField, and scalar controls (StringControl, NumberControl, BooleanControl, EnumControl).
  • Added useSiblingTaskNames hook to resolve valid transition targets for ThenField.
  • Integrated SDK workflow validation into the new form via useWorkflowErrorsForForm.
  • Preserved the existing footer implementation and dirty field check from EditFormFooter.
  • Read-only and editable form modes driven by the editor's current state, with a filter to show only defined fields in read-only mode.
  • Removed EditableProperties, FieldControls, Fields, and their associated tests, replaced by the new form layer.

How to Test

  • Use the Examples / Workflows → Set Example story in Storybook to validate setTask in both read-only and edit mode.

@netlify

netlify Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploy Preview for openworkflow-editor ready!

Name Link
🔨 Latest commit 2e7c66f
🔍 Latest deploy log https://app.netlify.com/projects/openworkflow-editor/deploys/6aa44c93a5e6850008d128a5
😎 Deploy Preview https://deploy-preview-414--openworkflow-editor.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Map editing can corrupt or retain task data, alongside security and accessibility regressions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces schema-driven task forms, replacing task-instance-derived property rendering.

Changes:

  • Adds schema walking, dynamic fields, custom controls, and SDK error mapping.
  • Integrates form state with Apply/Cancel and read-only modes.
  • Replaces legacy property components and expands tests.
File summaries
File Description
tests/side-panel/SidePanel.test.tsx Updates footer integration coverage.
tests/side-panel/ReadOnlyProperties.test.tsx Removes legacy read-only tests.
tests/side-panel/NodeDetailsView.test.tsx Tests schema form rendering modes.
tests/side-panel/forms/useSiblingTaskNames.test.ts Tests sibling resolution.
tests/side-panel/forms/taskFormContext.test.ts Tests read-only filtering helpers.
tests/side-panel/forms/schemaToFormFields.test.ts Tests schema conversion.
tests/side-panel/forms/NumberControl.test.tsx Tests numeric input behavior.
tests/side-panel/forms/KeyValueMapField.test.tsx Tests map restoration.
tests/side-panel/forms/flattenTask.test.ts Tests task flattening.
tests/side-panel/forms/fieldHelpers.test.tsx Tests field error rendering.
tests/side-panel/forms/DurationField.test.ts Tests duration patterns.
tests/side-panel/FieldControls.test.tsx Removes legacy control tests.
tests/side-panel/EditFormFooter.test.tsx Updates footer workflow tests.
tests/side-panel/EditableProperties.test.tsx Removes legacy editor tests.
tests/fixtures/workflows.ts Adds a set-task fixture.
tests/core/taskDraft.test.ts Tests new draft application.
tests/core/taskDetails.test.ts Removes task-detail tests.
src/side-panel/SidePanel.css Styles generated forms.
src/side-panel/NodeDetailsView.tsx Renders TaskForm.
src/side-panel/forms/validation/useWorkflowErrorsForForm.ts Maps SDK errors to fields.
src/side-panel/forms/validation/index.ts Exports validation hook.
src/side-panel/forms/useSiblingTaskNames.ts Resolves transition targets.
src/side-panel/forms/taskFormContext.ts Provides form context/filtering.
src/side-panel/forms/TaskForm.tsx Implements form engine.
src/side-panel/forms/schemaToFormFields.ts Converts schemas to descriptors.
src/side-panel/forms/FormField.tsx Renders descriptor trees.
src/side-panel/forms/FieldControl.tsx Dispatches scalar controls.
src/side-panel/forms/customFields/ThenField.tsx Adds transition selector.
src/side-panel/forms/customFields/StringControl.tsx Adds string input.
src/side-panel/forms/customFields/ScrollableTextField.tsx Adds multiline input.
src/side-panel/forms/customFields/NumberControl.tsx Adds numeric input.
src/side-panel/forms/customFields/KeyValueMapField.tsx Adds map editor.
src/side-panel/forms/customFields/index.ts Exports custom controls.
src/side-panel/forms/customFields/fieldHelpers.tsx Adds error helpers.
src/side-panel/forms/customFields/EnumControl.tsx Adds enum selector.
src/side-panel/forms/customFields/DurationField.tsx Adds duration input.
src/side-panel/forms/customFields/ChildTaskListField.tsx Displays child tasks.
src/side-panel/forms/customFields/BooleanControl.tsx Adds boolean switch.
src/side-panel/Fields.tsx Removes legacy property rendering.
src/side-panel/FieldControls.tsx Removes legacy controls.
src/side-panel/EditSession.tsx Simplifies shared form state.
src/side-panel/EditFormFooter.tsx Applies schema-form drafts.
src/side-panel/EditableProperties.tsx Removes legacy editor.
src/i18n/locales/en.ts Adds form translations.
src/core/taskDraft.ts Adds dirty-path application.
src/core/taskDetails.ts Removes task-detail extraction.
src/core/schemaWalker.ts Maps node types to schemas.
src/core/index.ts Exports schema utilities.
src/components/ui/textarea.tsx Restyles textareas.
src/components/ui/shadcn.css Adds theme tokens.
src/components/ui/select.tsx Adds styled native select.
src/components/ui/input.tsx Restyles inputs.
src/components/ui/input-group.tsx Adjusts focus styling.
src/components/ui/combobox.tsx Refactors combobox presentation.
.changeset/form-generation.md Records the minor feature release.
Review details

Suppressed comments (4)

packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/KeyValueMapField.tsx:150

  • A map key is user-authored data, not a path segment. Concatenating it into an RHF dot path means a valid key such as user.name is interpreted as nesting and is applied as { user: { name: ... } } instead of preserving the literal key. Store the map as one object value or restore segment-safe encoding throughout flatten/apply.
      if (newKey !== "") {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        setValue(`${field.path}.${newKey}` as any, newValue, { shouldDirty: true });

packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/KeyValueMapField.tsx:169

  • Deleting a row only unregisters it. The apply pipeline iterates values that remain in getValues(), so it never receives a deletion for this path and the cloned original entry survives Apply (the form may also stop being dirty). Keep an explicit dirty tombstone or pass deleted paths to applyDirtyValues.
  const deleteRow = React.useCallback(
    (id: string) => {
      const current = rowsRef.current.find((r) => r.id === id);
      if (current?.key) {
        unregister(`${field.path}.${current.key}`);
      }

packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/KeyValueMapField.tsx:128

  • Rows are synchronized only when taskData changes. Cancel calls form.reset() without changing taskData, so map edits remain in this local state even though Apply becomes disabled and the footer reports a clean form. Subscribe the row state to form resets or move the rows into RHF state so Cancel actually restores the committed map.
  // Re-sync rows whenever the task data changes externally (undo/redo, node
  // switch). `taskData` identity changes whenever TaskForm resets the form.
  const prevTaskDataRef = React.useRef(taskData);
  React.useEffect(() => {
    if (prevTaskDataRef.current === taskData) return;
    prevTaskDataRef.current = taskData;
    setRows(extractEntries(taskData, field.path));
  }, [taskData, field.path]);

packages/open-workflow-diagram-editor/src/side-panel/forms/customFields/KeyValueMapField.tsx:147

  • Renaming an existing entry unregisters the old path, so it disappears from getValues(). Because applyDirtyValues starts from the original task and only processes paths still present in allValues, Apply adds the new key but never removes the old key. Track the old path as an explicit dirty deletion instead of unregistering it.
      // Rename: unregister the old key before registering the new one.
      if (current.key !== newKey && current.key !== "") {
        unregister(`${field.path}.${current.key}`);
      }
  • Files reviewed: 55/55 changed files
  • Comments generated: 6
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/open-workflow-diagram-editor/src/core/taskDraft.ts Outdated
Comment thread packages/open-workflow-diagram-editor/src/components/ui/combobox.tsx Outdated
Comment thread packages/open-workflow-diagram-editor/src/side-panel/EditFormFooter.tsx Outdated
Comment thread packages/open-workflow-diagram-editor/src/side-panel/forms/FormField.tsx Outdated

@lornakelly lornakelly left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@handreyrc Really nice work, there is a lot in this, have added a few comments so far, will review again when those are addressed (havent looked at tests etc yet)

@@ -0,0 +1,835 @@
/*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this file into core folder instead as its pure TS

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

*/

import { GraphNodeType } from "@openworkflowspec/sdk";
import { CATCH_CONTAINER_NODE_TYPE } from "@/react-flow/nodes/taskNodeConfig";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are bringing in react parts here which we dont want in core, taskNodeConfig brings in React ComponentType and lucide-react. Since you only need the string, I would instead move CATCH_CONTAINER_NODE_TYPE into core folder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

import { GraphNodeType } from "@openworkflowspec/sdk";
import { CATCH_CONTAINER_NODE_TYPE } from "@/react-flow/nodes/taskNodeConfig";
import { getSchemaForDefinition } from "./schemaFilter";
import { schemaToFormFields } from "@/side-panel/forms/schemaToFormFields";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also move this into core folder, its a pure TS file and I think makes sense to be in core instead so we flow downward

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

<button
type="button"
className="dec-form-field-help"
aria-label={`Help: ${label}`}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you could move the "Help" string into translations file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These components are shadcn generated so if we need to update in the future, custom logic would be overwritten. Did a bit of reading and can see that there are some options like adding variants but need to research more. Its ok if the styling isnt there at this point

For now wondering if you can revert the changes except for combobox as that is the most involved but add a comment to the file /*DIVERGED FROM UPSTREAM SHADCN: TODO: investigate alternative way to custom style */. Also keep the shadcn.css changes, this is the correct pattern

I can add another ticket and look at best practices for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

const parsed = parseValue(rawValueStr);

// Rename: unregister the old key before registering the new one.
if (current.key !== newKey && current.key !== "") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that there is an issue with dirty tracking when deleting field in map

Screen.Recording.2026-09-11.at.09.43.35.mov

After looking I think its because of unregister, it seems to iterate over the values and if the value is deleted it doesnt get checked again so doesnt get deleted from the clone

If you try doing something like

const clearKey = React.useCallback((key: string) =>{
setValue(`${field.path}.${key}`) as never, undefined as never, {shouldDirty: true})

}, [field.path, setValue])

and then replace the unregister in updateRow and deleteRow:

unregister(`${field.path}.${current.key}`); > clearKey(current.key)

Add clearKey to dependencies list as well and remove unregister

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

Signed-off-by: Handrey Cunha <handrey.cunha@gmail.com>

# Conflicts:
#	packages/open-workflow-diagram-editor/src/i18n/locales/en.ts

# Conflicts:
#	packages/open-workflow-diagram-editor/src/side-panel/EditableProperties.tsx
Signed-off-by: Handrey Cunha <handrey.cunha@gmail.com>
Signed-off-by: Handrey Cunha <handrey.cunha@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants