Skip to content

fix(plugin-grid): fold a row's inline primary action into its menu while the table overflows with it - #33441

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-vbend6
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-vbend6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33230

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:列表行的主按钮(variant: primary)只在表格放得下它时内联显示;放不下就并进该行的「⋮」菜单顶部,右侧固定的操作列随之缩到只剩「⋮」。
  • 为什么改:记录抽屉里放大的相关列表只有 622 px 宽,内联按钮让固定操作列宽一倍(139 px 对 68 px),表格因此溢出,固定列盖住最后一列「状态」。
  • 风险与回滚:只动 plugin-grid 的行操作列宽度决策,不改协议、不改服务端;回滚即 revert 本 PR。全页列表放得下时行为不变,手机卡片布局不经过这段代码。
  • 先看:objectGridInlineActionFold.ts(折叠规则与读数时机)、objectGridActionsColumn.tsx(折叠时内联预算取 0)、ObjectGrid.tsx(接线)。
  • 两分钟验证:在本 PR 的 E2E chromium 检查日志里搜 related-list-pinned-actions,预期 drawer1440 / drawer1280 / drawer1024 三行都是 folded:true、inlineButtons:0,且 statusRight 不大于 actions.left;fullPage1440 一行 folded:false、inlineButtons:3。
  • 需要你做的:无,席位落地。

What changes

ObjectGrid pins its Actions column to the right edge of the table's scroll box. A variant: 'primary' row action draws as an inline button beside the row's "⋮" trigger, and that button roughly doubles the pinned column. In a record drawer's enlarged related list the content is 622 px wide at the default drawer size, and the extra width alone turns a table whose data columns fit into one that overflows. The pinned column then sits over the last data column (Status) at rest.

The grid now draws inline row buttons only while the table fits its scroll box with them.

  • When the table overflows with them, each row's primary action moves to the top of that row's "⋮" menu, through the row-menu planner with an inline budget of 0. No action is dropped, and the pinned column shrinks to the trigger.
  • The buttons come back once the scroll box is at least as wide as the table was with them.
  • Every reading waits until the table's size has held for two animation frames, because the table sizes its columns from what the cells drew over several passes, and a reading taken mid-pass sees widths it never settles at.
  • The decision starts over (unfolded) whenever the rows or the column set change.
  • The grid's host carries data-row-actions-inline="folded" while folded, so the state is readable in the DOM.

No export, prop or authored key changes. maxInlineRowActions stays a non-author surface.

Producer, measured on origin/main before the fix. It is the pinned column's width, not the scroll box's reservation. The box does reserve the column: its scroll width includes it, and scrolling to the end reveals Status. At rest, though, the 139 px column (with the inline button) covers the last data column, and elementFromPoint at the Status cell's centre answered the inline Allocate button (box 900 px, table 954 px, Status spanning 735 to 815 under the column from 761). Menu-only the column is 68 px.

Tests

ui/e2e/related-list-pinned-actions-width.spec.ts mounts the real RelatedList enlarged face (then object-grid, data-table and the row menu) in Chromium, with Vite-bundled sources and compiled Tailwind, inside a frame of the drawer's or the list page's width. Reachability is read from layout with elementFromPoint, and the menu path is driven with real clicks.

  • Drawer (622 px) at 1024, 1280 and 1440 px windows: the inline button is folded, the table no longer overflows, Status's cell and header are hit at their centres, Status ends at or before the Actions column's left edge, and Allocate dispatches from the "⋮" menu.
  • CONTROL, full list page (1184 px at a 1440 window): not folded, three inline buttons, Status hit, and the inline click dispatches.
  • CONTROL, 390 px: the card layout draws (no table scroll box, no fold mark), with Allocate in the card's menu.
  • A table that overflows the drawer even menu-only: it folds too, and Status is reached by scrolling to the end.
