fix(plugin-grid): a cancelled background import shows the rows the job committed, with Undo (objectui#11650) - #11657
Conversation
…b committed, with Undo (objectui#11650) After the user cancels an async import job, the wizard reads the job back (getImportJobProgress, the GET /api/v1/data/import/jobs/:id read) until its outcome is final, then shows the committed created/updated counts and an Undo button through the History list's own undo action. A cancelled read counts as final once it is undoable or repeats the previous cancelled read's counts: the server marks the row cancelled before its worker stops writing. The poll loop shares the same result builder and settle rule, and a run token keeps the poll loop and the cancel handler from both publishing or calling onComplete twice. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…objectui#11650) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…t refetches (objectui#11650) The cancel now hands its result to onComplete, so the host's list shows the committed rows; an Undo then deleted them while the list kept showing them. The shared undo action (History list and cancelled result) now calls notifyDataChanged for the object after a successful undo, the bus every write path is meant to use. A failed undo announces nothing. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…ete (objectui#11650) The run token dropped any outcome that landed after reset(), which also dropped the poll loop's completion of a background job the user had closed the dialog on: the host no longer refreshed or toasted when it finished. Only a newer run now moves the token; closing the wizard does not. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL 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
|
ACCEPT: PR objectui#11657, head
|
Fixes #11650
Clause-②: no
Session:
session_015W8GBu6sBiqus2L2xjMsAL(dispatched by PM seatdomain:ui#1, claim comment 5992312538).What changed
packages/plugin-grid/src/ImportWizard.tsx, one file of source.handleCancelImportused to write a result withimportedRows: 0right aftercancelImportJob. It now reads the job through the data source'sgetImportJobProgress, which is theGET /api/v1/data/import/jobs/:idread the card names: the@objectstack/clientdata.getImportJobProgressfetches{data route}/import/jobs/{id}. The wizard reads until the outcome is final, at mostIMPORT_JOB_CANCEL_SETTLE_READS(10) reads, the first at once and then one per poll interval (800 ms). It then shows the job's created and updated counts. No hand-rolled fetch, and no change to the cancel door or to the client'scancelImportJobanswer.cancelledJobToImportResultis the poll loop's oldcancelledresult, moved out of the loop.isSettledJobReaddecides when a read is final, andpublishJobOutcomepublishes it. The poll loop and the cancel handler share all three.isImportJobUndoable), the result shows an Undo import button. It runs the History list's own undo action:handleUndomoved into a module-privateuseImportJobUndothat both now use. After the undo the button gives way to "Undone".jobRunRef) lets only the first publisher for a run publish. The poll loop also returns when a Cancel landed while its read was in flight. So the two paths never both set a result or callonCompletetwice.notifyDataChanged({ objectName }). That is the existing data-invalidation bus from@object-ui/react, whichObjectGridalready listens to. This is an in-place fix declared below.Strings reused, none added:
grid.import.importCancelled,grid.import.createdCount,grid.import.updatedCount,grid.import.undoImport,grid.import.undoing,grid.import.undoConfirm,grid.import.reverted. No export, prop, type member or i18n key is added to any@object-ui/*package.Why the first read after a cancel is not always final
In
@objectstack/rest's import-job routes, the cancel route writesstatus: 'cancelled'andcompleted_atto the job row itself, then answers. The worker keeps writing to its next progress boundary, 200 rows. Only then does it patch the final counts, the per-row results and, for a job it can undo, the undo log. Until that last write the job reads terminal, but its counts can still grow andundoableis still false. SoisSettledJobReadtakes acancelledread as final only when it is undoable, or when it repeats the previouscancelledread's counts.succeededandfailedare written together with their final counts, so they stay final on first sight. The poll loop uses the same rule, so a job cancelled elsewhere, for example from the History list in another tab, is no longer shown short either.If the bound is hit (no read settles within 10 reads, or every read fails), the result says "Import cancelled" and shows no count, because the wizard never read one. The old "0 imported" badge is gone from that path.
onCompletedoes not fire there, as before.Live before / after (own backend, showcase
showcase_contact)Backend: objectstack
mainat2df3d13d, built in my own worktree,objectstack dev --seed-admin --freshon a private port. Console:apps/consolevite from each objectui worktree, proxied to that backend. A Playwright run uploads a 5000-row CSV, ticks background import, clicks Run, then Cancel after 2.5 s, and then reads the job through the API.f1a177cImport cancelled · 0 imported, no Undocancelled, 1800 created,undoable: trued0bfaa4Import cancelled · 1800 created · Undo importcancelled, 1800 created,undoable: true02d9266Import cancelled · 2000 created · Undo importcancelled, 2000 created,undoable: trueOn the after and final runs, clicking Undo showed "Undone" and the job then read
revertedAtset. A filtered contact read for that run's rows returned 0. The same filter returned 1800 for the before run's rows, which nobody undid. On the final run, one list read (GET .../data/showcase_contact) went out after the Undo click: that is the list refetch.Direct API measurement against the same backend: six cancels of a 5000-row job, each after a 1.0 to 4.0 s delay. The cancel POST answered 5.8 to 9.4 s after it was sent. In the four sequential runs, the first read sent after the cancel answered was already
cancelledwithundoable: trueand the final counts. In the two runs with a concurrent poller (one read every 50 ms plus the request time), no read ever showed acancelledjob that was not yet undoable. That was on this driver (SQLite). The window the settle rule guards is therefore a reading of the server source, not a live observation. The rule costs nothing when the first read is already undoable.Tests
Head
02d9266(after the one merge oforigin/mainat4367146):pnpm exec vitest run packages/plugin-grid/:Test Files 186 passed (186),Tests 1721 passed (1721).vitest list --filesOnlylists 186 files, the new file among them in thedomproject.pnpm --filter @object-ui/plugin-grid type-checkexits 0. It runstsc --noEmit && tsc -p tsconfig.test.json, and--listFilesOnlyshows the test project compiles the new test file.pnpm --filter @object-ui/plugin-grid lint:0 errors. Per file, ESLint JSON reportsImportWizard.tsxwith 10 warnings, the same 10 rules as on base, and the new test file with 0.check:i18n-keys,check:i18n-dead-keys,check:new-line-citations,check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:handler-key-reads,check:unreferenced-sources,check:shell-escape-residue,check:vi-mock-specifiers,check:phantom-deps,scripts/check-changeset-no-major.mjsandscripts/check-changeset-presence.mjs.New pins, in
ImportWizard.cancelCounts-11650.test.tsx(10 tests, wizard driven end to end with real timers):onCompleteis called once;running, and a later read settles;cancelledreads agree, with no Undo;cancelledread;onComplete(a behaviour on base that this change keeps);onComplete.Reverse verification, one-off and committed first.
ImportWizard.tsxwas swapped for basef1a177cunder a restore trap, with blob hashes checked: 9 of 10 red, and the close-mid-run pin green, as a must-keep pin should be. Restore was proven by blob equals HEAD and an emptygit diff HEAD.Ablations, each through
ablation-replace.mjswith the landing and the restore proven:notifyDataChangedcall removed: exactly the 2 bus pins red;Not run locally, left to CI: the full
pnpm testfarm, the repo-widepnpm lint, andcheck:eager-closure. Consumers inapp-shellwere not re-tested: the public face of@object-ui/plugin-gridis byte-unchanged (no export, prop or type change).In-place fix, declared
Undo now calls
notifyDataChangedfor the object (the History list and the new result button share it). This round makes the cancel callonComplete, so the console list refetches and shows the committed rows. An Undo that left the list showing those rows after deleting them would be a stale state this change makes newly reachable. The bus's own contract asks for exactly this call: its doc comment says "Call this from every WRITE path after the mutation succeeds". The fix is one line, in the claimed file, inside the same gate family. The pins and the ablation are above.Behaviour changes for hosts
onCompletenow fires once after a user Cancel whose job reads back, withcancelled: trueand the committed counts. In the console this refreshes the list and raises the usual import toast ("Imported 1800 rows."). Before, it fired for a cancelled job only when the poll loop itself saw it end.Acceptance notes (observations, not filed)
yieldToEventLoopdoc names framework#2824). These are latency readings, not a wrong answer. Carrier: none.ImportHistoryPanel.loadsets its error tonullat the start, and the undo's reload runs right after a failure. So a failed History undo shows its message only until the reload begins. This change keeps that behaviour: the refactor kept it byte-equivalent, and the new result-screen Undo keeps its own error. This was read from the source and not measured live. Carrier: none.Generated by Claude Code