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:
select → Select
lookup/reference (with referenceTo) → LookupField (the only @object-ui/fields widget it reuses)
textarea → Textarea
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_TYPES — file, 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:
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.
- 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.
- Preserve param semantics —
required validation (isMissingValue already treats []/empty as missing), visible CEL gating (usePredicateScope + ExpressionEvaluator), helpText, and error styling must survive the swap unchanged.
- 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
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)
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_TYPESin@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
filebranch 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
ActionParamDialogis a bespoke, hand-rolled form, not routed through any shared field-component registry. Its rendering is a manual ternary chain overparam.type:select→Selectlookup/reference(withreferenceTo) →LookupField(the only@object-ui/fieldswidget it reuses)textarea→Textareanumber→ numberInputInputfallbackSee
packages/app-shell/src/views/ActionParamDialog.tsx(render branches ~L195–270; it imports onlyLookupFieldfrom@object-ui/fieldsat 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 addfileas 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 asFORM_FIELD_TYPESat L2223;'file' → FileFieldat ~L2174,markdown/html/richtext → RichTextField, etc.), lazy-loaded and registered throughComponentRegistry(createFieldRendererat L2078,mapFieldTypeToFormTypeat L1835). The inline-grid dispatcherFieldEditWidgetdeliberately excludes the heavy/binary types (INLINE_EXCLUDED_FIELD_TYPES—file,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:
paramToField(param)— a small pure adapter mapping anActionParamDefto 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/maxSizefor file, options for select, etc.). MirrorfilterVisibleParams' pure, testable style.param.typepresent inFORM_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.requiredvalidation (isMissingValuealready treats[]/empty as missing),visibleCEL gating (usePredicateScope+ExpressionEvaluator),helpText, and error styling must survive the swap unchanged.UploadProviderandSchemaRendererContext(dataSource for relational widgets) are already in scope whereActionParamDialogmounts, exactly as theLookupFieldreuse relies on today.Considerations / risks (why this is bigger than #2698)
ActionParamDef(@object-ui/core) is not a field def; the adapter must be exhaustive and lossless, and spec may need to widenActionParamSchemafor widget config.lookup/master_detail/userreaddataSourcefromSchemaRendererContext; confirm every host ofActionParamDialog(ObjectView / RecordDetailView / DeclaredActionsBar / approvals inbox) provides it.string | string[], date → ISO). Confirm each round-trips to what the action runner/endpoint expects; add coverage.fieldWidgetMapis lazy; keep the dialog's widgets lazy so a param dialog doesn't pull every widget eagerly.Scope / acceptance
ActionParamDialogrenders params through the shared form field-widget renderer; bespoke per-type branches removed (or reduced to genuine param-only fallbacks).FORM_FIELD_TYPEStype usable as a param renders its real widget (spot-checkfile,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 theFieldEditWidget↔FORM_FIELD_TYPESdrift test).UploadProvider/SchemaRendererContext.Relationship to #2698
Do #2698 first (it lands the
filebranch + spectype: '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'sfilebranch is the seed ofparamToField.Refs
objectui#2698 (P2 — the single
filebranch; 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)