Repository navigation
fix(plugin-grid,plugin-list,components,types): the grid's inline-edit saves send each row's If-Match, and Save All reports per row (objectui#12292) - #12309
Conversation
… saves send each row's If-Match and report a batch per row (objectui#12292) WIP: implementation before build and pins. Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
…es, the token projection and the per-row batch outcome (objectui#12292) Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
…ite, and Save All reads a per-row outcome (objectui#12292) Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
…l; drop an unused import from the per-row pin (objectui#12292) Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
… default saves now pass (objectui#12292) Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Seat REWORK: objectui PR #12309 at
|
…atchSave outcome (objectui#12292) Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
Seat ACCEPT: objectui PR #12309 at
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Seat REWORK (2): objectui PR #12309 at
|
…ves to writeRowGuarded and resolveRowConflict (objectui#12292) Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #12292 (body, triage ① Derived judgmentsGate verdicts on the head. 33 check-runs; 30 1. 2. 3.
4. 5. Pins against the ruling. The grid pin renders the real ② Semver level
Clause-②: yes — the claim's ③ Boundary flags
Implemented-by: VERDICT: PASS |
Seat ACCEPT: objectui PR #12309 at
|
…any rows they saved Save All and the per-row save raise the shared success toast, counting only the rows the server accepted (built on the per-row outcome #12309 landed). Every locale pack gains the table.saved count family. Claude-Session: https://claude.ai/code/session_01RZYixBnXPWvcm2YenWo6eJ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #12292
Clause-②: yes —
DataTableSchema.onBatchSavein@object-ui/types(published) widens its declared return: a handler may now resolve with{ failed: [{ rowIndex, error }] }. Additive: a handler that resolves with nothing, or rejects, is read exactly as before, so no existing handler changes. It is needed because the ruling's batch clause (a refused row fails on its own, the other rows are reported as they are) cannot be met while the table reads every batch as all-or-nothing. No prop, spec key, export or i18n key is added.Dispatched by the
domain:uiseat 1 (claim comment 6137901330). Implementing session:https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z.What changed
@object-ui/plugin-gridObjectGriddefaultRowSaveanddefaultBatchSavesend each row'supdated_atasifMatch, read as the record surfaces read it (occVersionOfinplugin-form, the save inplugin-detail'sInlineEditSaveBar). A row with no token stays unguarded, as before.versionToSendruleplugin-formapplies to a form that stays open, applied per record. A newer read of the row wins over it.409opensConcurrentUpdateDialogfrom@object-ui/plugin-detail, the dialogInlineEditSaveBaropens.plugin-gridalready depends onplugin-detail, so there is no new dependency edge. Reload latest writes nothing and declares the record changed on the invalidation bus, so whoever holds the rows re-reads them. Overwrite anyway re-sends the same changes keyed to the409'scurrentVersion. Cancel rejects with the409, so the table keeps the edit and flags the row. There is no grid-only dialog, and nothing is retried without the user's choice.Promise.allSettled), asks about conflicts one row at a time, and hands back the rows that did not persist as{ failed }. Nothing the server accepted is rolled back.updated_atwhen the grid is authorededitable.@object-ui/plugin-listListView: its query asks forupdated_atwhile the grid it hands rows to is in inline-edit mode, the same expression as that grid'seditable.@object-ui/componentsdata-table:saveBatchreads a{ failed }outcome per row. It keeps and flags exactly those rows and clears the rest. A failure naming a row the batch did not hand over falls back to all-or-nothing, so an unsaved edit is never cleared.@object-ui/types: theonBatchSavedeclaration and its doc.content/docs/plugins/plugin-grid.mdx,content/docs/components/complex/data-table.mdx,packages/plugin-grid/README.md.@object-ui/plugin-formwrite census (affordanceGrantMap-12082.test.tsx, a test file; nothing ofplugin-formships from it): the grid's twoupdatesites move fromObjectGrid > defaultRowSaveandObjectGrid > defaultBatchSavetoObjectGrid > writeRowGuardedandObjectGrid > resolveRowConflict, stillmapped(['listInlineEdit'], OBJECT_GRID)(round 3,b87fdb4; this line added by the seat, as the dev's report6140159070names)..changeset/12292-grid-save-if-match.md:minorfor@object-ui/typesand@object-ui/components,patchfor@object-ui/plugin-gridand@object-ui/plugin-list(levels corrected at0af4fccafter the seat's REWORK; this line updated by the seat, as the dev's report6139070698names).Measured before writing (the dispatch's mechanism assumptions)
data-table'ssaveRowkeeps and flags its one row.saveBatchwas all-or-nothing: on a rejection it kept every pending row, flagged every row and showed one "Save failed" line, with no toast. UnderPromise.all, one refused row therefore left the rows that had already landed pending and flagged, and a retry would send them again. So the batch clause needs the table to read a per-row outcome. That is the published-type change above.plugin-detailexportsConcurrentUpdateDialogandisConcurrentUpdateError, andplugin-gridalready depends on it.plugin-form'suseOccSaveis module-private toplugin-form, whichplugin-griddoes not depend on. The grid builds the dialog's payload the wayInlineEditSaveBar'sbuildConflictdoes (one field shows its own before and after; several show the record summary under the existingdetail.concurrentUpdateRecordLabelkey) rather than exporting that callback, soplugin-detailis read, not edited.If-Matchanswers through the console's adapter. Read from the installed 17.7.0 packages.@objectstack/typesmaps aConcurrentUpdateErrorto409with{ error, code: 'CONCURRENT_UPDATE', currentVersion, currentRecord, object }.@objectstack/clientthrows an Error carryingcode,httpStatusanddetailsset to that body.data-objectstack'snormaliseClientErrorre-wraps it asConcurrentUpdateErrorwithcurrentVersionandcurrentRecord. The pins drive the real adapter and client over a stubbedfetchthat answers exactly that envelope. NOT MEASURED: a round trip against a live server, because no objectstack server runs in this container.updated_atalone sends nothing in the console. The console list's rows come fromListView's query, and its$selectis the columns plusid, expand and predicate operands. No column of the showcase task grid view showsupdated_at, and the server projects exactly the keys selected. The grid's own query has the same shape. So both projection sites now carry the token. Without that, the save code would be correct and the console would still send noIf-Match.Pins (all on the real
ObjectGridanddata-table)packages/plugin-grid/src/__tests__/inlineEditConcurrency-12292.test.tsx. Writes go through the realObjectStackAdapterand@objectstack/client; reads honour$selectthe way the server does, so a projection that drops the token fails these pins too.If-Matchand surfaces the409on the conflict dialog. Cancel writes nothing, keeps the edit and flags the row.409'scurrentVersion. Reload latest writes nothing and re-reads.PATCHes (no retry).updated_atthough no column shows it.packages/plugin-list/src/__tests__/ListView.concurrencyTokenProjection-12292.test.tsx: the list asks forupdated_atin inline-edit mode, and not outside it (control).packages/components/src/__tests__/data-table-batch-per-row-12292.test.tsx: a{ failed }outcome keeps only the named row; an empty one clears all; an unplaceable one keeps all.Two existing pins (
inlineEditExitMode.test.tsx,inlineEditPersistence.test.tsx) asserted the save's three-argument call. They now state the fourth argument,undefined, because their fixtures carry noupdated_at.Ablation (one-shot, each leg restored from
HEADand proven by blob hash and an emptygit diff HEAD)Run through
ablation-replace.mjs; tests resolve@object-ui/*tosrcthrough the root vitest alias table, so nodistrebuild was involved.ifMatchfrom the default saves (at989c000): 5 of 6 grid pins red (no dialog appears; the next-save pin readsifMatch: undefined). The projection pin stays green, as expected.aee4247): 6 of 6 grid pins red.ListView's projection (ataee4247): the inline-edit pin red, the control green.data-tableignore the per-row outcome (ataee4247): the per-row pin red, the other two green.Gates (at
d061c98unless stated)pnpm exec vitest run packages/components/ packages/types/: 753 files passed, 1 skipped; 14089 tests passed, 24 skipped.pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/plus five outside suites that render the grid in inline-edit mode (plugin-viewObjectView.namedViewInlineEdit-10885,ObjectView.tableEditable-10976,ObjectView.namedViewEditInlineFold-5144;app-shellObjectView.inlineEditSessionOnly-5144,InterfaceListPage.editInlineFold-5144): at5a050db, 339 files passed and 2 failed, both on the call-shape assertion above.d061c98changes only those two test files, and they plus the new grid pins pass there (3 files, 13 tests).type-checkof@object-ui/types,@object-ui/components,@object-ui/plugin-grid,@object-ui/plugin-list: exit 0 each, script name echoed (plugin-gridre-run at5a050db).--format json: 0 errors. The config sets no type-aware parser options and no rule ineslint-rules/reads another file, so this diff cannot move the verdict of an untouched file.node scripts/check-changeset-presence.mjs,pnpm check:changeset-claims,check:pending-changeset-literals,check:new-line-citations,check:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:handler-key-reads,check:i18n-keys,check:spec-symbols,check:doc-fences,check:doc-json: all exit 0.check:phantom-deps/check:unused-deps(no package gains a dependency);check:eager-closure(no module edge is added:ObjectGridalready imports theplugin-detailentry);check:i18n-*beyondi18n-keys(no string added).Acceptance notes
ObjectGrid.tsxalone. Measurement put two producers elsewhere: the token's projection inplugin-list'sListView, and the batch outcome's reader incomponents'data-tablewith its declaration intypes. The dispatch allowed a fix at the producer; the widening is the only published change.ApiDataSource(@object-ui/core) throws a plain Error on any non-2xx, so a409through it is not recognised as a conflict on any surface. The console usesdata-objectstack. Noted only.Generated by Claude Code