Theia-native Node Properties Dialog using @eclipse-emfcloud/jsonforms-property-view
Context
continuum-workbench (a Yarn/Turborepo monorepo containing an Eclipse Theia app) currently edits node properties through workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx — a plain MUI Dialog (not Theia-native) that embeds @jsonforms/react's <JsonForms> directly with @jsonforms/material-renderers. There is already a dead Theia-native placeholder, ContinuumNodeDialog.tsx (a ReactDialog<void> that just renders <div>Node Dialog</div> and is never opened anywhere), bound in WorkflowEditorExtentionModule.ts but unused.
The goal: bring ContinuumNodeDialog to life as the real, Theia-native properties dialog, rendering the JSONForms form via @eclipse-emfcloud/jsonforms-property-view's JsonFormsPropertyViewWidget, and fully replace the MUI NodeDialog (per user decision). Research established:
@eclipse-emfcloud/jsonforms-property-view's PropertyDataService/WidgetProvider layer is built for Theia's selection-driven sidebar Properties panel and is not needed/wanted here.
- Its actual renderer,
JsonFormsPropertyViewWidget (widget.tsx), is a plain BaseWidget that can be driven imperatively — updatePropertyViewContent(dataService, selection) — with no dependency on SelectionService. This is the reusable part for a modal.
- Its
renderForms() method hardcodes @jsonforms/vanilla-renderers (vanillaCells/vanillaRenderers) but is protected, so it can be overridden in a subclass. User decision: keep the library's default vanilla-renderers (not swap to Material) — accepting the Theia-native visual style. The two existing custom controls (CodeEditorControl, CredentialControl) must still be prepended to the renderer list in the override, since they're tester-matched (format === 'code' | 'credential') and independent of the base renderer set — losing them would regress KotlinScript/credentialed nodes.
- User decisions: (1) fully replace
NodeDialog.tsx and rewire the existing double-click / "Open node settings" triggers in WorkflowEditor.tsx; (2) port the hand-built "Retry Policy" tab over as-is (it's not JSONForms-driven, keep its MUI implementation); (3) use the library's default vanilla-renderers.
Files
Modify in place:
workflow-editor-extension/src/browser/dialog/node-dialog/ContinuumNodeDialog.tsx — replace the placeholder with the real ReactDialog<ContinuumNodeDialogResult | undefined>.
workflow-editor-extension/src/browser/WorkflowEditorExtentionModule.ts — add binding for the new widget class; keep existing ContinuumNodeDialog/ContinuumNodeDialogProps bindings (update title to "Node Settings").
workflow-editor-extension/src/browser/components/workflow-editor/WorkflowEditor.tsx — replace nodeDialogProps state + inline <NodeDialog> render with an injected openNodeDialog(node, readOnly) callback prop, awaited from onNodeDoubleClick and the openNodeSettings imperative handle.
workflow-editor-extension/src/browser/widgets/workflow-editor/WorkflowEditorWidget.tsx — @inject(ContinuumNodeDialog), implement openNodeDialog, pass it to <WorkflowEditor>.
workflow-editor-extension/package.json — add @eclipse-emfcloud/jsonforms-property-view, @jsonforms/vanilla-renderers (pin 3.7.0 to match existing @jsonforms/core/react), @theia/property-view.
continuum-workbench/package.json and continuum-workbench-thin/package.json — add @theia/property-view as a direct dependency (Theia's build only discovers theiaExtensions contributions from packages the app directly depends on, same reason @theia/filesystem/@theia/workspace are listed there today even though they're transitively pulled in by the extension).
New:
workflow-editor-extension/src/browser/dialog/node-dialog/ContinuumJsonFormsPropertyWidget.tsx — subclass of the library's JsonFormsPropertyViewWidget, overriding renderForms() to prepend codeEditorTester/CodeEditorControl and credentialTester/CredentialControl ahead of vanillaRenderers, keeping vanillaCells.
workflow-editor-extension/src/browser/dialog/node-dialog/NodePropertyDataService.ts — a minimal one-off JsonFormsPropertyDataService implementation (getSchema/getUiSchema/providePropertyData reading node.data.propertiesSchema / .propertiesUISchema / .properties); no DI binding needed, instantiated directly inside ContinuumNodeDialog.
workflow-editor-extension/src/browser/dialog/node-dialog/RetryPolicyPanel.tsx — the ported MUI "Retry Policy" tab, extracted near-verbatim from the current NodeDialog.tsx (lines ~53-118, 196-232, 436-598: WORKFLOW_DEFAULT_RETRY, DURATION_UNITS, pickBestUnit, DurationInput, override-toggle state, and the tab's JSX), as a standalone component taking { retryOptions, onChange, readOnly }.
Delete (after wiring/parity confirmed):
workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx.
CodeEditorRenderer.tsx / CredentialRenderer.tsx are kept, just imported from their new call site in ContinuumJsonFormsPropertyWidget.tsx instead of NodeDialog.tsx.
Implementation outline
-
Dependencies. Add the three packages above. Confirm after yarn install that @theia/core/filesystem/workspace resolve within the library's peer range (^1.74.1); today's yarn.lock already resolves @theia/[email protected], so this should be satisfied, but re-check since these are all pinned "latest" and can drift.
-
NodePropertyDataService.ts — implements the library's JsonFormsPropertyDataService interface against a React Flow Node<IBaseNodeData> passed in as the "selection":
providePropertyData(selection) → selection.data.properties ?? {}
getSchema(selection) → selection.data.propertiesSchema ?? {}
getUiSchema(selection) → selection.data.propertiesUISchema ?? {}
No canHandleSelection scoring logic needed — always return a constant score, it's never compared against competitors here.
-
ContinuumJsonFormsPropertyWidget.tsx — extends JsonFormsPropertyViewWidget, overrides renderForms(properties, typeSchema, uiSchema) to render <JsonForms> with renderers={[{tester: codeEditorTester, renderer: CodeEditorControl}, {tester: credentialTester, renderer: CredentialControl}, ...vanillaRenderers]} and cells={vanillaCells}. Verify at implementation time: confirm the exact protected member names used internally (host React root, style-context getter, the bound onChange handler passed to <JsonForms>) against the installed package's actual widget.tsx — the names used above are inferred from a partial fetch of the source and must be checked once the dependency is installed. If any needed member turns out to be private rather than protected, fall back to re-implementing the equivalent ~10-line renderForms body directly against the widget's public API (composition) rather than relying on an inherited private field.
-
RetryPolicyPanel.tsx — lift the retry-policy constants/helpers and JSX out of NodeDialog.tsx unchanged, parameterized as { retryOptions: IRetryOptions; onChange: (r: IRetryOptions) => void; readOnly: boolean }, owning its own overrides toggle state internally (same behavior as today).
-
ContinuumNodeDialog.tsx — the real dialog:
export interface ContinuumNodeDialogResult { properties: any; retryOptions?: IRetryOptions }
class ContinuumNodeDialog extends ReactDialog<ContinuumNodeDialogResult>, constructed with title: "Node Settings", this.appendAcceptButton(Dialog.OK) (Theia-native OK/Cancel chrome replaces the old dialog's custom DialogActions/Save/Cancel buttons and the draggable-resize/maximize chrome — an intentional simplification of going Theia-native, flagged here since it's a visible behavior change from today's dialog).
setNode(node: FlowNode<IBaseNodeData>, readOnly: boolean): void — public method called by the caller before .open(); stores the node, resets currentProperties/currentRetryOptions/hasErrors from node.data, calls this.update().
render() returns MUI Tabs/Tab ("Properties" / "Retry Policy") + two persistent panes (both always mounted, visibility toggled via CSS display, so the attached JSONForms widget's DOM node is never torn down by a tab switch):
- Properties pane: a
<div ref={this.handlePropertiesHost}>. The ref callback calls Widget.attach(this.jsonFormsWidget, el) exactly once (guarded by a flag) — mirrors how @theia/property-view's own PropertyViewWidget attaches JsonFormsPropertyViewWidget, just done directly instead of through the selection/provider layer.
- Retry Policy pane:
<RetryPolicyPanel retryOptions={this.currentRetryOptions} onChange={...} readOnly={...} />.
@inject(ContinuumJsonFormsPropertyWidget) protected readonly jsonFormsWidget — on setNode(), call await this.jsonFormsWidget.updatePropertyViewContent(new NodePropertyDataService(), node).
- Subscribe once (e.g. in
@postConstruct) to this.jsonFormsWidget.onChange(data => { this.currentProperties = data; this.recomputeErrors(); this.validate(); }). Verify at implementation time whether the event payload is bare data or {data, errors} — if errors aren't included, compute hasErrors locally using @jsonforms/core's createAjv(schema).validate(schema, data) against the schema from NodePropertyDataService, mirroring the AJV-based validation NodeDialog.tsx got for free from <JsonForms onChange> today.
- Override
isValid(value): boolean { return !this.hasErrors; } so Theia's native OK button enables/disables itself (replacing the old disabled={hasErrors || readOnly} prop on the MUI Save button).
get value(): ContinuumNodeDialogResult { return { properties: this.currentProperties, retryOptions: this.currentRetryOptions }; }
- Dispose: on the dialog's
dispose(), Widget.detach(this.jsonFormsWidget) if attached (the dialog and its jsonFormsWidget are both DI singletons reused across opens, so detach only needs to happen on true disposal, not on every close).
-
Wiring into the module — WorkflowEditorExtentionModule.ts: add bind(ContinuumJsonFormsPropertyWidget).toSelf().inSingletonScope(); next to the existing ContinuumNodeDialog binding; update ContinuumNodeDialogProps constant value's title to "Node Settings".
-
Wiring into the editor — WorkflowEditorWidget.tsx: inject ContinuumNodeDialog, add
private openNodeDialog = async (node: FlowNode<IBaseNodeData>, readOnly: boolean) => {
this.nodeDialog.setNode(node, readOnly);
return this.nodeDialog.open(); // undefined on Cancel/Escape, per Theia's AbstractDialog contract
};
pass as a new openNodeDialog prop to <WorkflowEditor>.
WorkflowEditor.tsx: remove nodeDialogProps state and the inline {nodeDialogProps && <NodeDialog .../>} render. In onNodeDoubleClick and the openNodeSettings imperative-handle method, replace the setNodeDialogProps(...) call with await props.openNodeDialog(node, isActive), then apply the result exactly as today's onNodeDialogSaved does — mutate selectedNode.data.properties/.retryOptions in place and call setFlowNodes(flowNodes) — only if the result isn't undefined (Cancel). This preserves the existing (mutate-then-same-reference) update pattern rather than changing it, since fixing that isn't in scope here.
-
Cleanup — once the new dialog is verified end-to-end, delete NodeDialog.tsx and its now-unused NodeDialogProps export; CodeEditorRenderer.tsx/CredentialRenderer.tsx stay, only their import site changes.
Known side effects / trade-offs to call out
- Adding
@theia/property-view as a direct app dependency will also register Theia's generic sidebar "Properties" view (empty/unused, since no PropertyDataService/WidgetProvider are bound for it) — harmless but visible as a new, currently-empty view in the app's view registry.
- Visual style changes from today's custom MUI dialog: Theia-native title bar/OK-Cancel buttons (no more custom drag-resize handle, maximize/restore, or custom-styled
DialogActions), and vanilla-renderers styling for standard JSONForms controls (Theia CSS classes) instead of Material — both are accepted consequences of the "Theia-native + library defaults" decisions made above, not regressions to fix.
- The two custom controls (Monaco code editor, credential
Autocomplete) still render with their own MUI/Monaco styling regardless of the vanilla base — a pre-existing minor visual inconsistency (Monaco already looked distinct from the rest of the form today), not something introduced by this change.
Verification
yarn install && yarn build at the continuum-workbench root — confirms the new dependencies resolve and the extension/app packages compile.
yarn start:workbench, open http://localhost:3002, open a workflow, double-click a node:
- Confirm the dialog opens as a native Theia dialog (Theia title bar, OK/Cancel buttons) rendering the node's schema-driven properties via vanilla-renderers.
- Test a node using the code-editor control (e.g. a KotlinScript node) and a credentialed node — confirm both custom controls still render and function.
- Introduce a validation error (e.g. clear a required field) and confirm the OK button disables; fix it and confirm it re-enables.
- Switch to the "Retry Policy" tab, toggle a field to "Custom", change a value, save — confirm it persists on the node and survives reopening the dialog.
- Cancel/Escape the dialog after making edits — confirm no changes are applied to the node.
- Use the "Open node settings" trigger (toolbar/context menu path) in addition to double-click, confirming both call sites work.
- Confirm the workflow's dirty/save flow (
WorkflowDocument.save()) still round-trips the edited properties/retryOptions correctly to the persisted workflow JSON.
Theia-native Node Properties Dialog using
@eclipse-emfcloud/jsonforms-property-viewContext
continuum-workbench(a Yarn/Turborepo monorepo containing an Eclipse Theia app) currently edits node properties throughworkflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx— a plain MUIDialog(not Theia-native) that embeds@jsonforms/react's<JsonForms>directly with@jsonforms/material-renderers. There is already a dead Theia-native placeholder,ContinuumNodeDialog.tsx(aReactDialog<void>that just renders<div>Node Dialog</div>and is never opened anywhere), bound inWorkflowEditorExtentionModule.tsbut unused.The goal: bring
ContinuumNodeDialogto life as the real, Theia-native properties dialog, rendering the JSONForms form via@eclipse-emfcloud/jsonforms-property-view'sJsonFormsPropertyViewWidget, and fully replace the MUINodeDialog(per user decision). Research established:@eclipse-emfcloud/jsonforms-property-view'sPropertyDataService/WidgetProviderlayer is built for Theia's selection-driven sidebar Properties panel and is not needed/wanted here.JsonFormsPropertyViewWidget(widget.tsx), is a plainBaseWidgetthat can be driven imperatively —updatePropertyViewContent(dataService, selection)— with no dependency onSelectionService. This is the reusable part for a modal.renderForms()method hardcodes@jsonforms/vanilla-renderers(vanillaCells/vanillaRenderers) but isprotected, so it can be overridden in a subclass. User decision: keep the library's default vanilla-renderers (not swap to Material) — accepting the Theia-native visual style. The two existing custom controls (CodeEditorControl,CredentialControl) must still be prepended to the renderer list in the override, since they're tester-matched (format === 'code' | 'credential') and independent of the base renderer set — losing them would regress KotlinScript/credentialed nodes.NodeDialog.tsxand rewire the existing double-click / "Open node settings" triggers inWorkflowEditor.tsx; (2) port the hand-built "Retry Policy" tab over as-is (it's not JSONForms-driven, keep its MUI implementation); (3) use the library's default vanilla-renderers.Files
Modify in place:
workflow-editor-extension/src/browser/dialog/node-dialog/ContinuumNodeDialog.tsx— replace the placeholder with the realReactDialog<ContinuumNodeDialogResult | undefined>.workflow-editor-extension/src/browser/WorkflowEditorExtentionModule.ts— add binding for the new widget class; keep existingContinuumNodeDialog/ContinuumNodeDialogPropsbindings (update title to "Node Settings").workflow-editor-extension/src/browser/components/workflow-editor/WorkflowEditor.tsx— replacenodeDialogPropsstate + inline<NodeDialog>render with an injectedopenNodeDialog(node, readOnly)callback prop, awaited fromonNodeDoubleClickand theopenNodeSettingsimperative handle.workflow-editor-extension/src/browser/widgets/workflow-editor/WorkflowEditorWidget.tsx—@inject(ContinuumNodeDialog), implementopenNodeDialog, pass it to<WorkflowEditor>.workflow-editor-extension/package.json— add@eclipse-emfcloud/jsonforms-property-view,@jsonforms/vanilla-renderers(pin3.7.0to match existing@jsonforms/core/react),@theia/property-view.continuum-workbench/package.jsonandcontinuum-workbench-thin/package.json— add@theia/property-viewas a direct dependency (Theia's build only discoverstheiaExtensionscontributions from packages the app directly depends on, same reason@theia/filesystem/@theia/workspaceare listed there today even though they're transitively pulled in by the extension).New:
workflow-editor-extension/src/browser/dialog/node-dialog/ContinuumJsonFormsPropertyWidget.tsx— subclass of the library'sJsonFormsPropertyViewWidget, overridingrenderForms()to prependcodeEditorTester/CodeEditorControlandcredentialTester/CredentialControlahead ofvanillaRenderers, keepingvanillaCells.workflow-editor-extension/src/browser/dialog/node-dialog/NodePropertyDataService.ts— a minimal one-offJsonFormsPropertyDataServiceimplementation (getSchema/getUiSchema/providePropertyDatareadingnode.data.propertiesSchema/.propertiesUISchema/.properties); no DI binding needed, instantiated directly insideContinuumNodeDialog.workflow-editor-extension/src/browser/dialog/node-dialog/RetryPolicyPanel.tsx— the ported MUI "Retry Policy" tab, extracted near-verbatim from the currentNodeDialog.tsx(lines ~53-118, 196-232, 436-598:WORKFLOW_DEFAULT_RETRY,DURATION_UNITS,pickBestUnit,DurationInput, override-toggle state, and the tab's JSX), as a standalone component taking{ retryOptions, onChange, readOnly }.Delete (after wiring/parity confirmed):
workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx.CodeEditorRenderer.tsx/CredentialRenderer.tsxare kept, just imported from their new call site inContinuumJsonFormsPropertyWidget.tsxinstead ofNodeDialog.tsx.Implementation outline
Dependencies. Add the three packages above. Confirm after
yarn installthat@theia/core/filesystem/workspaceresolve within the library's peer range (^1.74.1); today'syarn.lockalready resolves@theia/[email protected], so this should be satisfied, but re-check since these are all pinned"latest"and can drift.NodePropertyDataService.ts— implements the library'sJsonFormsPropertyDataServiceinterface against a React FlowNode<IBaseNodeData>passed in as the "selection":No
canHandleSelectionscoring logic needed — always return a constant score, it's never compared against competitors here.ContinuumJsonFormsPropertyWidget.tsx— extendsJsonFormsPropertyViewWidget, overridesrenderForms(properties, typeSchema, uiSchema)to render<JsonForms>withrenderers={[{tester: codeEditorTester, renderer: CodeEditorControl}, {tester: credentialTester, renderer: CredentialControl}, ...vanillaRenderers]}andcells={vanillaCells}. Verify at implementation time: confirm the exact protected member names used internally (host React root, style-context getter, the boundonChangehandler passed to<JsonForms>) against the installed package's actualwidget.tsx— the names used above are inferred from a partial fetch of the source and must be checked once the dependency is installed. If any needed member turns out to beprivaterather thanprotected, fall back to re-implementing the equivalent ~10-linerenderFormsbody directly against the widget's public API (composition) rather than relying on an inherited private field.RetryPolicyPanel.tsx— lift the retry-policy constants/helpers and JSX out ofNodeDialog.tsxunchanged, parameterized as{ retryOptions: IRetryOptions; onChange: (r: IRetryOptions) => void; readOnly: boolean }, owning its ownoverridestoggle state internally (same behavior as today).ContinuumNodeDialog.tsx— the real dialog:export interface ContinuumNodeDialogResult { properties: any; retryOptions?: IRetryOptions }class ContinuumNodeDialog extends ReactDialog<ContinuumNodeDialogResult>, constructed withtitle: "Node Settings",this.appendAcceptButton(Dialog.OK)(Theia-native OK/Cancel chrome replaces the old dialog's customDialogActions/Save/Cancel buttons and the draggable-resize/maximize chrome — an intentional simplification of going Theia-native, flagged here since it's a visible behavior change from today's dialog).setNode(node: FlowNode<IBaseNodeData>, readOnly: boolean): void— public method called by the caller before.open(); stores the node, resetscurrentProperties/currentRetryOptions/hasErrorsfromnode.data, callsthis.update().render()returns MUITabs/Tab("Properties" / "Retry Policy") + two persistent panes (both always mounted, visibility toggled via CSSdisplay, so the attached JSONForms widget's DOM node is never torn down by a tab switch):<div ref={this.handlePropertiesHost}>. The ref callback callsWidget.attach(this.jsonFormsWidget, el)exactly once (guarded by a flag) — mirrors how@theia/property-view's ownPropertyViewWidgetattachesJsonFormsPropertyViewWidget, just done directly instead of through the selection/provider layer.<RetryPolicyPanel retryOptions={this.currentRetryOptions} onChange={...} readOnly={...} />.@inject(ContinuumJsonFormsPropertyWidget) protected readonly jsonFormsWidget— onsetNode(), callawait this.jsonFormsWidget.updatePropertyViewContent(new NodePropertyDataService(), node).@postConstruct) tothis.jsonFormsWidget.onChange(data => { this.currentProperties = data; this.recomputeErrors(); this.validate(); }). Verify at implementation time whether the event payload is baredataor{data, errors}— if errors aren't included, computehasErrorslocally using@jsonforms/core'screateAjv(schema).validate(schema, data)against the schema fromNodePropertyDataService, mirroring the AJV-based validationNodeDialog.tsxgot for free from<JsonForms onChange>today.isValid(value): boolean { return !this.hasErrors; }so Theia's native OK button enables/disables itself (replacing the olddisabled={hasErrors || readOnly}prop on the MUI Save button).get value(): ContinuumNodeDialogResult { return { properties: this.currentProperties, retryOptions: this.currentRetryOptions }; }dispose(),Widget.detach(this.jsonFormsWidget)if attached (the dialog and itsjsonFormsWidgetare both DI singletons reused across opens, so detach only needs to happen on true disposal, not on every close).Wiring into the module —
WorkflowEditorExtentionModule.ts: addbind(ContinuumJsonFormsPropertyWidget).toSelf().inSingletonScope();next to the existingContinuumNodeDialogbinding; updateContinuumNodeDialogPropsconstant value'stitleto"Node Settings".Wiring into the editor —
WorkflowEditorWidget.tsx: injectContinuumNodeDialog, addpass as a new
openNodeDialogprop to<WorkflowEditor>.WorkflowEditor.tsx: removenodeDialogPropsstate and the inline{nodeDialogProps && <NodeDialog .../>}render. InonNodeDoubleClickand theopenNodeSettingsimperative-handle method, replace thesetNodeDialogProps(...)call withawait props.openNodeDialog(node, isActive), then apply the result exactly as today'sonNodeDialogSaveddoes — mutateselectedNode.data.properties/.retryOptionsin place and callsetFlowNodes(flowNodes)— only if the result isn'tundefined(Cancel). This preserves the existing (mutate-then-same-reference) update pattern rather than changing it, since fixing that isn't in scope here.Cleanup — once the new dialog is verified end-to-end, delete
NodeDialog.tsxand its now-unusedNodeDialogPropsexport;CodeEditorRenderer.tsx/CredentialRenderer.tsxstay, only their import site changes.Known side effects / trade-offs to call out
@theia/property-viewas a direct app dependency will also register Theia's generic sidebar "Properties" view (empty/unused, since noPropertyDataService/WidgetProviderare bound for it) — harmless but visible as a new, currently-empty view in the app's view registry.DialogActions), and vanilla-renderers styling for standard JSONForms controls (Theia CSS classes) instead of Material — both are accepted consequences of the "Theia-native + library defaults" decisions made above, not regressions to fix.Autocomplete) still render with their own MUI/Monaco styling regardless of the vanilla base — a pre-existing minor visual inconsistency (Monaco already looked distinct from the rest of the form today), not something introduced by this change.Verification
yarn install && yarn buildat thecontinuum-workbenchroot — confirms the new dependencies resolve and the extension/app packages compile.yarn start:workbench, openhttp://localhost:3002, open a workflow, double-click a node:WorkflowDocument.save()) still round-trips the editedproperties/retryOptionscorrectly to the persisted workflow JSON.