fix(react)!: async editable never reached the checking phase - #469
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.beginEditdeclarededit?: { draft?: unknown; status?: "checking" | "editing" }and silently discardedstatus.createGridUiCore.beginEdithard-codedstatus: "editing", andPretableIndexedEditingState.statusdid not contain"checking"at all.So
"checking"was unreachable in the shipped grid — while four places compared against it:use-cell-edit-controller.ts:113asks forbeginEdit(addr, { status: "checking" })on an asynceditablepredicateuse-editor-field.ts'sPENDING_STATUSESincludes it — it drivesreadOnly,aria-busy, and the blur-commit gateBooleanCellControl.tsx:33disables on ittypes.ts:195typesPretableEditorInput.statusasPretableEditStatusConsequence: with an async
editablepredicate, 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.status—const facade = {…}atpretable-surface.tsx:2328is not annotatedSurfaceFacade<TRow>, so the consumption site reads the inferred type, which was already narrow.status: stringonly governed the 11as unknown as SurfaceFacade<TRow>casts, none of which read.editing.hidesCollapsedRows—kindonly ever holdsPretableExpansionDefaultvalues. But it decides CSVcomplete/omissions, so a rename would have flipped completeness on every export.The rename mutation, which is the whole point
"expanded"→"expandedd"incsv.ts:363:TS2367: This comparison appears to be unintentional because the types '"collapsed" | "expanded" | "through-depth"' and '"expandedd"' have no overlapFor 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 failsTS2367, and compiled clean before.What changed
PretableOpenEditStatus— the"editing" | "validating" | "saving" | "error"union that was spelled out in four places unnamed.PretableIndexedEditingState.status→PretableEditStatus;beginEditgainsstatus?: "checking" | "editing"(default"editing"); the facade now forwards it.SurfaceFacade.getSnapshot().editing.status:string→PretableEditStatus.hidesCollapsedRowstakesReadonly<PretableExpansionState>instead of a structural copy.Both unions spell their literals out rather than using
Exclude<>: anExcludewhose 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 atoEqualTypeOfpair that fails if either list drifts.Test Plan
data-pretable-edit-statuswas"editing", expected"checking"), and both@ts-expect-errordirectives were unused (TS2578), which is how a type assertion fails.default.kind !== "expanded"vsoverrideCount > 0) — the existing expand-all/collapse-all pair could not tell them apart.typecheck:public, lint, prettier, build,api:check— all clean.Found, not fixed
const facade = {…}is not annotatedSurfaceFacade<TRow>, and its 11 uses go throughas unknown as. The interface therefore does not check the object implementing it — which is precisely howstatus: stringsurvived. Making it load-bearing is a real refactor (the concrete object carriesrowModel/scrollToRowthe interface omits).pretable-surface.tsx:6279assertsPretableEditorInputthroughas unknown as— the editor contract asserted rather than checked.apps/website/.../QtyEditor.tsx:4re-declares the status union inline. Exact rather than widened, so it can drift but not silently widen. Left outsidepackages/.🤖 Generated with Claude Code