Readings, at this head
drawer1440    box 622/622  folded true   inline 0  actions left 554 width 68   statusRight 554  hit true
drawer1280    box 622/622  folded true   inline 0  actions left 554 width 68   statusRight 554  hit true
drawer1024    box 622/622  folded true   inline 0  actions left 554 width 68   statusRight 554  hit true
fullPage1440  box 1184/1184 folded false inline 3  actions left 1045 width 139 statusRight 1045 hit true
drawerWide1440 box 622/912 folded true   inline 0  actions left 554 width 68   statusRight 844 (reached after scrolling)
6 passed
Ablation at 3afff0c (the fold's decision made a no-op, through scripts/ablation-replace.mjs)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 53abf59026e5 -> 4dec1046a3e3
drawer1440  box 622/654 folded false inline 3 actions left 483 width 139 statusRight 515
  Error: the pinned Actions column covers part of Status at rest
drawer1280  (same readings)  Error: the pinned Actions column covers part of Status at rest
drawer1024  (same readings)  Error: the pinned Actions column covers part of Status at rest
fullPage1440 unchanged, passed
drawerWide1440 folded false, failed
4 failed, 2 passed
ablation-replace: ok restored: blob == HEAD (53abf59026e5) and `git diff HEAD` is empty

Acceptance notes

  • When the table overflows the box even with the menu alone (many columns in a drawer), the folded "⋮" column still sits over the data at the box's right edge at rest, as on any list that scrolls sideways, including the full list page today. The fold makes room for every table that the inline button alone tipped over. Holding the line for a table that overflows anyway would need un-pinning the column or drawing columns narrower than their content, which is a separate design choice and not in this card.
  • The card's figures (a 753 px reading against 623 px of scroll) do not add up to one table width, so this PR reproduced the defect from the drawer's measured content width (622 px at the default 720 px pane) rather than from those figures.
  • The live showcase drawer has no enlarged related list to open (its authored page has no enlarge control), so the drawer width was measured live and the defect was reproduced and pinned in the browser harness above.

Verification

Head ee03c81e. The gate list comes from dispatch-gates --commands on this diff (37 families). Each exit code was captured before any pipe, then reconciled: dispatch-gates --ran reads 37 derived, 37 run, 0 NOT-MEASURED, 0 UNRUN, plus 4 ui runs outside the derivation. The workspace packages were built first (turbo run build over ./packages/*, exit 0).

The first ui scripts run went red on one test, e2e-type-check.test.ts "imports no workspace package". The pin's page entry named @object-ui/* packages in its source string. It now names their sources by path, and the whole suite reran green (171 files passed, 2 skipped). The gates that read the spec file reran after that change.

Local gate record (COMMAND :: exit N)
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/ci/snapshot-next.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 --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:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm --dir ui --filter @object-ui/plugin-grid run type-check :: exit 0
pnpm --dir ui run type-check:e2e :: exit 0
pnpm --dir ui run check:new-line-citations :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:published-files :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --dir ui exec vitest run packages/plugin-grid/ :: exit 0

CI families outside this list (the e2e chromium run that executes the new pin, the type-check lanes and the wide-population families) are read from this PR's check runs.


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 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nWoa1Ze1QzjRTSYXD

席位核对

Verdict: ACCEPT · head ee03c81e · 2026-11-14T21:36Z

  1. Shape. Draft → main, Fixes #33230. 5 files, +585/−4:

    • ui/packages/plugin-grid/src/ (ObjectGrid.tsx, objectGridActionsColumn.tsx, the new objectGridInlineActionFold.ts)
    • one Playwright geometry pin, ui/e2e/related-list-pinned-actions-width.spec.ts
    • one root changeset (@object-ui/plugin-grid patch)

    All of it is on the claimed surface (6883363208).

  2. Premise. Reproduced on main in Chromium. The producer is the pinned Actions column's own width: 139 px with the inline primary, 68 px menu-only. That width alone tips a table whose data columns fit into one that overflows. The scroll box does reserve the column.

    • The fix: ObjectGrid draws inline row buttons only while the table fits its scroll box with them. Otherwise each row's primary action moves to the top of that row's "⋮" menu (planner inline budget 0), so no action is dropped. The buttons return once the box is as wide as the table was with them. Readings wait for the size to hold two frames.
    • Pins (real RelatedList enlarged face, Vite-bundled sources, compiled Tailwind):
      • Drawer at 1024, 1280 and 1440: Status and its header are hit at rest, there is no overflow, and Allocate is dispatched from the row menu.
      • CONTROL: the full page keeps 3 inline buttons and covers nothing.
      • CONTROL: at 390 px the list draws cards.
      • A wide table folds and Status is reached by scrolling.
      • Ablation: 4 red, both CONTROLs green.
  3. Contract record. Not owed (Clause-②: no, Metadata-protocol: none): no export, prop or authored key changes. foldInlineRowActions is an internal parameter of resolveObjectGridActionsColumn.

  4. Gates on head ee03c81e.

    • All 53 check runs are success or skipped, including Build & E2E UI (the Playwright testDir is ./e2e), Live E2E UI, Test UI 11 planned shards + aggregate, Type Check UI, Lint UI, Lint & Repo Gates, TypeScript Type Check, Check Changeset, Governed Surface Queue Guard and PR-body guards.
    • check-governed-merges.mjs --pr 33441: NOT governed, 589 lines ≤ 3000.
    • The dev's local --ran is 37/37.
  5. The open question, ruled by the seat: A, keep as is. The order's line covers the column that the inline action's own width pushed over the data, which is the card's defect. A table that overflows even menu-only scrolls sideways under a 68 px pinned column. That is the existing list contract on the list page, and no report asks to change it.

  6. Accepted deviations.

    • The PR carries no assignees, as the order says.
    • The geometry is reproduced in a browser harness. The showcase's authored page has no enlarge control, so the drawer's 622 px content width was measured live and set by a frame.
  7. K. 21 first-parent commits since base bd439f17 ≥ 15, so I synced the base before landing. The landing waits for the rerun to go green.

  8. Out of scope. None.

  9. Authority. Landing under the maintainer's standing authorisation 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nWoa1Ze1QzjRTSYXD

Landing · head 930ebafc (base sync of the ACCEPTed ee03c81e, merge of main at 19cc8976; patch-id 178d6659780c on both heads) · 2026-11-14T21:59Z

reading value
check runs on 930ebafc 53, all success or skipped
contract record not owed (Clause-② no)

State: ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 22:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 43645886119 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/bare-name-one-holder-flow-family.dogfood.test.ts > one package holder per flow, webhook, connector and email template name, on a booted showcase > the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: one warning line: expected [] to have a length of 1 but got +0
    @objectstack/dogfood:test:  FAIL   isolated  test/bare-name-one-holder-flow-family.dogfood.test.ts > one package holder per flow, webhook, connector and email template name, on a booted showcase > ins
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: {"success":true,"data":{"manifestId":"com.qa.bare.delta","version":"1.0.0","versionId":"1.0.0","installedAt":"2026-11-14T22:03:58.968Z","hotLoaded":true,"upg
    @objectstack/dogfood:test:  FAIL   isolated  test/bare-name-one-holder-flow-family.dogfood.test.ts > one package holder per flow, webhook, connector and email template name, on a booted showcase > the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: expected [ Array(3) ] to deeply equal [ Array(2) ]
    @objectstack/dogfood:test:  FAIL   isolated  test/bare-name-one-holder-flow-family.dogfood.test.ts > one package holder per flow, webhook, connector and email template name, on a booted showcase > the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: expected [ Array(3) ] to deeply equal [ Array(2) ]
    @objectstack/dogfood:test:  FAIL   isolated  test/bare-name-one-holder-flow-family.dogfood.test.ts > one package holder per flow, webhook, connector and email template name, on a booted showcase > the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: expected [ [ 'com.qa.bare.alpha', true ], …(2) ] to deeply equal [ [ 'com.qa.bare.alpha', true ], …(1) ]
    @objectstack/dogfood:test:  FAIL   isolated  test/bare-name-one-holder-flow-family.dogfood.test.ts > one package holder per flow, webhook, connector and email template name, on a booted showcase > the
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: expected [ 'com.qa.bare.alpha', …(3) ] to deeply equal [ 'com.qa.bare.alpha', …(2) ]
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 16 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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.

ui(plugin-grid): in a record drawer, a related list's right-pinned action column (with an inline primary action) covers the Status column at rest

1 participant