Skip to content

fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares (#30169) - #30387

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-8y2k2m
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-8y2k2m

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread
domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

Fixes #30169

Clause-②: no (narrowing) — the grid stops drawing a bare bulkActions or legacy rowActions name that matches no action the object declares. packages/spec/src/ui/bulk-action.zod.ts already says the member "names an action the object declares", and lint action-name-undefined (ADR-0049) refuses any other at authoring. Owes a CONTRACT_REVIEW_TIER review before the queue.
Metadata-protocol: narrowed (an undeclared bare bulkActions / rowActions name no longer draws)
Skills-docs: none owed — skills/** teaches no handler-only bulk or row action name (searched skills/ for bulkActions / rowActions examples and runner-handler text: no hit); content/docs/ui/** already names only declared actions.

维护者速读

  • 改了什么:列表的选择栏和行菜单只画对象声明过的操作。视图里写了对象没声明的裸名字(拼错的,或被禁用模块扣下的操作),不再画按钮或菜单项,开发态在控制台打一行警告点名。
  • 为什么改:HotCRM 在 18.3.0 上关掉营销模块后,勾选线索仍出现「Create Campaign」幽灵按钮,点了只弹「不可执行」。按裁决 C,渲染器回到 spec 原文「指对象声明的操作」,不再容忍只靠前端接管的名字。
  • 风险与回滚:只动 plugin-grid(两个解析函数、ObjectGrid、行操作列、新增一个警告模块)与其测试、demo、README,不动 spec 和服务端。若有仓外宿主靠前端接管一个未声明的名字,它的按钮会消失(仓内实测无此用法)。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-grid/src/ObjectGrid.tsx(effectiveBulkActions 不再并入未声明名字、两次警告调用)→ ui/packages/plugin-grid/src/undeclaredActionNames.ts → 测试 objectBulkActionDispatch.test.tsx 新 describe「a bulk action a disabled package withholds」。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-grid/src/__tests__/objectBulkActionDispatch.test.tsx,预期全部通过;其中「withheld」用例在旧逻辑下转红(选择栏画出了被扣下的按钮)。
  • 需要你做的:无,席位落地。

What changes

resolveBulkActions and resolveLegacyRowActions kept a bare name that matched no action in objectDef.actions, and ObjectGrid drew it by humanized name and dispatched it to the runner by name, on the reasoning that a host might have registered a runner handler under it. That is how a list whose app has a package disabled kept a ghost button: the server withholds the package's actions from the served object, so their names match nothing, and the click toasted Action "create_campaign" is not executable. Ruling 6651753071 (option C) retires the tolerance.

  • resolveBulkActions / resolveLegacyRowActions. A name with no declared match produces no def. It comes back as undeclared (renamed from unresolved; neither function is a package export), used only for the warning below. Both module headers now state the rule and its source (the spec sentence and action-name-undefined), replacing the "that path must keep working" paragraph.
  • ObjectGrid. The selection bar's string list is now only the canonical 'delete' (author-declared or the implicit bulk delete), unchanged in its routing (onBulkDelete, or the runner by name when no onBulkDelete is wired). The row menu no longer receives a by-name list: objectGridActionsColumn.tsx drops the customRowActions input, so the Actions column and the kebab read only resolved defs plus the canonical Edit / Delete.
  • undeclaredActionNames.ts (new). describeUndeclaredActionNames builds one message per fold naming the key, the block address and every dropped name; useUndeclaredActionNamesWarning logs it once per distinct list, only when NODE_ENV is not production, and only once the object's schema has loaded (or the grid names no object), so a list still loading its object is never reported. ObjectGrid calls it twice, for bulkActions and rowActions, in hook order before the early returns.
  • Demo / README. demo/bulk-actions.tsx swaps legacy_only_handler (a handler-only name) for not_declared, drops the handler, and says the bar draws no button for it. README.md replaces bulkActions: ['delete', 'export'] and rowActions: ['view', …] (export and view are handler-only) with a declared deactivate, and states the rule under Row Actions.

Unchanged: a name that matches a declared action is promoted exactly as before (label, icon, params, visible, requiredPermissions); bulkActionDefs are untouched; delete keeps its own path (keptDeleteAction / onBulkDelete).

Consumers measured (this tree, every non-test bulkActions / batchActions / rowActions list under examples/, ui/apps/, ui/packages/, packages/): examples/app-showcase (task.view.ts, field-zoo.view.ts) names only actions declared in src/ui/actions/index.ts; packages/lint fixtures mirror the showcase. The one handler-only name was the plugin-grid demo, changed here. The Console's runner handlers (useObjectActions.ts: create, delete, navigate, refresh; AppContent.tsx: crud_success, dialog_cancel, navigate_create, navigate_edit) are action TYPES for other surfaces, not bulk or row names. No fallback is kept for an out-of-repo host; its remedy is in the changeset's FROM → TO.

Tests

Pins of the old tolerance, each flipped to the new behaviour with the trigger named in the test:

  • bulkActionMembers-8071.test.tsx row 3: bulkActions: ['approve', 'no_such_action'] draws exactly bulk-action-approve, no "No Such Action", and one ObjectGrid bulkActions warning naming 'no_such_action'.
  • objectBulkActionDispatch.test.tsx ("keeps an unresolvable name alongside the promoted defs" → "draws no button for a name the object does not declare, even with a runner handler under it"): a handler registered under crm_only_handler neither draws the button nor runs.
  • legacyRowActionDispatch.test.tsx ("leaves a name with no declared action to the by-name handler path" → "draws no entry … and warns once naming it"): the declared convert_lead entry draws, crm_only_handler does not, one ObjectGrid rowActions warning.
  • resolveBulkActions.test.ts, resolveLegacyRowActions.test.ts: the by-name cases flip to "no def, reported as undeclared"; a new row pins that a repeated undeclared row name is reported once.

Trigger test (new top-level describe in objectBulkActionDispatch.test.tsx, the shared harness): the view names push_down and create_campaign. Withheld (the served object lacks create_campaign): no bulk-action-create_campaign, no "Create Campaign", one warning naming it and not push_down. Served: the button reads the action's own label "Add to Campaign", and no warning.

Fixtures that used an undeclared name as a stand-in, repaired (behaviour under test unchanged):

  • bulkActionsSpecKey.test.tsx, rowCrudEffectiveOps.test.tsx, gridPagerSelectionMembers-8071.test.tsx, actionsColumnAndChips-12708.test.tsx: the object now declares the named action (approve / reject, notify, approve, archive).
  • bulkActionRefresh.test.tsx: the by-name dispatch path these rows pin (refresh and selection reset after a string action) is now reached by the canonical 'delete' with no onBulkDelete, the handler registered under delete.

Runs (all pnpm --dir ui exec vitest run --maxWorkers=2 … under scripts/pm/os-verify-lock.sh):

  • Base 9afcd6a6, the five resolver / dispatch files before any edit: Test Files 5 passed (5), Tests 54 passed (54) (the old pins green, encoding the tolerance).
  • Whole packages/plugin-grid/ after the source change, before the test changes: Test Files 10 failed | 267 passed (277), Tests 27 failed | 2350 passed (2377), the 27 being exactly the pins and fixtures listed above.
  • The 11 changed / harness files after: Test Files 11 passed (11), Tests 124 passed (124).
  • Consumers outside plugin-grid that assert bulk or row action test ids or author those keys (app-shell 7, plugin-list 5, plugin-view 5, plugin-detail 2, console 1, ui scripts 1) plus three more plugin-grid harnesses: Test Files 14 passed (14), Tests 542 passed (542) and Test Files 10 passed (10), Tests 237 passed (237).

Ablation, two legs through scripts/ablation-replace.mjs (tests import src, no build involved), each landing verified and restored to the HEAD blob with git diff HEAD empty:

  1. effectiveBulkActions re-includes the undeclared names (? keptDeleteAction → ? [...undeclaredBulkActions, ...keptDeleteAction]): Tests 3 failed | 17 passed (20) — 8071 row 3, the withheld trigger, the handler-name row.
  2. The warning's console.warn disabled: Tests 3 failed | 24 passed (27) — 8071 row 3, the row-menu warning row, the withheld trigger.

Type-check: pnpm exec turbo run type-check --filter=@object-ui/plugin-grid --concurrency=2 (tsc --noEmit && tsc -p tsconfig.test.json): Tasks: 53 successful, 53 total. Lint: eslint on every touched plugin-grid file, 0 errors (warnings are pre-existing no-explicit-any in the touched files; the new module has none).

Acceptance notes

  • ui/packages/plugin-list/src/ListView.tsx draws its own bulk bar for non-grid views (currentView !== 'grid', the bulk-actions-bar region mapping permittedBulkActions), and it renders every bare name by label with no resolution against the object's actions. Under this ruling's rule ("the selection bar draws only actions the object declares") that bar still draws an undeclared or withheld name. Read-only inference, not reproduced in a browser; outside this card's file surface, so not touched.
  • ui/AGENTS.md ✨ Set up Copilot instructions #2 (docs-driven) also names the matching page under content/docs/frontend/; that tree is outside the claimed surface. content/docs/ui/actions.mdx and content/docs/ui/views.mdx already show only declared names.

Verification

Derived gates (dispatch-gates --commands on the final head 2f621a5): 36 run, all exit 0
  • node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
  • node scripts/check-empty-changeset.mjs --self-test :: exit 0
  • node scripts/check-sdui-manifest.mjs :: exit 0
  • node scripts/check-sdui-manifest.mjs --self-test :: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
  • node scripts/release-pending-publish.mjs --self-test :: exit 0
  • pnpm --dir ui exec vitest run scripts/ :: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
  • pnpm check:changeset-gate-self-tests :: exit 0
  • pnpm check:closed-ceremony-text :: exit 0
  • pnpm check:cross-package-test-inputs :: exit 0
  • pnpm check:dispatcher-error-vocabulary :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:engine-double-contract :: exit 0
  • pnpm check:error-status-conformance :: exit 0
  • pnpm check:gitlink-declared :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:published-files :: exit 0
  • pnpm check:query-options-erasure :: exit 0
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:registry-log-declared :: exit 0
  • pnpm check:rest-log-spy-declared :: exit 0
  • pnpm check:skill-identifier-liveness :: exit 0
  • pnpm check:test-source-alias :: exit 0
  • pnpm check:tier-file-adoption :: exit 0
  • pnpm check:type-check-coverage :: exit 0
  • pnpm check:undeclared-dep-imports :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • pnpm check:where-matcher :: exit 0
  • pnpm check:type-check-debt :: exit 0

pnpm check:type-check-debt was re-run alone under the verify lock (VERDICT command-exit 0); its first run in the loop exited 3 queued behind this card's own dogfood hold. node scripts/pm/dispatch-gates.mjs --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED.

Standing ui gates
  • node scripts/check-sdui-manifest.mjs (and --self-test): in the derived list above, exit 0.
  • pnpm --dir ui check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0.

Dogfood battery (final head, (narrowing) arm): pnpm --filter @objectstack/dogfood test under the verify lock on 899d981b (closure built first): Test Files 419 passed | 3 skipped (422), Tests 3492 passed | 36 skipped (3528), VERDICT command-exit 0. The final head 2f621a58 differs from 899d981b only in the changeset's remedy paragraph.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

Waits-on: main-red #30367

Lint UI on 2f621a58 fails in @objectstack/spec check:liveness (the query/searchFields row cites engine.ts, which no longer names the key after #30341). This PR touches only ui/packages/plugin-grid and a changeset, so the red is main's, not this diff's: the p0 anchor is #30367 and its fix is PR #30368. Once #30368 merges, this branch merges origin/main and CI re-runs; the seat ACCEPTs on that head once the contract review passes.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2f621a589699fc007e1194d01e6efc728b1a924b
Local-runs: none
Scope: ui/packages/plugin-grid/src/resolveBulkActions.ts, ui/packages/plugin-grid/src/resolveLegacyRowActions.ts, ui/packages/plugin-grid/src/undeclaredActionNames.ts, ui/packages/plugin-grid/src/ObjectGrid.tsx, ui/packages/plugin-grid/src/objectGridActionsColumn.tsx, ui/packages/plugin-grid/demo/bulk-actions.tsx, ui/packages/plugin-grid/src/tests/{actionsColumnAndChips-12708,bulkActionMembers-8071,bulkActionRefresh,bulkActionsSpecKey,gridPagerSelectionMembers-8071,legacyRowActionDispatch,objectBulkActionDispatch,rowCrudEffectiveOps}.test.tsx, ui/packages/plugin-grid/src/tests/{resolveBulkActions,resolveLegacyRowActions}.test.ts + packages/spec/src/ui/bulk-action.zod.ts (the sentence at ~:406), packages/lint/src/validate-action-name-refs.ts (action-name-undefined), ui/packages/plugin-grid/src/index.tsx (the published entry, unchanged), ui/packages/plugin-grid/src/components/RowActionMenu.tsx (rowActions prop, planRowActionMenu), ui/packages/plugin-grid/src/objectGridRowLoad.ts (setObjectSchema), ui/packages/core/src/utils/record-source.ts (resolveRecordSourceObjectName), ui/packages/core/src/utils/column-identity.ts and ui/packages/fields/src/resolveActionParams.ts (the two dev-gate idioms), ui/apps/console/index.html (the window.process shim) and ui/apps/console/vite.config.ts (define), ui/packages/plugin-list/src/ListView.tsx (grid delegation ~:4403-4504, non-grid bulk bar ~:2515, :7333), ui/packages/app-shell/src/views/objectViewListRenderer.tsx (:778-790), examples/app-crm/src/views/lead.view.ts. Prose read only for ② and code-match: README.md, the changeset.

Inputs: card #30169 (body and every comment; ruling 6651753071 = option C, read verbatim), PR #30387 body, file list, net diff origin/main...origin/claude/project-thread-8y2k2m (18 files, +429/-127), the check-runs on the head. The dispatch order and the seat's own conclusions were not inputs.

① Derived judgments

Accept-set and surface changes the diff implies, each judged against the ruling and the cited code:

  1. A bare bulkActions name with no match in the served objectDef.actions draws no selection-bar button and is never dispatched (resolveBulkActions.ts :488-500; ObjectGrid.tsx :1290-1295, effectiveBulkActions is now keptDeleteAction alone). Right: this is ruling C's first sentence and the spec's own ("names an action the object declares", bulk-action.zod.ts :406); lint action-name-undefined already refuses the name at authoring with severity error. A misspelling, a handler-only name and an action a disabled package withholds (absent from the served object by disabled package: /auth/me/permissions still grants its objects so current_user.can() stays true, and an app action whose flow ships in the disabled package is still drawn and fails with Flow not found #28877) all fall under it, and the code cannot tell them apart, which is exactly what C accepted.
  2. A bare legacy rowActions name with no match draws no row-menu entry (resolveLegacyRowActions.ts :93-100; objectGridActionsColumn.tsx drops the customRowActions input, hasRowActions :63 no longer counts bare names, the plan and the menu props no longer receive rowActions). Right: the ruling extends the rule to this fold. The Actions column therefore no longer appears for a list whose only row action is an undeclared name (12708 fixture repaired accordingly). Right.
  3. A declared name is promoted exactly as before (label, icon, params, visible, requiredPermissions; the claimed set still lets an authored bulkActionDefs / host rowActionDefs twin win and a repeated name fold once). Right; unchanged lines.
  4. 'delete' keeps its own path: filtered out before the fold (legacyBulkNames :1261), kept in keptDeleteAction, routed by dispatchBulkAction :1981 to onBulkDelete or to the runner by name; the implicit bulk-delete affordance is unchanged; 'edit'/'delete' in rowActions still select the native entries ahead of the fold (:1005). Right, as the ruling required ("untouched").
  5. Auto-enabled multi-select (hasBulkActions :1296 → selectionMode = 'multiple'): a list whose bulkActions carries only undeclared names no longer turns checkboxes on, and one with a declared name turns them on only once the object schema has landed (the 8071 pager fixture now waits). Right: a consequence of C, and the bar it would have served draws nothing.
  6. Return-shape rename unresolved → undeclared on both resolvers, and the customRowActions input removed from resolveObjectGridActionsColumn: none of the three is reachable from src/index.tsx (read on the head; the entry is byte-identical to main), so nothing published moves. Right.
  7. undeclaredActionNames.ts is new and NOT exported from the entry (the only importer is ObjectGrid.tsx :76). RowActionMenu and RowActionMenuProps stay exported with their pre-existing rowActions?: string[] prop and the plan's legacy arm; ObjectGrid simply no longer feeds it. So the published surface of @object-ui/plugin-grid neither grows nor shrinks: nothing is widened, one rendering tolerance is withdrawn. Clause-②: no (narrowing) is the right declaration; yes would be wrong.
  8. The dev-only warning (useUndeclaredActionNamesWarning): one console.warn per fold per distinct list, keyed on the names' values, held until objectSchema != null || !schema.objectName, called in hook order before the first early return (:1305-1310 against :1591). The mechanism is right in the Console grid path (ListView's baseProps :4403 always hands the node objectName, and objectGridRowLoad.ts :156 sets objectSchema on the first load). Two edge shapes are judged under ④ (should-fix).
  9. Demo and README: the handler-only legacy_only_handler / export / view examples are gone and replaced by a declared name; the README's rule paragraph matches the code. One nit: the sentence "'edit' and 'delete' are the built-in exceptions" sits under Row Actions and is right there; for bulkActions only 'delete' is built in (an 'edit' in bulkActions is now dropped and warned). Prose only; no action required.
  10. Tests: the three old pins of the tolerance (8071 row 3, "keeps an unresolvable name…", "leaves a name with no declared action…") flip to substantive assertions with the trigger named in the test; both resolver unit files flip; the new describe pins withheld (no button, no humanized text, one warning naming it and not the sibling) and served (the action's own label, no warning); five stand-in fixtures now declare the action they name; bulkActionRefresh reaches the by-name branch through the canonical 'delete' with no onBulkDelete, which is the one by-name path the ruling keeps. Right. The ablation legs are the dev's report and were not re-run here; the pins they name are present in the tree.

② Semver level

  • Changeset .changeset/30169-undeclared-bulk-action-name-draws-nothing.md: '@object-ui/plugin-grid': minor. The only package whose published behaviour moves is plugin-grid (@object-ui/core, plugin-list, app-shell, packages/spec untouched); Metadata-protocol: narrowed takes at least minor on that package. Matches.
  • Clause-②: no (narrowing) on the PR body and in the changeset, same arm, same reasoning (the spec sentence and action-name-undefined). Matches the tree (① item 7). (narrowing) is BREAKING: the changeset carries the BREAKING banner, the Before/Now/Unchanged account, the ADR-0087 marker not-required (no-migration-prescription) and a prose remedy (declare the action or remove the name; a withheld action needs nothing). The gates that read these (Check Changeset, Lint & Repo Gates, which carries check:adr-0087-registration) are green on this head.
  • Metadata-protocol: narrowed: the spec text is unchanged, but what an author's written bare name does is tightened to the spec's own requirement; narrowed is the conservative reading ("a requirement tightened") and matches the dispatch's own declaration. Accepted. Skills-docs: none owed matches what I can read: no skills/** or content/docs/ui/** example in the diff's reach names a handler-only bulk or row action.
  • No packages/spec change, so no generated artifact is owed.

③ Boundary flags

Every flag in the os-dev-report (card comment 6654001061) and the earlier report (6642981552):

  • Open question A/B/C (6642981552): answered by ruling 6651753071, option C; the tree executes C and not A (no ActionRunner change) nor B (no /meta/action read). Closed.
  • Deviation 1 (no handler-only consumer in-tree): read it myself on the head — every non-test bulkActions / rowActions string under examples/, ui/apps/, ui/packages/ names a declared action; the demo was the one exception and is changed here. Holds.
  • Deviation 2 (bulkActionRefresh re-targeted to 'delete' with no onBulkDelete): the pinned behaviour (refresh + selection reset after a string action) is still reached, through the one by-name path the ruling keeps. Accepted.
  • Deviation 3 (row-menu drop not ablated as one line): the drop is pinned directly by legacyRowActionDispatch (row-action-crm_only_handler absent while the declared sibling draws), which is a pin of the structural removal, not only of the warning. Accepted.
  • Deviation 4 (whole plugin-grid directory run once under the lock, 12m23s): a process deviation against the order, reported; not a tree defect. Noted for the seat.
  • Deviation 5 (first two commits without the trailer pair; history not rewritten): reporting deviation; not a tree defect. Noted.
  • Deviation 6 (PR footer form, no assignee/labels): hand-off state; not a tree defect.
  • Out-of-scope 1 — plugin-list ListView's non-grid bulk bar (ListView.tsx :7333-7375) draws every permittedBulkActions member by tActionLabel(action) with no resolution against the object's actions. Read on the head and confirmed. Judgment: a follow-up card, not a must-fix here. Reasons: (a) the ruling's execution list names only the plugin-grid resolvers, their pins, demo and README, and the dispatch's file surface is ui/packages/plugin-grid/**; (b) the card's reproduced defect is the grid's bar (Leads → All Leads is a grid view), and the grid path is where ListView delegates (batchActions: schema.bulkActions, :4504); (c) that bar is a different mechanism altogether: it has no promotion step even for declared names and dispatches by bare string through props.onBulkAction, a runtime slot that has no in-repo provider (types/src/zod/objectql.zod.ts :2421 says so), so bringing it under the rule means first deciding what that bar dispatches to, which is its own ruling. The changeset is scoped to @object-ui/plugin-grid, so its "a list's selection bar" reads as the grid's. The seat must file the card (class b, carrier seat, dedupe words given in the report) rather than let the finding live only in this thread.
  • Out-of-scope 2 (content/docs/frontend/ page not touched): observation; the pages in reach already show declared names. Accepted.
  • Main-red note from the brief: Lint UI on this head is judged on its own step, see the gate line below.

④ Design judgment

Per scope file, against the ruling and the cited code; each break attempt named.

  • resolveBulkActions.ts — holds. A declared-name collision with an authored def: claimed keeps the authored def and reports nothing (right, not undeclared). A declared action named delete: never reaches this fold (filtered at :1261), so it cannot be reported as undeclared and the onBulkDelete route is unchanged. A withheld package's action: absent from the served array, reported once. Header rewritten to state the rule and its two sources. No finding.
  • resolveLegacyRowActions.ts — holds. Same collision reasoning against host rowActionDefs; a repeated undeclared name reported once (new pin). The pre-existing "tolerates a missing object schema" row still returns the name as undeclared, which is why the grid gates the warning on readiness. No finding.
  • ObjectGrid.tsx — the drop holds (① 1, 4, 5). Hook order holds. Two should-fixes on the warning's readiness gate, both in undeclaredNamesReady (:1299):
    • should-fix (a): it reads schema.objectName, while the grid's object is objectName = resolveRecordSourceObjectName(schema, dataConfig) ?? schema.objectName (:811). A grid whose object arrives only through data: { provider: 'object', object } with no top-level objectName (a shape this file's own inline-edit note calls out, and the shape examples/app-crm/src/views/lead.view.ts writes) reads ready as true on the first render, before the schema has landed, so every bare name is reported as undeclared once, with a "no objectName" address, and the author is told to declare an action that is declared. Reach: standalone SDUI grids only; the Console path hands objectName down (ListView.tsx :4403). Remedy: key readiness on the resolved objectName, and use it for the address.
    • should-fix (b): with an objectName and no dataSource.getObjectSchema (an inline-data grid, or a schema fetch that failed) ready never flips, so the names are dropped silently, which is the half the ruling asked to keep audible. Remedy: mirror objectTypesPending (:836): ready when there is nothing to wait for, i.e. objectSchema != null || !(objectName && typeof dataSource?.getObjectSchema === 'function').
  • undeclaredActionNames.ts — the message is complete (key, block, names, prescription) and JSON.stringify keying makes the effect fire once per distinct list, not per render. Two should-fixes:
    • should-fix (c) production gate: isDevelopment() reads (globalThis).process?.env?.NODE_ENV !== 'production'. That is @object-ui/core's idiom (column-identity.ts :110, reference-keys.ts, dashboard-filters.ts, actionKeys.ts, errors/index.ts), so the module is consistent with the tree, but in the Console it does not gate: ui/apps/console/index.html :187 installs window.process = { env: { NODE_ENV: 'development' } } unconditionally, and a split optional chain is not a token the bundler replaces (the Console's define of 'process.env' reaches only the literal spelling). So a production Console logs the warning too. The tree's other idiom, the bundler-replaced literal process.env.NODE_ENV with a module-scoped declare const process (fields/resolveActionParams.ts :65-74, components/renderers/*), is replaced to "production" by the Console's build and stays live in the lib build for a consumer's bundler. Not a must-fix: five sibling dev warnings in core share the same leak, it is a console line and never a wrong draw, and the shim is the Console's; but this module is new and should take the idiom that gates. A card on the shim is the seat's call.
    • should-fix (d) warn-once under StrictMode: the Console mounts under StrictMode (its own tests say so), where every effect runs setup → cleanup → setup, so the "one warning" is two in development. The sibling column-identity.ts memoises on a module-level Set keyed by the message; the same here (keyed on key + address + names) makes the ruling's "once" literal and also quiets a remount.
  • objectGridActionsColumn.tsx — holds; the rowActions plumbing to the plan and the menu props is removed in both places, hasRowActions no longer counts bare names, and resolveLegacyRowActions is still the type source for resolvedRowActionDefs. No finding.
  • demo/bulk-actions.tsx — holds; the handler-only name and its ActionProvider handler are gone, the copy says what the bar does now. No finding.
  • Tests — each flipped pin names the trigger (withheld action) in its body; the withheld/served describe is in the shared harness file as the ruling asked; fixtures declare what they name. One observation: both row-menu warning assertions read the spy synchronously after openRowMenu resolves, which holds because the harness awaits the menu, i.e. a render after the schema; fine as written.
  • Against the published spec on main: the narrowing makes the spec's own refusal real at the consumer (the sentence at bulk-action.zod.ts :406 and lint's error) and takes no declared capability away: no key, value or export changes shape. Holds.

No must-fix. Should-fixes (a)-(d) are each a few lines in ObjectGrid.tsx / undeclaredActionNames.ts and do not block landing; the seat may fold them into a follow-up with the ListView card or push them on this branch before queueing (a new head would owe a fresh record).

Check-runs on this head (their conclusions are the gate verdicts, read after every run completed): Type Check UI success, Test UI success (all 16 shards), Build & E2E UI success, Live E2E UI success, Governed Surface Queue Guard success, Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Check Changeset success, PR-body guards success; Build Core, Build Docs, the Test Core shards and the dogfood shards were path-skipped. Lint UI failure — read at the step: the one red step is "Spec Liveness Check" (pnpm --filter @objectstack/spec check:liveness), whose single UNANCHORED pair is query/searchFields → packages/objectql/src/engine.ts; that is main's red (anchor #30367, fix PR #30368, open at review time), not this diff's: the diff touches no file under packages/spec/scripts/liveness/, and the ledger's one plugin-grid citation (key-mention.baseline.json :65, ObjectGrid.tsx) is among the 1159 anchored pairs. Every other Lint UI step is green. The seat lands this only after Lint UI is green on a head that carries the main fix (git merge origin/main once #30368 lands; a new head owes a fresh record).

Implemented-by: claude/project-thread-8y2k2m
Reviewed-by: session_01Utp6Cb5R6JDdfyvtj3dPR2-fable-subagent

VERDICT: PASS

…ect-thread-8y2k2m

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZ1rxZPzain2p5bv5x
…bject, reports an unread schema, gates in a production build, and says it once

Contract review 6654355327 should-fixes (a)-(d), objectstack#30169:

(a) Readiness and the message's address use the resolved objectName
    (resolveRecordSourceObjectName), so a grid naming its object only through
    data.object no longer reports declared names before the schema lands.
(b) Mirrors objectTypesPending: a schema is awaited only with an object and a
    getObjectSchema; a failed read (inline: objectSchemaReadFailed; row load:
    error) is not awaited. Names reported with no schema in hand are said to be
    unread, not undeclared.
(c) The dev gate is the bundler-replaced literal process.env.NODE_ENV with a
    module-scoped declare (the resolveActionParams idiom); the globalThis chain
    survived the Console build and read its window.process shim. Pinned by
    transforming the module with the Console's define.
(d) A module-level Set keyed by the message makes the warning once under
    StrictMode and across a remount; reset exported for tests.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZ1rxZPzain2p5bv5x
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 73d1535abc656c1e1447b3882875a123939238f9
Local-runs: none
Scope: ui/packages/plugin-grid/src/ObjectGrid.tsx, ui/packages/plugin-grid/src/undeclaredActionNames.ts, ui/packages/plugin-grid/src/tests/undeclaredActionNames.test.ts (new), ui/packages/plugin-grid/src/tests/{objectBulkActionDispatch,bulkActionMembers-8071,legacyRowActionDispatch}.test.tsx + ui/packages/plugin-grid/src/objectGridRowLoad.ts (the row-load catch, :676-678), ui/packages/plugin-grid/src/index.tsx (unchanged), ui/apps/console/index.html and ui/apps/console/vite.config.ts (the shim and the define the new pin models), ui/packages/fields/src/resolveActionParams.ts and ui/packages/core/src/utils/column-identity.ts (the idioms adopted). Every other path of the PR is unchanged since the reviewed head and is covered by the record below.

Delta review of this seat's record on head 2f621a58 (PR comment 6654355327, PASS, no must-fix, should-fixes (a)-(d)). Since that head the branch carries exactly two commits, read from the ref: the merge 001b6173 (parents 2f621a58 and origin/main at 38717d34, which carries #30368, the liveness fix; git diff 2f621a58 001b6173 -- ui/packages/plugin-grid is empty, so the merge brings nothing into the reviewed files), and the fix commit 73d1535a, whose diff is the delta judged here: six files under ui/packages/plugin-grid, +244/-25. The changeset, README, demo, resolvers, objectGridActionsColumn.tsx and the package entry are byte-identical to the reviewed head. The judgments of the earlier record stand unchanged except where this one says otherwise.

① Derived judgments

Each should-fix against the tree:

  • (a) resolved objectName — closed. ObjectGrid.tsx :1311-1319: readiness and the warning's address now read objectName (resolveRecordSourceObjectName(schema, dataConfig) ?? schema.objectName, :814), not schema.objectName. Pinned by the new row "an object named only through data.object waits for that object, and the warning names it" (objectBulkActionDispatch.test.tsx): the declared push_down draws and is not named, the address carries objectName: 'os_prod_plan'. Right.
  • (b) no hang, unread said as unread — closed on both load paths. objectSchemaAwaited = !!objectName && typeof dataSource?.getObjectSchema === 'function' && objectSchema == null && !objectSchemaReadFailed && error == null mirrors objectTypesPending (:836): nothing is awaited with no object, no getObjectSchema, a landed schema, a failed inline read (new objectSchemaReadFailed state, set in the inline fetch's catch :1138 under the cancel guard) or a failed row load (error, which the non-inline loadSchemaAndData sets in its one catch, objectGridRowLoad.ts :676-678, and a schema read on that path throws inside the same try). basis is 'unread' whenever an object is named and no schema is in hand, and describeUndeclaredActionNames then says the schema could not be read and prescribes a data source that answers getObjectSchema, not that the name is undeclared. Pinned by the it.each pair (no getObjectSchema / rejecting read → one warning containing "could not be read", both names, no button) and by the three unit rows in the new file. Right. One residual, not a finding: a grid whose schema.filter is refused never loads (objectGridRowLoad.ts returns before the try), so objectSchemaAwaited stays true and nothing is said; that grid renders the filter refusal and no bar, so there is nothing to report.
  • (c) production gate — closed. undeclaredActionNames.ts :42 declares process module-scoped and :109-111 reads the literal process.env.NODE_ENV, the @object-ui/fields resolveActionParams.ts :65-74 idiom (the same packages' hosts already carry this literal in @object-ui/components renderers, so no host that runs them lacks a replaced or shimmed process). The pin (undeclaredActionNames.test.ts, @vitest-environment node) transforms the module with transformWithOxc under the Console's define plus process.env.NODE_ENV: "production" and asserts no NODE_ENV read survives and the gate folds to "production" !== "production"; that is the right place to pin it, since under vitest both spellings read node's own NODE_ENV. vite is a declared devDependency of the package; the file read is in-package through new URL(rel, import.meta.url), a spelling check:cross-package-test-inputs knows. Right.
  • (d) warn once — closed. A module-level warnedMessages Set keyed by the full message (key, address, names) with resetUndeclaredActionNamesWarnings() exported for tests; the three test files that count warnings reset it in beforeEach. Pinned under React.StrictMode and across an unmount/remount of the same list: exactly one warning. The memo shape is column-identity.ts's. Right. Two grids with the same object, label and names on one page share one warning, which is the same sentence about the same fact.
  • Surface: resetUndeclaredActionNamesWarnings, UndeclaredActionNamesBasis and the fourth describeUndeclaredActionNames parameter are new exports of the module, and the module is still imported only by ObjectGrid.tsx; src/index.tsx is unchanged, so the published surface of @object-ui/plugin-grid does not move. The Clause-②: no (narrowing) reading stands.
  • No new must-fix. Break attempts on the delta: a declared name under the data.object-only shape (not reported, pinned); a schema that lands after a failed inline read (objectSchema != null wins, basis 'declared'); a list whose names change after the first warning (the message differs, so it is said again, by design); the hooks still run in hook order before the first early return (:1316-1323 against the filter-refusal return at :1607). All hold.

② Semver level

Unchanged from the reviewed head and still matching the tree: .changeset/30169-undeclared-bulk-action-name-draws-nothing.md byte-identical ('@object-ui/plugin-grid': minor, BREAKING banner, ADR-0087 not-required (no-migration-prescription), prose remedy); Clause-②: no (narrowing) and Metadata-protocol: narrowed on the PR body and in the changeset; the four changesets the merge brings in belong to the merged main commits (#30368, #30298, #30351, #30353), not to this PR. The delta publishes nothing new: its exports stay inside an un-exported module, and the dev warning's wording is not a contract.

③ Boundary flags

④ Design judgment

  • ObjectGrid.tsx — holds. The readiness predicate reads the same facts objectTypesPending reads plus the two failure signals, each of which is set exactly where the corresponding read fails and nowhere else; objectSchemaReadFailed is not cleared on a later successful read, which is harmless because a landed schema makes the predicate false on its own and the basis 'declared'. The address now names the object the grid actually queries. No finding.
  • undeclaredActionNames.ts — holds. The two message shapes share the rule line and differ only in what they assert about the object; the 'unread' text does not claim a name is undeclared. The gate is the bundler-replaced literal with its reasoning in the module header. The memo is keyed on the message, so a changed list or a second list still reports. No finding.
  • Tests — the new pins name their trigger in the body (the data.object shape, the two unread shapes, StrictMode + remount, the build fold); the fails leg deliberately does not wait for rows (masked columns under a failed read, objectui#10657) and waits on the warning instead, which is the observable it pins. No finding.
  • Against the published spec on main: unchanged — the narrowing makes the spec's own refusal real at the consumer and withdraws no declared capability; the delta only changes when and how the audible half speaks.

No must-fix, no new should-fix.

Check-runs on this head (their conclusions are the gate verdicts, read after every run completed): every non-skipped check-run is success, 44 in all — Lint UI (now green: the merge carries #30368, so the check:liveness red of the earlier record is gone), Type Check UI, Test UI and all 16 shards, Build & E2E UI, Live E2E UI, Governed Surface Queue Guard, Lint & Repo Gates and its nine lanes, TypeScript Type Check and its four lanes, Test Core, Dogfood Regression Gate, Check Changeset, Check PR Size, PR-body guards, Build UI Deps, Auto Label, filter; Build Core, Build Docs, the Test Core shards, the dogfood shards and the opt-in harnesses were path-skipped. Nothing is red on this head.

Implemented-by: claude/project-thread-8y2k2m
Reviewed-by: session_01Utp6Cb5R6JDdfyvtj3dPR2-fable-subagent

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2

维护者速读

  • 改了什么: 列表的选择栏和行菜单只画对象声明过的操作。视图里写了对象没声明的裸名字(拼错的,或被禁用模块扣下的操作)时,不再画按钮或菜单项;开发态下控制台打一行警告点名该名字,警告只打一次。读不到 schema 时,警告说的是「读不到」,不说「未声明」。
  • 为什么改: HotCRM 在 18.3.0 上关掉营销模块后,勾选线索仍出现「Create Campaign」幽灵按钮,点了只弹「不可执行」。按裁决 C(6651753071)回到 spec 原文的规则:只画对象声明的操作。
  • 风险与回滚: 改动只在 plugin-grid,不动 spec 和服务端。这是收窄(BREAKING,minor):若仓外宿主靠前端接管一个未声明的名字,它的按钮会消失(仓内实测无此用法)。回滚即撤销本 PR。
  • 先看: ui/packages/plugin-grid/src/ObjectGrid.tsx 的 effectiveBulkActions → undeclaredActionNames.ts。
  • 两分钟验证: pnpm --dir ui exec vitest run packages/plugin-grid/src/__tests__/objectBulkActionDispatch.test.tsx,预期全过;其中「withheld」用例在旧逻辑下转红。
  • 需要您做的: 这是 BREAKING 合并,需要您在席位线程里说一句「同意合并 fix(plugin-grid)!: a list's selection bar and row menu draw only actions the object declares (#30169) #30387」。
  • 席位意见: ACCEPT。

席位核对 — ACCEPT · domain:ui#2 · head 73d1535abc656c1e1447b3882875a123939238f9

  • 形态: draft,作者 objectstack-fleet[bot](中继 pr_create),无 assignee,无 review request。19 个文件,+654 −133。check-governed-merges --pr 30387:0/19 命中登记,NOT governed,未超 3000 行。
  • 契约复核: CONTRACT_REVIEW_TIER 两轮:
    • 首轮:record 6654355327,head 2f621a58,PASS,无 must-fix,should-fix (a)–(d)。
    • delta 复核:record 6655715518,现 head,PASS。(a)–(d) 逐条关闭并点名钉子,无新 must-fix 或 should-fix;@object-ui/plugin-grid 的发布面不变。
  • 门禁 check-runs: 73d1535a 上 55 个 run:44 success,11 skipped,0 失败。卡线程本地派生门禁 36/36 exit 0,--ran 0 NOT-MEASURED(/mnt/project-files/ui-seat2/30169-gates-ran-73d1535a.txt)。
  • 申报: PR 正文与 changeset 一致:
    • Clause-②: no (narrowing)
    • Metadata-protocol: narrowed
    • changeset @object-ui/plugin-grid minor,带 BREAKING 横幅
    • ADR-0087 not-required (no-migration-prescription)
  • 接受的偏差: 无。
  • 范围外去向: 非网格视图(kanban、calendar、gallery…)的选择栏是同一缺陷,已另立 Console list views other than the grid (kanban, calendar, gallery…) still draw every bare bulkActions name as a selection-bar button, declared or not, after #30169 #30394(p2,串行在本 PR 之后,复用 undeclaredActionNames.ts)。
  • 落地条款: Fixes A list's selection bar still draws a bulk action that a disabled package withholds, as a ghost button that toasts "not executable" #30169。BREAKING:需维护者在席位线程中明说同意后,席位才执行 pr_ready + automerge_enable。

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

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A list's selection bar still draws a bulk action that a disabled package withholds, as a ghost button that toasts "not executable"

1 participant