Skip to content

Theia-native Node Properties Dialog using @eclipse-emfcloud/jsonforms-property-view #103

Description

@projectcontinuum

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

  1. 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.

  2. 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.

  3. 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.

  4. 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).

  5. 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).
  6. 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".

  7. 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.

  8. 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

  1. yarn install && yarn build at the continuum-workbench root — confirms the new dependencies resolve and the extension/app packages compile.
  2. 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.
  3. Confirm the workflow's dirty/save flow (WorkflowDocument.save()) still round-trips the edited properties/retryOptions correctly to the persisted workflow JSON.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions