Repository navigation
fix(plugin-grid,fields,app-shell): a promoted bulk action's field-backed param resolves through the shared action-param resolver (objectui#12105) - #12119
Conversation
…ked param resolves through the shared action-param resolver (objectui#12105)
The selection bar's bulk dialog mapped a promoted action's params by hand
(`toBulkParam`): `field` became `name`, `type` defaulted to `text`, and the
picker target was read only off an inline `reference`. A field-backed param
(`{ field, objectOverride }`) therefore drew a raw text box and the confirm
step listed raw ids, while the record page resolved the same declaration
into the record picker through app-shell's `resolveActionParams`, which
plugin-grid cannot import.
The resolver moves, unchanged in behaviour, from app-shell to
`@object-ui/fields` (the one package both sides depend on that holds every
input it reads), together with its three-row param-type alias table.
app-shell imports it from there. plugin-grid resolves a promoted def's
params when its dialog opens (`resolvePromotedBulkParams`), after awaiting
the session's object metadata, exactly as the record dialog does, and maps
the resolved param onto the bulk vocabulary. A param whose backing field is
missing is refused in the bulk dialog as it is in the record dialog.
Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…solver, module-scoped (objectui#12105) `@object-ui/fields` carries no node typings, so the two dev-only warnings the resolver brought from app-shell failed `TS2591`. The read stays the bundler-replaced `process.env.NODE_ENV` it was in app-shell; only its type is declared, inside the module, so no ambient global can shadow node's in a program that loads `@types/node`. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
…nd the bulk path's field-backed params; changeset (objectui#12105) The fields README gains a section on the resolver exports, the plugin-grid and enhanced-actions guides say a field-backed param resolves on the selection bar as on the record page, and four core comments that named the resolver's old home now name `@object-ui/fields`. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
… the caller, as real use does (objectui#12105) The snippet's inline field def taught `type: 'lookup'`, which the doc component-type gate reads as a node type. The objects now come from the caller and the session store, which is where they come from in use. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
…resolve — picks up objectui#12117's ObjectGrid change (objectui#12105) Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card objectui#12105 (body, the ① Derived judgments
② Semver levelPR body: ③ Boundary flags
Deviations, each answered:
Out-of-scope findings, each answered or escalated:
Card-level follow-up carried forward: the four Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #12105
Clause-②: yes
A promoted bulk action's field-backed param (
{ field, objectOverride }) now resolves through the same resolver as the single-record path, so the selection bar's dialog renders the record picker and the confirm step shows the record's label. One resolver, shared: it moved out ofapp-shellinto@object-ui/fields. There is no second copy.Implemented by
os-devfor thedomain:uiseat, sessionhttps://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz, branchclaude/issue-12105-bulk-param-field-resolve.The defect, reproduced red on
maina21ff9aA throwaway probe against
main's source, with HotCRM's shape (params: [{ field: 'crm_campaign', objectOverride: 'crm_campaign_member' }]):resolveBulkActionsproduced{"field":"crm_campaign","objectOverride":"crm_campaign_member","required":true,"name":"crm_campaign","type":"text"}.bulkParamToFieldturned that into atextwidget.BulkActionDialogrenderedinput#bulk-param-crm_campaignwithtype="text".The card's premise holds.
toBulkParamrenamedfieldontoname, defaultedtypeto'text', and readobjectonly off an inlinereference. It never looked at the field definition.What changed, and why each package is touched
@object-ui/fieldsreceives the resolver.resolveActionParams.tsmoved here withgit mv, unchanged in behaviour, together with the three-row param-type alias table andresolveParamWidgetType. The index exports them (list under Clause-② below). The one other edit to the moved file is a module-scoped type for the bundler-replacedprocess.env.NODE_ENVread. This package has no node typings. The runtime read is the same bytes it was in app-shell, and nothing ambient is declared, so it cannot shadow node's global anywhere.@object-ui/app-shellnow imports the resolver from@object-ui/fields. Its two callers changed (useConsoleActionRuntime,RecordDetailView), andparamToField/ActionPreviewnow readresolveParamWidgetTypefrom fields. The resolver was never exported from app-shell, so its public face is unchanged. Most of the files counted under app-shell are tests whose import line moved. Two tests key on the file's path: theINHERITED_TARGETcitation pin and core's column-identity ratchet. Both were repointed.@object-ui/plugin-gridis the fix.resolveBulkActions.ts: the fold no longer attaches promotedparams. A newresolvePromotedBulkParams(def, ctx)runsresolveActionParamsand renames the resolved param onto the bulk vocabulary (helpText→help,defaultValue→default,referenceTo→object,displayField→labelField, plusdependsOn,acceptandmaxSize). It contains no field lookup of its own.ObjectGrid.tsx:dispatchBulkActionDefresolves when the dialog opens. It first awaitsuseMetadata().ensureType('object'), then unions the grid's own object schema with the session's objects (withKnownObjects, the grid's first). That is the record dialog's order, so anobjectOverridetarget that has not loaded yet is waited for, not refused.BulkActionDialog.tsx: a param the resolver could not resolve (unresolvedField) is refused before any widget is built, and Next is disabled. This matchesActionParamDialogand reuses itsactionDialog.unresolvedParamcopy. Without it, the resolver's own warning, "The action dialog refuses the param", would be false on the bulk path, and the missing-field case would still draw a raw text box. The key travels on a plugin-grid-local reader (unresolvedFieldOf), not as a declaredBulkActionParammember, because that interface is also the authoring face (AGENTS.md #0.1).dependsOnnote now says the promoted route exists.@object-ui/core: comments only. Four comments named the resolver's old home (expand-fields.ts,predicate-fields.ts,reference-keys.ts, one test). The column-identity ratchet total goes from 12 to 11, because the hand-mapper'sname || fieldread is gone.fieldsREADME has a new "Resolving declared action params" section.plugin-grid.mdxandenhanced-actions.mdxeach gain a short paragraph. Changeset:.changeset/12105-bulk-param-field-resolve.md.Why the resolver moved instead of being injected
I measured what the resolver imports before choosing. It reads
@object-ui/core(EXPANDABLE_FIELD_TYPESand theActionParamDef/ActionParamOptiontypes), the fields widget fold (resolveFormWidgetType, throughresolveParamWidgetType) and@objectstack/spec/ui(resolveI18nLabel). Its only app-shell-local import wasparamToField.ts'sresolveParamWidgetType, a thin wrapper over the fields fold plus a three-row alias table. plugin-grid already depends on all three of those packages.@object-ui/fieldsis the one package both sides depend on that holds all of them.@object-ui/corecannot host the resolver, because it cannot import the fields widget map.@object-ui/fields, and nothing to plugin-grid's or app-shell's published face. Every host gets the fix, including a grid rendered without the console.toBulkParammapping is that answer. That leaves two resolution behaviours, and the one a host gets by forgetting the provider is the defect this card reports.fields/src/resolveActionParams.tsimportsresolveFormWidgetTypefrom the package barrel that re-exports it. That is a cycle in the source graph only, and only at call time: nothing at the module's top level reads it, and the published bundle is one file. The fields build printed no circular-dependency warning, andcheck:node-esm-loadloads the built entry.Spec guidance sentence for the objectstack follow-up
Per the card, objectstack is not edited here. Quoted verbatim from objectstack
origin/main7098acaef,packages/spec/src/ui/bulk-action.zod.ts,BulkActionParamSchema'sguidance.field:After this PR the clause "
toBulkParamnever does" is false for a promoted action. An action declared on the object and named in a view'sbulkActionsnow resolves its field-backed params. An authoredbulkActionDefs[].paramsentry still has no field-backed route, so the refusal stays right and only its reason and remedy change. The remedy could be: "or declare the action on the object with a field-backed param and name it inbulkActions". Three more sentences in the same file make the same claim:BULK_PARAM_WIDGET_CONFIG_KEYSprescription: "⛔ Declaring the key on the object's FIELD does not reach this dialog either: the bulk surface has no field-backed param route."toBulkParamnever consults the object's field definitions"dependsOnJSDoc: "the FIELD-BACKED route (resolveActionParamsresolves the object's field definitions), which is the very route the bulk surface does not have."For the widget-config one, note that a promoted lookup param receives only
object,labelFieldanddependsOnfrom its field so far (see Acceptance notes).Tests
New file:
packages/plugin-grid/src/__tests__/bulkFieldBackedParam-12105.test.tsx, 8 cases.type: 'lookup',object: 'crm_campaign',labelField: 'name', andbulkParamToFieldbuilds the picker. A parity case shows the same answer asresolveActionParamfor the same declaration.ObjectGrid+BulkActionDialog:lookup-trigger-crm_campaignrenders andinput#bulk-param-crm_campaigndoes not. The confirm step shows "Spring Launch" (viafindOne('crm_campaign', 'c1')). Run sendscrm_campaign: 'c1'once per selected record. The metadata store loads late, so the case also pins theensureType('object')wait.reference: 'sys_user'still promotes to the person picker) and at grid level (input#bulk-param-notewithtype="text").bulk-param-unresolved-crm_campaignwith theOBJECT.FIELDlocator, and Next is disabled.resolveBulkActions.test.ts's key-mapping case now asserts throughresolvePromotedBulkParams.Results:
ef07e4e:pnpm exec vitest runover 47 files (plugin-grid bulk suites, app-shellresolveActionParams*/paramToField*/ActionParamDialog*/ActionPreview*/ the identity and twin pins, core's ratchet andActionParamDef.options, andone-authority-per-exported-name-6273):Test Files 47 passed (47),Tests 510 passed (510). The commits after it change typing, comments, docs and the changeset. The rerun at the final head is in theos-dev-reporton the card.resolveActionParams(raw, { ...ctx, objects: [] })): the three field-backed pins went red, and both CONTROLs, the refusal case and the authored-def case stayed green. The mutation was proven on disk (anchor 1 → 0, blobe2e6b01a→e770d6fb), and so was the restore (blob equal to HEAD,git diff HEADempty).ensureType('object')wait removed fromObjectGrid: only the grid-level picker pin went red (it got the refusal instead), 7 of 8 green. Restore proven the same way.ObjectGridhands the resolver,pnpm --filter @object-ui/plugin-grid type-checkreportedTS2353 … does not exist in type 'ResolveActionParamsContext'. That proves plugin-grid reads the rebuilt fields.d.ts. Restored,git diff HEADempty.type-check):@object-ui/fields,@object-ui/plugin-grid,@object-ui/app-shelland@object-ui/core, all exit 0, after a turbo build of each closure. The plugin-grid and fields run was atd5e2f59; the app-shell and core run was on the same code.Gates, all exit 0 at
5c915e9check:spec-symbols,check:handler-key-reads,check:action-forward-parity,check:phantom-deps,check:unused-deps,check:self-import,check:esm-specifiers(specifiers-only, as the root script runs it),check:unreferenced-sources.check:new-line-citations(VERDICT new-cross-file-line-citations: 0 new citation(s)),check-control-bytes.check-changeset-presence("30 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)"),check-changeset-no-major,check-changeset-fixed,check:changeset-claims,check:pending-changeset-literals.check:test-path-roots, the threecheck:vi-mock-*,check-type-check-coverage.check:i18n-keys. It confirms that the reusedactionDialog.unresolvedParamdefault matches the en pack. No locale copy changed, so the other i18n checks were not run.check:readme-exports,check-doc-snippet-types,check-doc-example-types,check-doc-component-types,check:doc-example-readers,check:doc-fences,check-doc-example-ids,check-doc-links.check:published-dist.check:node-esm-load --force-buildat5c915e9: "37 of 37 gradable entries were built by this tree". A first run without--force-buildwas refused on provenance alone, because 6 untouched packages were replayed from a sibling worktree's shared turbo cache.check:eager-closureat head: "Console eager closure is 3170.6 KB gzipped across 290 of 2474 chunks (budget: 3204.6 KB, headroom: 34.0 KB)". The base-to-head delta is in theos-dev-report.eslint.config.jsapplies its rules to**/*.{ts,tsx}. All 30 changed code files are in that population, and none came back as ignored.eslint --format jsonover those 30 files reported 30 results, 0 errors.parserOptions.projectorprojectService), and no rule undereslint-rules/reads the disk. So this diff cannot change a verdict on a file it did not touch.--no-inline-configthe same run shows 4 errors. They sit exactly on the 4 pre-existingeslint-disable-next-line react-hooks/static-componentscomments in these files. Repo-widepnpm lintis CI's.Clause-② surface added
New exports from
@object-ui/fields:resolveActionParam,resolveActionParams,resolveParamWidgetType,withKnownObjects,RESOLVED_ONLY_PARAM_KEYSRawActionParam,RawActionParamOption,ResolveActionParamsContextBefore this PR they were internal to app-shell (not exported from it). They are now exported from fields' package entry.
Nothing is added to plugin-grid's package entry.
resolvePromotedBulkParamsandunresolvedFieldOfare module exports ofresolveBulkActions.ts, which the index does not re-export. No prop, context member or locale key is added. The bulk dialog reuses the existingactionDialog.unresolvedParamkey.Acceptance notes (noted, not filed)
object,labelFieldanddependsOn.lookupFilters,lookupColumns,lookupPageSize,descriptionField,idFieldandtitleFormatare resolved but not forwarded.bulkParamToFieldhas no rename for them, while app-shell'sparamToFielddoes. Converging the two param adapters is its own change. The changeset and the docs say so. Carrier: whoever next convergesbulkParamToFieldwithparamToField._actions.ACTION.params.PARAMlocalization. The record dialog runs that pass after resolution (actionParamText), and the bulk path never had it. This is a gap from before this PR, not something it introduces. Carrier: none.dependsOnnote is still stale elsewhere. It saysBulkActionParamSchema"is not strict", but the spec has since closed that shape and declaresdependsOn. Only the sentence this PR made false was edited. Carrier: none.BULK_PARAM_TYPE_ALIASESinbulkParamToField.tsduplicates the param alias table. The table now lives in fields besideresolveParamWidgetType. Not touched, because it is a different defect class. Carrier: the same as the first note.Generated by Claude Code