Skip to content

fix(react)!: async editable never reached the checking phase - #469

Merged
blove merged 1 commit into
mainfrom
hazards-int
Aug 17, 2026
Merged

fix(react)!: async editable never reached the checking phase#469
blove merged 1 commit into
mainfrom
hazards-int

Conversation

@blove

@blove blove commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The live bug, found next to the hazard rather than in it

Two widened type copies were filed as latent tidy-ups. Narrowing the first exposed a user-visible defect.

SurfaceFacade.beginEdit declared edit?: { draft?: unknown; status?: "checking" | "editing" } and silently discarded status. createGridUiCore.beginEdit hard-coded status: "editing", and PretableIndexedEditingState.status did not contain "checking" at all.

So "checking" was unreachable in the shipped grid — while four places compared against it:

  • use-cell-edit-controller.ts:113 asks for beginEdit(addr, { status: "checking" }) on an async editable predicate
  • use-editor-field.ts's PENDING_STATUSES includes it — it drives readOnly, aria-busy, and the blur-commit gate
  • BooleanCellControl.tsx:33 disables on it
  • types.ts:195 types PretableEditorInput.status as PretableEditStatus

Consequence: with an async editable predicate, the editor opened fully interactive and would blur-commit a draft the grid had not yet agreed to accept, instead of rendering read-only and busy until the predicate answered.

The test that could not fail

use-cell-edit-controller.test.ts:87"gates begin through 'checking' for async editable" — passed throughout. It runs against the controller's own stub grid, which honours the entry status the real surface threw away. A green test over a phase the shipped grid could not enter.

That is the sixth instance of this shape found in this repo recently, and the same lesson each time: the test supplied for itself the exact step that was broken.

Reachability, checked rather than assumed

Both hazards were latent, not live, and the reasoning matters:

  • editing.statusconst facade = {…} at pretable-surface.tsx:2328 is not annotated SurfaceFacade<TRow>, so the consumption site reads the inferred type, which was already narrow. status: string only governed the 11 as unknown as SurfaceFacade<TRow> casts, none of which read .editing.
  • hidesCollapsedRowskind only ever holds PretableExpansionDefault values. But it decides CSV complete/omissions, so a rename would have flipped completeness on every export.

The rename mutation, which is the whole point

"expanded""expandedd" in csv.ts:363:

  • after this PR: TS2367: This comparison appears to be unintentional because the types '"collapsed" | "expanded" | "through-depth"' and '"expandedd"' have no overlap
  • before: compiles clean. Only a runtime test caught it.

For the editing hazard the obvious mutation errors in both states (the inferred type was already narrow), so a discriminating one was built inside a SurfaceFacade-typed helper: === "editting" now fails TS2367, and compiled clean before.

What changed

  • New public PretableOpenEditStatus — the "editing" | "validating" | "saving" | "error" union that was spelled out in four places unnamed.
  • PretableIndexedEditingState.statusPretableEditStatus; beginEdit gains status?: "checking" | "editing" (default "editing"); the facade now forwards it.
  • SurfaceFacade.getSnapshot().editing.status: stringPretableEditStatus.
  • hidesCollapsedRows takes Readonly<PretableExpansionState> instead of a structural copy.

Both unions spell their literals out rather than using Exclude<>: an Exclude whose second argument stops naming a member silently widens to the whole union instead of failing, and the docs guard can only pin a table to a union whose members the API report states outright. They are held together by a toEqualTypeOf pair that fails if either list drifts.

Test Plan

  • Pre-fix state reported for every assertion — two runtime tests failed (data-pretable-edit-status was "editing", expected "checking"), and both @ts-expect-error directives were unused (TS2578), which is how a type assertion fails.
  • Rename mutation demonstrated in both directions, output above.
  • Two CSV tests separate the predicate's two clauses (default.kind !== "expanded" vs overrideCount > 0) — the existing expand-all/collapse-all pair could not tell them apart.
  • 2385 tests, typecheck, typecheck:public, lint, prettier, build, api:check — all clean.

Found, not fixed

  1. const facade = {…} is not annotated SurfaceFacade<TRow>, and its 11 uses go through as unknown as. The interface therefore does not check the object implementing it — which is precisely how status: string survived. Making it load-bearing is a real refactor (the concrete object carries rowModel/scrollToRow the interface omits).
  2. pretable-surface.tsx:6279 asserts PretableEditorInput through as unknown as — the editor contract asserted rather than checked.
  3. apps/website/.../QtyEditor.tsx:4 re-declares the status union inline. Exact rather than widened, so it can drift but not silently widen. Left outside packages/.

🤖 Generated with Claude Code

…pping the edit entry status

Two copies of a named union had been widened to `string` and then compared
against string literals, so the compiler could not object to a rename or a
typo. Narrowing both, and fixing the live bug the first one was hiding.

THE LIVE BUG. `SurfaceFacade.beginEdit` accepted `{ status?: "checking" |
"editing" }` and threw it away: `createGridUiCore.beginEdit` hard-coded
`status: "editing"`, and `PretableIndexedEditingState` did not even include
`"checking"` in its union. So the phase `useCellEditController` opens an async
`editable` check in was unreachable in the shipped grid, and three consumers
compared against a value nothing could produce — `useEditorField`'s pending
set, `BooleanCellControl`'s disabled gate, and the blur-commit guard. The
user-visible consequence: while an async `editable` predicate was in flight the
editor was fully interactive and would blur-commit a draft the grid had not yet
agreed to accept, instead of rendering read-only and `aria-busy`.

The test that "covered" this asserted against the controller's own stub grid,
which honoured the entry status the real surface discarded. The new tests drive
`PretableSurface` itself and fail without the fix.

`"editing" | "validating" | "saving" | "error"` appeared unnamed in four places
and is now `PretableOpenEditStatus` — spelled out rather than written as
`Exclude<PretableEditStatus, "checking">`, because an `Exclude` whose second
argument stops naming a member silently widens instead of failing, and the docs
guard can only pin a table to members the API report states outright. The two
unions are held together by assertion instead of by construction.

`hidesCollapsedRows` in csv.ts took `{ default: { kind: string } }` and decided
CSV export completeness off `kind !== "expanded"`. Renaming the expansion kind
would have flipped every export's `complete`/`omissions` report with nothing
failing; it now takes `PretableExpansionState`, and the typo is a build error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove merged commit cf3c340 into main Aug 17, 2026
31 of 34 checks passed
@blove
blove deleted the hazards-int branch August 17, 2026 16:01
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pretable Ignored Ignored Aug 17, 2026 4:22pm

Request Review

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant