Repository navigation
[P3] SDUI: render ActionParamDialog params through the shared form field-widget renderer (retire bespoke per-type branches) #2700
Description
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→Selectlookup/reference(withreferenceTo) →LookupField(the only@object-ui/fieldswidget it reuses)textarea→Textareanumber→ numberInput- everything else → a plain text
Inputfallback
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 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.- Render via the shared renderer — for any
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. - Preserve param semantics —
requiredvalidation (isMissingValuealready treats[]/empty as missing),visibleCEL gating (usePredicateScope+ExpressionEvaluator),helpText, and error styling must survive the swap unchanged. - No adapter threading — the ambient
UploadProviderandSchemaRendererContext(dataSource for relational widgets) are already in scope whereActionParamDialogmounts, exactly as theLookupFieldreuse 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 widenActionParamSchemafor widget config. - Relational widgets need context.
lookup/master_detail/userreaddataSourcefromSchemaRendererContext; confirm every host ofActionParamDialog(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.
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). - Every
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). - 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)
Activity
feat(app-shell): render ActionParamDialog params through the shared f…
fix(app-shell): guard ActionParamDialog submit during file upload + m…