Repository navigation
Commit d5ab8fc
fix(plugin-grid): on a phone, each list card offers its row's actions through one 44 px menu (#29301)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbUxcj8HNVLVidnzZ1WNvjyD)_
Fixes #29150
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:手机宽度(768 px 以下,列表画成卡片)时,每张卡片右侧多了一个 44 px
的「⋮」按钮,点开就是这一行在桌面上的全部行操作(编辑、删除、对象声明的行操作),顺序与桌面行的右键菜单一致。点卡片其它位置仍然打开记录。这一行没有任何可用操作时不画按钮。桌面表格不变。
- **为什么改**:#29074 在 375 / 390 px
实测:手机卡片上没有任何行操作入口,长按也只是打开记录。手机用户要编辑或删除一条记录必须先进记录页,只在列表上提供的行操作(`list_item`)根本够不到。
- **风险与回滚**:低。只改 `ObjectGrid.tsx` 卡片渲染里一处:复用桌面行的 `RowActionMenu` 和
`rowActionMenuProps(row)`(同一套权限、谓词、处理函数和确认框),内联预算设为
0,所以所有操作都在一个菜单里。按钮外层用 `-my-2`,实测卡片高度不变(62 px)。不加键、不加
prop、不加导出、不加文案。回滚即回退本 PR。
- **先看**:`ui/packages/plugin-grid/src/ObjectGrid.tsx` 的
`buildMobileCardRenderer` 末尾 `grid-card-actions` 那一段(只在
`rowOffersAction(row)` 时绘制);新测试
`ui/packages/plugin-grid/src/__tests__/phoneCardRowActions-29150.test.tsx`。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/plugin-grid/src/__tests__/phoneCardRowActions-29150.test.tsx`,预期
4 个用例全部通过;在 `main` 上同一文件 3 个失败。
- **需要你做的**:无,席位落地。
Claim: issuecomment-6574754357.
## What changes
Below the 768 px card breakpoint, `ObjectGrid` draws each row as a card
(the #28484 layout: title, one subtitle line, one status chip). A card
had no action control, so a phone reader had to open each record to
edit, delete or run a declared row action, and an action offered only on
the list (`list_item`) could not be reached at all.
- **`ObjectGrid`** (plugin-grid), `buildMobileCardRenderer`: each card
whose row offers an action ends with the row's own `RowActionMenu`,
rendered from `rowActionMenuProps(row)`, the props the desktop row's "⋮"
cell and its context menu already read. So it applies the same
capability gate, per-record predicates, record-level verdicts, handlers
and confirm steps.
- The inline budget is `0` (`maxInlineActions={0}`), the value the
desktop row's context menu passes: every action sits in the one menu, in
the plan's order (Edit, Delete, primary actions, other actions, legacy
actions). On desktop the same set shows in the same order, except that
up to one primary action is drawn as a button left of the "⋮".
- The trigger is `RowActionMenu`'s existing one, 44×44 below `sm`. Its
wrapper stops the click, so a tap on the control does not open the
record, and a tap anywhere else on the card still does. The card's
wrapper box takes `-my-2`, so the 44 px target does not make the card
taller.
- A card whose row offers nothing (`rowOffersAction(row)` false) draws
no control.
The desktop table, the row context menu and the metadata are unchanged.
No key, prop, export or locale string is added; the trigger's
screen-reader label is the existing `grid.openMenu`.
## Tests
- `phoneCardRowActions-29150.test.tsx` (new), on the real `ObjectGrid`
with Edit, Delete and three `list_item` actions (one primary, one gated
by `visible`):
- At 1280 px, the row context menu is read as the reference: Edit,
Delete, Promote (primary), Submit for review (draft rows only), Archive.
At 390 px, each card's menu lists exactly that row's reference, in the
same order, and no inline button is drawn on a card.
- One trigger per card, inside the card's last box, with the 44 px
classes.
- Choosing Edit from a card's menu calls `onEdit` with that row and does
not call `onRowClick`. A tap on another card's title calls `onRowClick`
with that row.
- Control: a grid with no action draws no trigger on any card.
- Red on `main` (0c0d3b6): the first three cases fail ("Unable to find
an element by: [data-testid="row-action-trigger"]"), the control passes.
- Ablation (one time, not kept): `maxInlineActions={0}` changed to
`maxInlineActions={1}` through `scripts/ablation-replace.mjs` reds the
order case (the primary moves onto the card as an inline button). The
restore left `git diff HEAD` empty. A first attempt replaced the anchor
with a string that already occurred 41 times; the tool refused it as a
no-op, so it did not run.
- Neighbours: `phoneCardRow-28484.test.tsx` and
`rowContextMenu-28434.test.tsx` pass unchanged (29/29 with the new
file).
## Acceptance notes
Live measurement, Console production build of this branch (`pnpm
console:build` at 49c4abf), served at `/_console` by `pnpm dev:crm --
--seed-admin --fresh`. Chromium with `isMobile` and `hasTouch`, DPR 2,
at 390×844 and 375×812; desktop reference at 1280×900. Script and
readings: `/mnt/project-files/issue-29150/measure.cjs`,
`after/measure.json` (`crm_lead`) and `after/measure-account.json`
(`crm_account`).
| Check (`crm_lead`, which declares the `list_item` action Convert Lead)
| 390 | 375 |
| --- | --- | --- |
| Each card has one row-action control | yes, 44×44, inside the card |
yes, 44×44, inside the card |
| Card height with / without the control | 62 / 62 px | 62 / 62 px |
| Card menu equals the desktop row's actions, same order | Edit, Delete,
Convert Lead = desktop "⋮" and context menu | same |
| Tap on the control opens the record | no (URL unchanged) | no |
| Tap on the card opens the record | yes | yes |
| Menu fits the viewport; no horizontal page scroll | yes; 390 = 390 |
yes; 375 = 375 |
| Title width beside the chip and the control | 200 px on a lead card;
292 px on an account card | 277 px on an account card |
`crm_account` (Edit and Delete only) reads the same: 44×44 control, card
62 px either way, menu equal to the desktop menu, the card tap opens the
record. Before this change (`main` a4015ab, #29074's run record, check
L06) a card held no control and a long press opened the record.
Out of scope, reported to the seat: the menu's items are 32 px tall at
phone width. That is the shared dropdown item, the same in every phone
menu, not this card's control.
## Verification
All local readings are on head 49c4abf.
<details><summary>Touched package and neighbours</summary>
- `pnpm --dir ui exec vitest run
packages/plugin-grid/src/__tests__/phoneCardRowActions-29150.test.tsx
packages/plugin-grid/src/__tests__/phoneCardRow-28484.test.tsx
packages/plugin-grid/src/__tests__/rowContextMenu-28434.test.tsx`: 3
files, 29 tests passed.
- The plugin-grid test files that import `ObjectGrid` (`git grep` set,
163 files) at `--maxWorkers=2` in four batches: 41 + 41 + 41 + 40 files
passed, 269 + 372 + 286 + 360 = 1287 tests passed. CI's Test UI runs the
rest of the package.
- `pnpm exec turbo run type-check --filter=@object-ui/plugin-grid`: exit
0 (`tsc --noEmit && tsc -p tsconfig.test.json`; `--listFilesOnly` on the
test project lists the new file).
- `eslint` on the two changed ui files: 0 errors (warnings only, the
file's existing `any`).
- `pnpm --dir ui run check:new-line-citations`: 0 new citations.
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands` derived 35 families for
this diff; each ran locally on 49c4abf and exited 0. `dispatch-gates
--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.
```text
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 --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:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: 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:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
```
`vitest run scripts/` read 169 files passed, 2 skipped (4900 tests
passed). The type-check lanes and the wide-population families are CI's.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; lint passes on the changed files.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 section is
the maintainer summary the lane asks for).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jVMaEeoiKQgHPaTy3Z)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 40eab63 commit d5ab8fc
3 files changed
Lines changed: 244 additions & 1 deletion
File tree
- .changeset
- ui/packages/plugin-grid/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
| 77 | + | |
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| |||
3315 | 3315 | | |
3316 | 3316 | | |
3317 | 3317 | | |
| 3318 | + | |
| 3319 | + | |
| 3320 | + | |
| 3321 | + | |
| 3322 | + | |
| 3323 | + | |
| 3324 | + | |
| 3325 | + | |
| 3326 | + | |
| 3327 | + | |
3318 | 3328 | | |
3319 | 3329 | | |
3320 | 3330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
0 commit comments