Repository navigation
fix(plugin-grid,types): an import undo that leaves rows unreverted says so (objectui#13015) - #13071
Conversation
…ys so The undo route answers `success: false` with one `failures` entry per row a control refused, and keeps the job undoable for those rows. The wizard never read the answer. `useImportJobUndo` now hands a partial answer to both lists that run Undo, which show how many rows stayed, each refusal sentence, and that undoing again retries only those rows. `ImportJobUndoResult` is bound to the spec's `UndoImportJobResponse` by reference instead of a hand-written copy that lacked `failures`, and `@object-ui/types` floors `@objectstack/spec` at 18.0.0-next.5, the first release that carries it. The History list keeps the undo outcome in its own slot: its reload cleared the error slot an undo wrote to, so a thrown undo there showed nothing either. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7
The wizard offers a background job only when the data source can read a job's results too, so the stub carries `getImportJobResults`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7
✅ 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
|
… arm `ImportJobUndoResult` now requires `failures`, so a custom DataSource whose `undoImportJob` resolves to the old shape no longer compiles. The changeset's BREAKING paragraph said so in prose only; the declaration line now carries it as `Clause-②: yes (narrowing)`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014MXhk1yjWh37d634hLy3p7
✅ 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: Face: the PR body and the changeset both declare ① Derived judgments(a) (b) Implementers of (c) The (d) The (e) The UI — RIGHT on every point the card names. Check-runs on the head, read 2026-10-19T19:12:13Z (36 of 36 returned): success 32 — Build & E2E, Build Docs, Bundle Analysis, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Lint, Live E2E (informational), Repo Gates, Test, Test (dist pins), Test shards 1 through 16 of 16, label; in_progress 1 — Type Check; skipped 3 — Test (coverage), Test (coverage shard), dependabot. None failed. In-progress is recorded as what it is; the queue reads the required set itself. ② Semver levelMatches what the diff publishes. ③ Boundary flagsEvery dev deviation and note on #13015 (comments 6274833740 and 6274944547), answered; neither report lists an open question.
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT · objectui#13015 · PR #13071 ·
|
Fixes #13015
Clause-②: yes (narrowing) (
ImportJobUndoResult, exported from@object-ui/types, gains the requiredfailuresmember. The newgrid.import.undoPartialcount family widens@object-ui/i18n'sTranslationKeys, andfailuresis now required of a customDataSource'sundoImportJobanswer.)What this does
The undo route answers
200withsuccess: falseand onefailuresentry per row a control refused. That shape landed in objectstack-ai/objectstack#24431, which objectui's pinf4755a68a797carries. The job stays undoable for exactly those rows. The wizard never read the answer.useImportJobUndo(packages/plugin-grid/src/ImportWizard.tsx) now takes the result ofds.undoImportJob(jobId). Onsuccess: falseit hands the answer to the list that ran Undo. A throw is handed over as before. The object's readers still getnotifyDataChanged, because a partial undo reverted the other rows.ImportUndoOutcomeNotice. It shows:failed), and that undoing again retries only those rows (grid.import.undoPartial, a count family in all ten packs);failures[].errorcarries, the app's own words. Rows with the same sentence share one line, because one hook typically refuses many rows alike (an undoable job holds up to 5,000 rows). After ten distinct sentences the rest are counted with the existinggrid.import.moreErrors.undoableand notrevertedAt. That is correct per the contract, so the button is not hidden. Undoing again clears the notice as it starts.success: trueshows nothing new and flips the row to "Undone" as before.ImportJobUndoResultis bound to the spec. It is nowUndoImportJobResponsefrom@objectstack/spec/api, by reference, the wayImportRowResultand its siblings at the top ofpackages/types/src/data.tsalready are. The hand-written copy it replaces said it mirrored the server and lackedfailures; that drift is this card's defect.Bounded in-place fix in the same hook (named here, with evidence)
In the History list, a thrown undo also showed nothing. The hook wrote the message into the panel's
errorslot, then called the reload, andload()clearserroras it starts, in the same tick. The partial notice would have been wiped the same way, so the undo outcome now has its own slot (undoOutcome), and the thrown message goes there too.c2191654a): render the History list, an undo that rejects with a sentence, wait for the reload, and assert the sentence is on screen. Result:1 failed,expect(received).toBeInTheDocument(). The probe was deleted, and the file was restored to HEAD (blob equal,git diff HEADempty).History list: an undo that throws shows its message after the list reloads.Premises measured (PM hypotheses)
c2191654athe hook ranawait ds.undoImportJob(jobId); notifyDataChanged(...)and its only error path was thecatch. It had two callers: the History list and the cancelled-result screen.f4755a68a797,packages/rest/src/data-action-routes.ts) answers a flatres.json({ success, jobId, object, deleted, restored, failed, failures }). The installed client at the pin (node_modules/.cache/objectstack/client,18.0.0-next.5) hasunwrapResponseunwrap only a body that hasdata, and itsfetchthrows only on!res.ok.@object-ui/data-objectstack'sundoImportJobreturns the client's answer unchanged. Sofailuresalready reached the wizard at runtime; only the type and the UI dropped it.check:spec-symbolsdoes not require it: the local name differs from the spec's, and the comment named no@objectstack/specsymbol. But the tree's convention (the header block ofpackages/types/src/data.ts, "Spec-owned names are bound here, not re-declared") and commandment #0 point the same way, and the hand copy is exactly what drifted. Readingfailuresthrough the spec makes@object-ui/typesthe first consumer of the release that first publishes it, so the floor moves (below).Spec floor
@object-ui/typesmoves@objectstack/specfrom^18.0.0-next.2to^18.0.0-next.5.18.0.0-next.5, the first published release carryingUndoImportJobResponseSchema.failures(objectstackadb7910f8, PR fix(rest)!: an import undo runs under the object's declared controls for every caller but the system principal objectstack#24431). The registry lists18.0.0-next.0to18.0.0-next.3and18.0.0-next.5;18.0.0-next.4was never published.git merge-base --is-ancestor adb7910f8against the@objectstack/spec@18.0.0-next.5tag commit gives exit 0. Against the…next.3tag commit it gives exit 1. The control leg (the…next.2tag commit against…next.3) gives exit 0, so the exit 1 is a real non-ancestor on that checkout.failures: z.ZodArrayhas 2 hits in next.5'sdist/api/index.d.mtsand 0 in next.2's.file:../../.objectstack-pin/spec).@object-ui/plugin-gridkeeps its own floor. It readsfailuresonly through@object-ui/types, and its artifact names noUndoImportJobResponse.check:spec-floorsover@object-ui/types: 0 findings at^18.0.0-next.5. The run also clears the twofloor-too-lowfindings@object-ui/typeshad at next.2 (checkPageMainRegionWidth,checkPageRecordBindingPlacement); next.5'sdist/ui/index.d.mtscarriescheckPageMainRegionWidth(1 hit, 0 in next.2). That is objectui#13065's subject; this PR closes no card but this one. The whole-repo run exits 1 only on 14no-artifactfindings for packages outside this branch's build closure.Tests
All runs below are at
13af833(the source has not changed since92728ce).packages/plugin-grid/src/ImportWizard.undoPartial-13015.test.tsx, which drives the real wizard with a stubbed data source: 4 tests,4 passed.success: false,failed: 3, threefailureswith two distinct sentences). It shows "3 rows were not undone. Undo again to retry only those rows.", lists both sentences once each, survives the reload, keeps Undo offered, and emits one data change.pnpm exec vitest run packages/plugin-grid/:Test Files 229 passed (229),Tests 2077 passed (2077).pnpm exec vitest run packages/i18n/ packages/types/(the ten-pack key parity, plural-category and count-family pins, plus thetypessuite):Test Files 472 passed (472),Tests 11870 passed | 13 skipped (11883). The skips are the env-gateddescribe.runIf(DRIVEN)timezone suites, the only conditional markers in those two packages.Ablations (one-off, through
ablation-replace.mjs, each restored to HEAD with blob equality and an emptygit diff HEAD)The subject resolves to
packages/plugin-grid/src(a relative import), and@object-ui/typesresolves tosrcthrough the root vitest alias. No build was needed.if (!answer.success) onOutcome({ kind: 'partial', answer });. Anchor 1 to 0. Result:3 failed | 1 passed. The three partial-answer pins went red; the throw pin stayed green.setLoading(true); setUndoOutcome(null); setError(null);), which is the base mechanism. Anchor 1 to 0. Result:3 failed | 1 passed. The three History pins went red; the cancelled-screen pin stayed green. The first attempt at this leg was refused by the tool before any test ran, because the replacement contained the anchor. It is declared here and was not counted.Reverse type check
A probe under
packages/plugin-grid/src, type-checked through the rebuiltpackages/types/dist, now refuses both old-shape literals. The old hand shape withoutfailuresgivesTS2741 Property 'failures' is missing. Anaction: 'update'givesTS2322 Type '"update"' is not assignable to type '"delete" | "restore"'. The probe was removed.Gates (local, at
13af833, each exit captured before any pipe)@object-ui/plugin-gridand@object-ui/data-objectstack, plus@object-ui/types(concurrency 2). Exit 0, 13 tasks.type-check:@object-ui/typesexit 0,@object-ui/plugin-gridexit 0,@object-ui/data-objectstackexit 0. The last is the downstream sweep: the consumers that nameImportJobUndoResult.plugin-grid'stsconfig.test.jsonprogram contains the new test file (--listFiles: 1 hit). It readspackages/types/dist/data.d.ts.@object-ui/app-shellnames onlyundoImportJob: undefined, an optional method whose type did not change. It is declared to CI.lint:@object-ui/plugin-gridexit 0 (0 errors),@object-ui/typesexit 0 (0 errors). The warnings predate this branch.pnpm check:i18n-keys0 ·check:i18n-drift0 (0 en values changed, 3 keys added) ·check:i18n-dead-keys0 (the new keys are not listed).pnpm check:spec-symbols0 ·check:spec-floors1 (whole repo;@object-ui/types0 findings, as above).pnpm check:new-line-citations0 (0 new citation(s)) ·check:control-bytes0.check-changeset-presence0 ·check-changeset-no-major0 ·check-changeset-fixed0.check-governed-queue-guard --testover the 15 changed paths:NOT GOVERNED.pnpm install --frozen-lockfile --offline0 (Lockfile is up to date): the floor move leaves the lockfile valid.Acceptance notes
content/docs/**or any package README describes the import undo today. This claim's file surface does not cover docs, so none was added; that is noted here and not filed.failuresentry also carriesid,actionandcode. The notice shows the refusal sentence the card asks for, not the opaque record id.Generated by Claude Code