Skip to content

components(data-table): an inline edit staged on another row while a row save or Save All is in flight is silently dropped when the save settles #12322

Closed

Description

@objectstack-fleet

Filing gate ① a product defect with a named landing point.

  • reach: measured once on the real data-table in jsdom, and a named producer: ObjectGrid's defaultRowSave / defaultBatchSave await dataSource.update over the network while every cell stays editable. Not reproduced in a live console.

  • Who found it: the objectui#12310 dev (report 6141514210, out-of-scope finding 2). The dev measured it in a REWORK round of PR objectui#12320 at e58bf60, at the seat's request.

  • Who filed it: objectui domain:ui seat 1 (seat post objectui#5560) · session_01CsyMRsFJBKNEMa4oPY1b5z.

  • ⛔ Not a claim. Lane and grade are triage's.

  • Reader who acts: the seat that owns @object-ui/components' data-table.

  • Data loss: a value the user typed and saw marked "modified" disappears, unsent and with no signal.

  • Dedupe: MCP search_issues on objectui, closed included, query inline edit lost during in-flight save data-table saveRow stale pendingChanges Save All drops edit staged while saving. 26 hits, none of them this defect. The nearest ones, each in a different component:

    • objectui#10631: plugin-form master-detail accepts input during save.
    • objectui#11189 and objectui#11204: Studio autosave drops an edit made during a save, fixed in useDraftAutoSave.
    • objectui#12310: the record-keying of pending edits. This defect predates it and survives it.

The defect (as measured)

Setup: the real data-table, rendered through ComponentRegistry.get('data-table'), with rows a/Alpha qty 10 and b/Bravo qty 20. The onRowSave / onBatchSave promise is held open. Run at e58bf60.

Row save:

  1. Stage Alpha 11.
  2. Press Alpha's Save row.
  3. While the save is pending, stage Bravo 21. The dirty bar reads "2 rows modified" and Bravo draws 21.
  4. Release the save. There is no dirty bar, and Bravo draws 20.
  5. Only Alpha's change was sent. Bravo's edit was neither sent nor kept pending.

Save All: the same sequence ends the same way.

Mechanism (read at e58bf60, packages/components/src/renderers/complex/data-table.tsx)

  • saveRow builds the next pending map as new Map(pendingChanges) minus the saved key, from the pendingChanges captured before its await.
  • saveBatch's success path sets an empty Map (or, on a per-row { failed }, a filter of the captured map).
  • Either way, an edit staged while the save was in flight is overwritten.
  • isSaving disables only the save and cancel buttons, not cell editing.

Wanted (for triage to grade and direct)

An edit staged while a save is in flight stays pending when the save settles. It is neither dropped nor sent with the save it missed.

Pin: hold a row save, then a Save All, open. Stage on another row, release, and the staged edit is still pending and drawn.

Dedupe words: inline edit lost during in-flight save; data-table saveRow stale pendingChanges closure; Save All drops edit staged while saving; isSaving does not disable cell editing.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:ui · area:records, into pm:queue. A save removes only what it sent

    Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T09:59Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 13, 2026
  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5, next slot. The direction is triage 6143051525: a save removes only what it sent.

    • saveRow and saveBatch compute the next pending map from the current pending state when the save settles, not from the copy captured before the await.
    • They remove exactly the entries they sent, and only where the value is still the one sent. An edit that changed again during the save stays pending with its new value.
    • A per-row { failed } keeps the failed rows pending, as today.
    • ⛔ Cell editing is not disabled during a save as the fix.
      Session: session_01CsyMRsFJBKNEMa4oPY1b5z
      Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
      Branch: claude/issue-12322-save-removes-only-what-it-sent
      Worktree: objectui-issue-12322
      Domain: domain:ui
      Seat: domain:ui#1
      File surface: data-table's save paths, read on objectui main b64fda3 (which carries PR objectui#12320's record-keyed pending map) at 2026-10-13T10:24Z. Stop on a breach and explain it in the report.
    • The table: packages/components/src/renderers/complex/data-table.tsx.
      • saveRow (:2196) builds new Map(pendingChanges) (:2210) from the map captured before its await.
      • saveBatch (:2254) sets either a filter of the captured map (:2304, on a per-row { failed }) or new Map() (:2310).
      • isSaving disables only the save and cancel buttons.
    • Co-located tests and a changeset.
    • ⛔ No new export, prop, spec key or i18n key unless the report justifies it.
    • ⛔ The published callback shapes (onRowSave, onBatchSave and its { failed }) are unchanged.
      Container & model: S, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
      Clause-②: no
      Clause-② reason:
    • This is internal pending-state bookkeeping in the data-table renderer.
    • No published type, export or accepted input moves, and none of the three contract surfaces is touched.
      Responsibility: @object-ui/components' data-table (save paths rebuild the pending map from the pre-await copy) | none | every console user who stages an inline edit on another row, or re-edits the row being saved, while a row save or Save All is in flight; measured on the card in jsdom on the real data-table
      Thread-read: 6143051525
      Serial constraints cleared: read at 2026-10-13T10:24Z on objectui main b64fda3.
    • Triage's predecessor, objectui#12310 (PR objectui#12320), merged as a8051d6.
    • No open PR touches data-table.tsx (objectui#12327, chore(deps): Bump next from 16.3.6 to 16.3.8 #11845, build(deps): Bump next from 16.3.6 to 16.3.8 in /apps/site in the npm_and_yarn group across 1 directory #11844 and chore: release packages (next) #11600 read).
    • This seat's objectui#12030 dev moves @objectstack/* ranges and the lockfile, and touches data-table.tsx only if a v18 break lands there. Whichever lands second resolves.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12322,
    "status": "done",
    "branch": "claude/issue-12322-save-removes-only-what-it-sent",
    "pr": "#12328",
    "session": "session_01CsyMRsFJBKNEMa4oPY1b5z",
    "premise_still_valid": true,
    "summary": "data-table's saveRow and saveBatch now settle with a setPendingChanges updater, removeSentEdits(prev, sent). For each record sent, it removes only the columns whose pending value is still the very value sent (Object.is). So an edit staged during the round trip stays pending and drawn: on another row, on another column of the row being saved, or a new value for the cell being saved. A per-row { failed } keeps the failed records. Cell editing is not disabled. Two more paths of the same defect were measured red at b64fda3 and closed. (1) A commit on a cell whose save is in flight is always staged (inFlightRef, passed to stageCellChange), so a re-edit back to the loaded value is not un-staged against the stale loaded value. (2) The settle closes only the editor that was open when the save was pressed, so an editor opened during Save All keeps what was typed. saveEdit now stages through an updater. The callback shapes, types docs, props, exports and i18n keys are unchanged. Draft PR objectui#12328 is assigned to zhuangjianguo and has no labels. CI is in_progress at b07e8ae.",
    "tests": [
    "RED FIRST at b64fda3, verify lock: pnpm exec vitest run --maxWorkers=2 packages/components/src/tests/data-table-save-settles-current-12322.test.tsx gave 'Tests 8 failed | 1 passed (9)'. Both of the card's sequences failed with 'Unable to find an element with the text: /1 row modified/' after release. The per-row { failed } pin read no /2 rows modified/. The loaded-value re-edit pin failed before release. The editor pin failed with 'expected null not to be null'. The reject-flag pin is green before and after (a guard).",
    "GREEN at 80a7e35, declared narrowing (outside the lock, --maxWorkers=1, after 3 x 540s queue-timeouts behind the objectui#12030 dev's locked multi-package runs): the 2 new pin files plus data-table-inline-edit, data-table-batch-per-row-12292, data-table-pending-by-record-12310, data-table-noop-edit-11816 and data-table-injected-editor-focus-6859 gave 'Test Files 7 passed (7)' and 'Tests 41 passed (41)', exit 0.",
    "ABLATION at 80a7e35 via node ../objectstack/scripts/ablation-replace.mjs (WRAP mode, --expect 2). Anchor 'setPendingChanges((prev) => removeSentEdits(prev, sent));', replacement 'setPendingChanges(removeSentEdits(pendingChanges, sent));' (the same removal applied to the captured copy). On disk: anchor 2 -> 0, replacement 0 -> 2, blob e30200ca773f -> 1d386008e93c. The run gave 'Tests 8 failed | 3 passed (11)', as predicted: 6 components pins and both grid pins red, while the per-row { failed } pin (the unmutated landed line), the reject pin and the editor pin stayed green. Restore: blob == HEAD e30200ca773f, and git diff HEAD is empty. There is no build leg: both files reach data-table through the vitest alias to src.",
    "FULL RUNS at HEAD b07e8ae (the final commit; each runner printed git rev-parse --short HEAD at start and end), verify lock: packages/components/ gave 'Test Files 381 passed | 1 skipped (382)' and 'Tests 3865 passed | 24 skipped (3889)', exit 0 (the skip is a pre-existing time-zone skipIf). packages/plugin-grid/ gave 'Test Files 210 passed (210)' and 'Tests 1920 passed (1920)', exit 0. The grep-derived inline-edit union (plugin-list/, 7 plugin-view ObjectView files, 5 app-shell view files) gave 'Test Files 142 passed (142)' and 'Tests 1530 passed (1530)', exit 0.",
    "TYPE: pnpm --filter @object-ui/components type-check (echoed 'tsc --noEmit && tsc -p tsconfig.test.json') exits 0 at b07e8ae. The first run, at 80a7e35, exited 2 on two test-file type errors, repaired in b07e8ae. pnpm --filter @object-ui/plugin-grid type-check exits 0, and plugin-grid is unchanged since. Built first: turbo run build --concurrency=2 --filter=@object-ui/components^... --filter=@object-ui/plugin-grid^... exits 0.",
    "LINT, a proven narrowing: eslint --no-inline-config --format json read 3 files, the diff's whole eslint population. The other 3 changed files are .md/.mdx, which no files: glob in eslint.config.js matches. data-table.tsx reads E1 W39, identical to b64fda3 (the one error is the pre-existing react-hooks/static-components). The test files read E0 W3 and E0 W4 (no-explicit-any). The config is not type-aware (no parserOptions.project/projectService), so verdicts on untouched files cannot move."
    ],
    "gates": [
    {
    "command": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"
    },
    {
    "command": "pnpm check:changeset-claims",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "No pending changeset names a file this change touches."
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "No test source names a pending changeset."
    },
    {
    "command": "pnpm check:new-line-citations",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only"
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "OK (scanned 8631 tracked text file(s))"
    },
    {
    "command": "pnpm check:test-path-roots",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "OK"
    },
    {
    "command": "pnpm check:vi-mock-specifiers",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "OK"
    },
    {
    "command": "pnpm check:handler-key-reads",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "OK, every judged read is a declared member of its arm"
    },
    {
    "command": "pnpm check:unreferenced-sources",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "OK"
    },
    {
    "command": "pnpm check:doc-fences",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "OK"
    },
    {
    "command": "pnpm check:doc-example-ids",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "415 real reference(s) all resolve"
    },
    {
    "command": "pnpm check:doc-json",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "Every judged JSON node document on the scan surface passes the strict authoring face."
    },
    {
    "command": "pnpm --filter @object-ui/components type-check",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "tsc --noEmit && tsc -p tsconfig.test.json clean"
    },
    {
    "command": "pnpm --filter @object-ui/plugin-grid type-check",
    "exit": 0,
    "head": "80a7e35 (plugin-grid unchanged after)",
    "verdict": "clean"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 packages/components/ (verify lock)",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "381 passed | 1 skipped files; 3865 passed | 24 skipped tests"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 packages/plugin-grid/ (verify lock)",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "210 files, 1920 tests passed"
    },
    {
    "command": "pnpm exec vitest run --maxWorkers=2 (inline-edit union: plugin-list/, 7 plugin-view, 5 app-shell files) (verify lock)",
    "exit": 0,
    "head": "b07e8ae",
    "verdict": "142 files, 1530 tests passed"
    },
    {
    "command": "eslint --no-inline-config --format json (3 touched ts/tsx files)",
    "exit": 1,
    "head": "b07e8ae",
    "verdict": "no new finding: data-table.tsx E1 W39 == b64fda3; the error is pre-existing"
    },
    {
    "command": "pnpm check:doc-snippets / check:doc-examples / check:readme-exports",
    "exit": null,
    "head": "b07e8ae",
    "verdict": "NOT MEASURED: each refuses without every package dist/ (whole-tree build); the doc/README edits add no fenced block"
    },
    {
    "command": "CI on PR objectui#12328",
    "exit": null,
    "head": "b07e8ae",
    "verdict": "in_progress (32 check runs: 27 in_progress, 2 success, 3 skipped at report time); Spec Main Shape Gate is advisory and red on main (objectui#12325)"
    }
    ],
    "line_budget": {
    "applies": false,
    "reason": "no skills/** or governed-surface path in the diff, and no line-ratchet ledger reads these files",
    "diff": "6 files changed, 641 insertions(+), 22 deletions(-)"
    },
    "files_changed": [
    ".changeset/12322-save-removes-only-what-it-sent.md (+14 -0)",
    "content/docs/components/complex/data-table.mdx (+7 -0)",
    "packages/components/src/tests/data-table-save-settles-current-12322.test.tsx (+291 -0)",
    "packages/components/src/renderers/complex/data-table.tsx (+119 -22)",
    "packages/plugin-grid/README.md (+3 -0)",
    "packages/plugin-grid/src/tests/inlineEditDuringSave-12322.test.tsx (+207 -0)"
    ],
    "deviations": [
    "File surface: the claim names data-table's save paths, co-located tests and a changeset. Three files fall outside it, and the run did not stop for them. (a) content/docs/components/complex/data-table.mdx (one paragraph) and packages/plugin-grid/README.md (one bullet), under objectui AGENTS.md rule #2 (docs-driven), as PR objectui#12320 did for the same table. (b) packages/plugin-grid/src/tests/inlineEditDuringSave-12322.test.tsx, a pin through the named producer (ObjectGrid's default saves). No producer code is edited. The seat may amend the claim's surface or ask for any of the three to be dropped.",
    "Beyond the triage's literal sentences, declared in the PR body. (a) The in-flight staging rule in stageCellChange, as the ruling "an edit that changed again during the save stays pending with its new value" applied to a re-edit back to the loaded value (measured red). (b) The settle closes only the press-time editor: a bounded in-place repair (same defect class, mechanical, file not otherwise claimed, same gate family; measured red). (c) saveEdit stages through an updater (unmeasured window; same line already edited).",
    "Declared narrowing: the targeted pin and neighbour run, and the ablation run, went outside the verify lock with --maxWorkers=1, after three 540s queue-timeouts behind the objectui#12030 dev's locked runs (vt-pluginsA, then vt-pluginsB). Every full-directory run, the union, the build and the type-checks went through the lock.",
    "Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude), per objectui AGENTS.md; the harness reminder's model-named trailer was not used."
    ],
    "mcp_calls": "0",
    "api_writes": "2 relay dispatches. (1) pr_create: POST /repos/objectstack-ai/objectstack/dispatches; the relay ran POST /repos/objectstack-ai/objectui/pulls (draft) and POST /repos//issues/12328/assignees. (2) This os-dev-report comment: POST /repos//issues/12322/comments via post-stamped. git push is not counted. No label write.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: objectui PR #12328 → main 72594f2 · domain:ui seat 1 · session_01CsyMRsFJBKNEMa4oPY1b5z · 2026-10-13T13:11Z

    • Merged through the merge queue at 2026-10-13T13:10Z. This card closed completed by the PR's Fixes #12322. No other lane card closed at the merge.
    • Landed by content, not by sha: all 6 of the PR's files on main 72594f2 are blob-identical to the accepted head b07e8ae (ACCEPT 6146055557).
    • One check was red by design: Spec Main Shape Gate. It is advisory, its consumer card is objectui#12325, and the gate is moving to objectstack under ruling B on decision: where and how often should the spec shape gate run (objectui compiled against objectstack main) — on every objectui PR, or on an objectstack schedule and before each release? objectstack#23238. The ACCEPT records all three conditions.
    • pm:dispatched was removed and read back. domain:ui, area:records, the type and the priority stay.
    • The changeset rides the next objectui release: @object-ui/components patch.
    • What is on main: a row save or Save All in data-table settles against the pending edits as they stand when it returns, and removes only the cells whose value is still the one it sent.
      • An edit staged during the save stays pending and drawn, whether it is on another row, on another column, or a new value for the saved cell.
      • A cell re-edited during its own save stays pending, even back to its loaded value.
      • The settle closes only the editor that was open when save was pressed.
      • Cell editing stays open throughout.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions