Repository navigation
Commit 75ecaf4
fix(ui): a refused inline-grid save marks its cells and says every reason; a refused form save is said once (#28396)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoBcbFtjDn1QChVRkBtHrTF3)_
Part of #27520 (rows 6 and 19; every other row stays on the card)
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:表格行内编辑点「Save
All」被服务端拒绝时,出错的单元格加红框(悬停显示原因),每条原因完整地单独一行显示在工具栏下方,不再截断;表格自带保存在发送前先按字段规则(必填、最小/最大值、长度、邮箱/网址格式)检查,违规的行不发送,其他行照常保存;「Save
All」不再贴在表格右边缘。表单保存被拒(如 Studio 建的对象的校验规则)时只显示一条横幅并滚动到可见处,不再同时弹一条一模一样的
toast。
- **为什么改**:18.3.0 RC 走查第 6、19 行:用户看不出是哪一格错了、原因被截断、明知违反 min
的值还发到服务端;表单被拒时同一句话说两遍。
- **风险与回滚**:发送前检查只读字段自己声明的规则,读不到就不判,服务端仍是最终裁判;表单不再弹错误 toast,这推翻了
objectui#13340 钉住的「横幅+一条 toast」行为(测试已按席位要求改写,保留「重试不叠加」的保证),需要契约复审。回滚即
revert 本 PR。
-
**先看**:`ui/packages/components/src/renderers/complex/data-table.tsx`(`saveRefusalLines`、`cellErrors`);`ui/packages/plugin-grid/src/objectGridInlineRules.ts`;`ui/packages/components/src/renderers/form/form.tsx`(`raiseRefusal`)。
- **两分钟验证**:showcase 打开 Tasks → Grid,清空一行的 Title、把 Estimate 改成 -5,点 Save
All,预期两格红框、横幅列出两条原因、网络面板没有 PATCH;用一个带 script 校验规则的对象新建记录并触发规则,预期只有横幅、没有
toast。
- **需要你做的**:无,席位落地。
## What changes
**Row 6, a refused inline save says every reason and marks its cells**
(`data-table.tsx`, the renderer that owns the grid's save bar).
- The refusal was one `truncate` line in the save cluster, with the rest
only in a `title` tooltip. It is now its own `role="alert"` row
(`data-testid="table-save-refusal"`) under the toolbar, taking the full
width (`sm:basis-full`). One reason reads `Save failed: <reason>`;
several read `Save failed:` with one list item each. The lines come from
the shared `@object-ui/react` readers the form already uses
(`unmarkedRefusalLines`, falling back to the error's message),
deduplicated across rows.
- The cells a refusal names are marked: `aria-invalid`,
`data-cell-error` and `title` carry the reason, and an inset destructive
ring draws it. The marks come from `extractFieldErrors` (`fields[]` /
`details.fields` / `validationErrors`) and are kept per row key, so they
follow the record across re-reads. Editing a marked cell clears that
cell's mark; a save that lands, Cancel and Cancel All clear the row's or
all marks. A whole-batch rejection names no row, so it says its reasons
and marks no cell.
- The toolbar gets `data-slot="table-toolbar"` and wraps on `sm`.
`ObjectGrid` insets it (`px-3 pt-2`), so Save All no longer sits on the
grid's right edge.
**Row 6, the grid checks a field's own rules before sending**
(`plugin-grid`).
- New `objectGridInlineRules.ts` (not exported):
`inlineEditRuleRefusals` reads each changed field's declaration through
`buildValidationRules` from `@object-ui/fields`, the same rules the
record form checks: `required` (blank value), `min` / `max` (a staged
number or number string), `minLength` / `maxLength` and the email / URL
`pattern`. An authored rule message wins; otherwise the `validation.*`
key is localized with the header's label. `inlineRuleRefusalError`
builds the server's refusal shape (`code: 'VALIDATION_FAILED'`,
`fields[]`), so the table marks and says it like a server refusal.
- `defaultRowSave` throws that refusal before claiming or writing.
`saveRowsClaimed` refuses such a row unsent and still sends the others.
A host's own `onRowSave` / `onBatchSave` is unchanged. A field with no
declaration, or a rule set that cannot be read, is not judged;
validation rules, uniqueness and state machines still reach the server.
- `objectGridTranslations.ts` gains the `validation.*` defaults,
byte-identical to the en pack.
**Row 19, a refused form save is said once** (`form.tsx`).
- The three refusal points (a `visibleWhen` fault, `onAction`'s
`result.error`, a thrown write) raised the banner and `toast.error(…, {
id: outcomeToastId })` together. They now call `raiseRefusal`: dismiss
any outcome toast, set the banner, and scroll the banner into view
(`block: 'nearest'`). No error toast is raised, so a refusal the banner
draws is said once. Every refusal path of this renderer draws the
banner, so the seat's "keep the toast where no banner is drawn" case
does not arise here.
- The field mark for a rule that names a field already worked on main
(measured: a `cross_field` rule with `fields: [qty, max_qty]` marks
Quantity). A `script` rule declares no fields, so its refusal names only
`_record` and there is nothing to mark. Recorded on the card, not filed.
## Tests
- New
`ui/packages/components/src/__tests__/data-table-refusal-cells-27520.test.tsx`
(4): Save All says two reasons as two whole list items and marks the
refused row's two cells only; editing a marked cell removes its mark and
keeps the other; a single-row save marks, and a later save that lands
clears; a whole-batch rejection marks no cell.
- New
`ui/packages/plugin-grid/src/__tests__/inlineEditRuleCheck-27520.test.tsx`
(7): over the real `ObjectGrid` + `data-table`, Save All refuses the row
below `min` unsent, marks its cell and saves the other row; the per-row
save refuses the same edit unsent; a control edit inside the rules is
sent. Unit: one entry per broken rule with the localized message; a
staged number string is read as a number and length is judged on text;
no declaration judges nothing; an authored message wins.
- Adapted, not deleted (the seat's condition):
`form-one-toast-per-refusal-13340.test.tsx` now pins one banner,
scrolled into view, zero error toasts, and a retry that still leaves one
banner. The other refusal pins that asserted the toast now read the
banner and assert no error toast: in components
`form-invalid-toast-joiner`, `form-refusal-lines-28053`,
`form-server-field-errors`, `visibleWhen-fault-refuses-submit-8069`,
`fieldtab-visiblewhen-6237`; in plugin-form
`MasterDetailForm.outcomeToastSupersede`,
`MasterDetailForm.singleRefusalRaiser`, `MasterDetailForm`,
`WizardForm.outcomeToastSupersede`, `submitHandlerSeam`,
`submitTargetRefusal`.
- `pnpm --dir ui exec vitest run --maxWorkers=3 packages/components/
packages/plugin-grid/ packages/plugin-form/` at f77d796: 944 files
passed, 1 skipped; 9131 tests passed, 25 skipped (lock VERDICT
command-exit 0). The first run at 9edf9af had 25 reds in seven
plugin-grid inline-edit files, all `Cannot read properties of undefined
(reading 'min')` from the pre-send check. f77d796 makes an unreadable
rule set judge nothing, and the rerun above is green.
- `type-check` of `@object-ui/components` and `@object-ui/plugin-grid`:
exit 0.
- The whole app-shell and console suites were not run locally; they are
left to CI. A search of both for the old refusal toast and the save-bar
text found no pin.
## Acceptance notes
- **This reverses a pinned visible behaviour.** objectui#13340 pinned "a
refusal raises the banner and exactly one toast". The seat accepted the
change on two conditions, both met: the banner is the one message and is
scrolled into view, and the #13340 test is adapted, keeping its retry
guarantee. Contract review is owed for this.
- Row 19's field mark for a `script` rule cannot be drawn: the spec
gives a script rule no `fields`, so the server's answer names `_record`.
Recorded, not filed.
- The pre-send check covers only the built-in saves. A host that
supplies `onRowSave` / `onBatchSave` owns its own checking.
## Verification
<details><summary>Live before/after (showcase on a --fresh stack, admin,
1440x900)</summary>
Base `origin/main` 3ba5206 (before) and this branch (after). Backend
`examples/app-showcase` `objectstack dev --seed-admin --fresh`, Console
from `ui/apps/console` source through vite, driven with Playwright.
- Row 6, Tasks → Grid, Title cleared and Estimate set to -5, Save All.
Before: one truncated banner line `Save failed: Title is required and
cannot be cleared; Es…` (384x20 px), no cell marked, `PATCH
/api/v1/data/showcase_task/<id>` sent with -5 and answered `400
VALIDATION_FAILED` naming `title` and `estimate_hours`, Save All 0 px
from the content edge. After: Title and Estimate cells marked, no write
sent, banner "Save failed:" with "Title is required" and "Estimate (h)
must be at least 0" as two lines, Save All 12 px from the edge.
- Row 19, an object authored with `PUT
/api/v1/meta/object/t27520_widget` (a `script` rule and a `cross_field`
rule), create page `/new` and the list's New dialog. Before: a
script-rule refusal (`fields: [{ field: '_record', code:
'rule_violation' }]`) drew the banner and an identical toast, on both.
After: the banner only, no toast, on both. The `cross_field` refusal
marks Quantity before and after.
</details>
<details><summary>Derived gates</summary>
At f77d796: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 35 families against the diff. All 35
ran with `</dev/null`, exit codes recorded first. `dispatch-gates
--ran`: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED".
- `pnpm check:type-check-debt` first exited 3 (not measured): it queued
behind this branch's own test run on the verify lock and timed out.
Rerun under the lock: exit 0, "0 raw tsc error(s) total, none above its
recorded number".
- `pnpm --dir ui exec vitest run scripts/` under the verify lock: 169
files passed, 2 skipped, exit 0.
- Also run: `pnpm --dir ui check:new-line-citations` exit 0; `node
scripts/check-sdui-manifest.mjs` exit 0.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibxQtKRyLyL6eK14pgY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4a7b99f commit 75ecaf4
20 files changed
Lines changed: 814 additions & 141 deletions
File tree
- .changeset
- ui/packages
- components/src
- __tests__
- renderers
- complex
- form
- __tests__
- plugin-form/src
- 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 | + | |
| 13 | + | |
| 14 | + | |
| 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 | + | |
0 commit comments