Repository navigation
fix(ui): a refused inline-grid save marks its cells and says every reason; a refused form save is said once - #28396
Conversation
The grid's save bar says a refusal whole, one line per reason, marks the cells the refusal names, and sits on the grid's inset; ObjectGrid checks the record form's field rules before sending an inline edit. A refused form submit is said once, by the banner brought into view. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxQtKRyLyL6eK14pgY
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxQtKRyLyL6eK14pgY
…grid's pre-send rule check Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxQtKRyLyL6eK14pgY
…e-send check Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxQtKRyLyL6eK14pgY
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxQtKRyLyL6eK14pgY
|
domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #27520 (rows 6 and 19 of the 18.3.0 RC walkthrough; the seat's claim 6541435370 and its amendment 6541672013 after the live measure, which adds ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The form says a refusal once and brings that one message into view, which is the case the retired toast existed for; the grid says every reason whole, marks the cells the refusal names and clears them on edit or save; the pre-send check judges the field's own rules through the form's own emitter and leaves everything else to the server with the refused row unsent and the others saved; the declarations and the patch level are right; the pins are adapted not deleted and hold both ways; and every check on this head is green or skipped. Implemented-by: VERDICT: PASS |
|
domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN 席位核对ACCEPT · seat 席位意见:在表格里批量编辑后点「全部保存」,如果服务器拒绝,保存栏现在完整列出每一条原因,一条一行,不再截断;被拒绝的单元格会标出来,改动该单元格后标记消失。表格自带的保存在发送前会先检查必填、最小值/最大值、长度和格式:不通过的行不发送,其余行照常保存。「全部保存」按钮离右边缘留出 12 像素。表单保存被拒绝时只显示一条消息,就是表单顶部的横幅,并自动滚动到可见处,不再同时弹出内容相同的 toast。
|
Requested by Tesla · project thread
Part of #27520 (rows 6 and 19; every other row stays on the card)
Clause-②: no
Metadata-protocol: none
维护者速读
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)。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).truncateline in the save cluster, with the rest only in atitletooltip. It is now its ownrole="alert"row (data-testid="table-save-refusal") under the toolbar, taking the full width (sm:basis-full). One reason readsSave failed: <reason>; several readSave failed:with one list item each. The lines come from the shared@object-ui/reactreaders the form already uses (unmarkedRefusalLines, falling back to the error's message), deduplicated across rows.aria-invalid,data-cell-errorandtitlecarry the reason, and an inset destructive ring draws it. The marks come fromextractFieldErrors(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.data-slot="table-toolbar"and wraps onsm.ObjectGridinsets 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).objectGridInlineRules.ts(not exported):inlineEditRuleRefusalsreads each changed field's declaration throughbuildValidationRulesfrom@object-ui/fields, the same rules the record form checks:required(blank value),min/max(a staged number or number string),minLength/maxLengthand the email / URLpattern. An authored rule message wins; otherwise thevalidation.*key is localized with the header's label.inlineRuleRefusalErrorbuilds the server's refusal shape (code: 'VALIDATION_FAILED',fields[]), so the table marks and says it like a server refusal.defaultRowSavethrows that refusal before claiming or writing.saveRowsClaimedrefuses such a row unsent and still sends the others. A host's ownonRowSave/onBatchSaveis 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.tsgains thevalidation.*defaults, byte-identical to the en pack.Row 19, a refused form save is said once (
form.tsx).visibleWhenfault,onAction'sresult.error, a thrown write) raised the banner andtoast.error(…, { id: outcomeToastId })together. They now callraiseRefusal: 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.cross_fieldrule withfields: [qty, max_qty]marks Quantity). Ascriptrule declares no fields, so its refusal names only_recordand there is nothing to mark. Recorded on the card, not filed.Tests
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.ui/packages/plugin-grid/src/__tests__/inlineEditRuleCheck-27520.test.tsx(7): over the realObjectGrid+data-table, Save All refuses the row belowminunsent, 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.form-one-toast-per-refusal-13340.test.tsxnow 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 componentsform-invalid-toast-joiner,form-refusal-lines-28053,form-server-field-errors,visibleWhen-fault-refuses-submit-8069,fieldtab-visiblewhen-6237; in plugin-formMasterDetailForm.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, allCannot 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-checkof@object-ui/componentsand@object-ui/plugin-grid: exit 0.Acceptance notes
bulkCreateisPromise.all(map(create)), so a refused row leaves every earlier row of the batch landed —updateManyon the same driver refuses before mutating anything #13340 test is adapted, keeping its retry guarantee. Contract review is owed for this.scriptrule cannot be drawn: the spec gives a script rule nofields, so the server's answer names_record. Recorded, not filed.onRowSave/onBatchSaveowns its own checking.Verification
Live before/after (showcase on a --fresh stack, admin, 1440x900)
Base
origin/main3ba5206 (before) and this branch (after). Backendexamples/app-showcaseobjectstack dev --seed-admin --fresh, Console fromui/apps/consolesource through vite, driven with Playwright.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 answered400 VALIDATION_FAILEDnamingtitleandestimate_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.PUT /api/v1/meta/object/t27520_widget(ascriptrule and across_fieldrule), create page/newand 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. Thecross_fieldrefusal marks Quantity before and after.Derived gates
At f77d796:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 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-debtfirst 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.pnpm --dir ui check:new-line-citationsexit 0;node scripts/check-sdui-manifest.mjsexit 0.Generated by Claude Code