feat: Generate task form from schema - #414
Conversation
✅ Deploy Preview for openworkflow-editor ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
🟡 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.nameis 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 toapplyDirtyValues.
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
taskDatachanges. Cancel callsform.reset()without changingtaskData, 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(). BecauseapplyDirtyValuesstarts from the original task and only processes paths still present inallValues, 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.
lornakelly
left a comment
There was a problem hiding this comment.
@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 @@ | |||
| /* | |||
There was a problem hiding this comment.
I would put this file into core folder instead as its pure TS
| */ | ||
|
|
||
| import { GraphNodeType } from "@openworkflowspec/sdk"; | ||
| import { CATCH_CONTAINER_NODE_TYPE } from "@/react-flow/nodes/taskNodeConfig"; |
There was a problem hiding this comment.
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
| 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"; |
There was a problem hiding this comment.
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
| <button | ||
| type="button" | ||
| className="dec-form-field-help" | ||
| aria-label={`Help: ${label}`} |
There was a problem hiding this comment.
If you could move the "Help" string into translations file
| import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
| import { Button } from "@/components/ui/button"; |
There was a problem hiding this comment.
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
| const parsed = parseValue(rawValueStr); | ||
|
|
||
| // Rename: unregister the old key before registering the new one. | ||
| if (current.key !== newKey && current.key !== "") { |
There was a problem hiding this comment.
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
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>
c581b98 to
2e7c66f
Compare
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 thesetTaskdefinition only; other task types will be covered in subsequent issues.Not Addressed in This PR
setTaskis fully functional.Changes
schemaWalkerto navigate the task definition and extract the field structure from the JSON schema.taskDetailsand the base form implementation with a schema-based form generator (schemaToFormFields) and aTaskFormengine.DurationField,KeyValueMapField,ThenField,ChildTaskListField, and scalar controls (StringControl,NumberControl,BooleanControl,EnumControl).useSiblingTaskNameshook to resolve valid transition targets forThenField.useWorkflowErrorsForForm.EditFormFooter.EditableProperties,FieldControls,Fields, and their associated tests, replaced by the new form layer.How to Test
setTaskin both read-only and edit mode.