Skip to content

[P3] SDUI: render ActionParamDialog params through the shared form field-widget renderer (retire bespoke per-type branches) #2700

Description

@os-zhuang

Goal

Stop hand-rolling ActionParamDialog's per-type rendering. Route each action param through the same field-widget renderer the object form uses (fieldWidgetMap / FORM_FIELD_TYPES in @object-ui/fields), so a declared action param of any field type — file, image, richtext, markdown, color, address, … — renders its real widget for free, and the param surface can never silently drift behind the form surface again.

This is the architectural generalization of #2698 (which adds a single file branch by hand). #2698 fixes the immediate approvals need; this issue removes the whole class of "param type X falls through to a text box" bugs.

Why

ActionParamDialog is a bespoke, hand-rolled form, not routed through any shared field-component registry. Its rendering is a manual ternary chain over param.type:

  • selectSelect
  • lookup/reference (with referenceTo) → LookupField (the only @object-ui/fields widget it reuses)
  • textareaTextarea
  • number → number Input
  • everything else → a plain text Input fallback

See packages/app-shell/src/views/ActionParamDialog.tsx (render branches ~L195–270; it imports only LookupField from @object-ui/fields at L36). So every rich type a designer might declare — file, image, richtext, markdown, color, address, geolocation, code, … — collapses to a text box. #2698 is about to add file as branch #6; without this refactor, the next type is another one-off branch, and the param surface keeps trailing the form surface.

Meanwhile the object form already resolves every one of these via fieldWidgetMap (packages/fields/src/index.tsx:2138, keys frozen as FORM_FIELD_TYPES at L2223; 'file' → FileField at ~L2174, markdown/html/richtext → RichTextField, etc.), lazy-loaded and registered through ComponentRegistry (createFieldRenderer at L2078, mapFieldTypeToFormType at L1835). The inline-grid dispatcher FieldEditWidget deliberately excludes the heavy/binary types (INLINE_EXCLUDED_FIELD_TYPESfile, image, richtext, …), so that path is not the one to reuse; the form path is.

Proposed design (B)

Replace the bespoke branch chain with a param→field adapter that delegates to the form field-widget renderer:

  1. paramToField(param) — a small pure adapter mapping an ActionParamDef to the { name, type, ...config } field shape the form widgets consume. It already exists in spirit: resolveActionParams() copies lookup config from the underlying object-field; extend that to carry the widget-relevant config for all types (multiple, accept/maxSize for file, options for select, etc.). Mirror filterVisibleParams' pure, testable style.
  2. Render via the shared renderer — for any param.type present in FORM_FIELD_TYPES, render the form widget ({ value, onChange, field }) instead of a hand-written control. Keep the current bespoke branches only as fallbacks for param-only shapes with no field widget.
  3. Preserve param semanticsrequired validation (isMissingValue already treats []/empty as missing), visible CEL gating (usePredicateScope + ExpressionEvaluator), helpText, and error styling must survive the swap unchanged.
  4. No adapter threading — the ambient UploadProvider and SchemaRendererContext (dataSource for relational widgets) are already in scope where ActionParamDialog mounts, exactly as the LookupField reuse relies on today.

Considerations / risks (why this is bigger than #2698)

  • Param model ≠ field model. ActionParamDef (@object-ui/core) is not a field def; the adapter must be exhaustive and lossless, and spec may need to widen ActionParamSchema for widget config.
  • Relational widgets need context. lookup/master_detail/user read dataSource from SchemaRendererContext; confirm every host of ActionParamDialog (ObjectView / RecordDetailView / DeclaredActionsBar / approvals inbox) provides it.
  • Value-shape parity. Widgets emit their own value shapes (e.g. file → string | string[], date → ISO). Confirm each round-trips to what the action runner/endpoint expects; add coverage.
  • Regression surface. This rewrites the render path for every existing param type, so it needs a broad snapshot/behavior test net (select, lookup, textarea, number, boolean, date) before flipping, not just the new types.
  • Bundle. fieldWidgetMap is lazy; keep the dialog's widgets lazy so a param dialog doesn't pull every widget eagerly.

Scope / acceptance

  • ActionParamDialog renders params through the shared form field-widget renderer; bespoke per-type branches removed (or reduced to genuine param-only fallbacks).
  • Every FORM_FIELD_TYPES type usable as a param renders its real widget (spot-check file, image, richtext, color, date, select, lookup).
  • required + visible (CEL) + multiple + value round-trip preserved for all types; a drift test asserts param support ⊇ form support so the two can't silently diverge again (mirrors the FieldEditWidgetFORM_FIELD_TYPES drift test).
  • No adapter threading — ambient UploadProvider / SchemaRendererContext.
  • app-shell suite green; console typecheck + lint clean.

Relationship to #2698

Do #2698 first (it lands the file branch + spec type: 'file' + tests, unblocking the approvals composer retirement). This issue then generalizes that one branch into "all types via the shared renderer" and deletes the hand-written branches. If prioritized, #2698's file branch is the seed of paramToField.

Refs

objectui#2698 (P2 — the single file branch; approvals need) · objectui#2697 (declared-action / DeclaredActionsBar) · packages/fields/src/index.tsx (fieldWidgetMap / FORM_FIELD_TYPES / mapFieldTypeToFormType) · packages/fields/src/FieldEditWidget.tsx (INLINE_EXCLUDED_FIELD_TYPES — why the inline path is the wrong one to reuse) · packages/app-shell/src/views/ActionParamDialog.tsx (the bespoke branch chain being retired)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions