fix(list): OBJECT_API_DISABLED renders an honest cannot-work state instead of the empty state (#4408) - #4495
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…orts + find() resolve→reject on the two denial codes)
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 12, 2026 21:25
This was referenced Aug 12, 2026
yinlianghui
pushed a commit
that referenced
this pull request
Aug 13, 2026
`generateTimeScaleHeaders` is reachable from the package entry (`index.tsx` does `export * from './renderer'`) and its published declaration grew an optional trailing `locale?: string`. Entry-reachable additive API growth is minor, not patch — patch is for changes with no API-surface movement at all. `dist/index.d.ts` being byte-identical does not argue for patch: the entry re-exports by reference, so the resolved public surface moved even though the entry file's bytes did not. The contrapositive of #4496, which was graded patch precisely because its .d.ts additions were NOT re-exported from the entry; #4403 / #4177 / #4485 / #4495-regrade are the line this follows. Additive and back-compatible is what minor means — no consumer breaks, existing three-argument callers keep compiling and keep producing byte-identical output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
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.
Closes #4408
A list pointed at an object whose
enableblock withholds the API rendered its ordinary empty state, so "this page cannot work, and never could" reached the user as "you have no records". The reported instance —Setup › Advanced › Signing Keys, whosesys_jwksdeclaresenable.apiEnabled: false— could not load for any persona and said so to nobody. That is also why the upstream defect objectstack#7544 survived review for its whole life: a merely unpopulated page invites nobody to click through.Measurement first — the card's mechanism section was a declared assumption
1. Where the 404 is swallowed.
ObjectStackDataSource.find(),packages/data-objectstack/src/index.ts. Its catch ranis404Error(err)and returned{ data: [], total: 0 }, then memoised the resource inmissingResourcesso later calls short-circuited to empty without touching the network. An error state does exist —ListView'slist-error-statepanel, withclassifyLoadErrorbehind it — and this path never reached it: a resolved promise carrying zero rows is indistinguishable from a genuinely empty object. So this was a swallow, not a missing state.2. Does the ADR-0112
codesurvive the transport? YES — on the primary path.@objectstack/client's fetch wrapper stampserror.code(top-levelcode, else the nestederror.code) anderror.httpStatuson every non-2xx, andclient.data.findgoes through it. Both denial spellings are declared members of the spec'sStandardErrorCodein the vendored@objectstack/spec@17.0.0-rc.6. So no heuristic on status was needed — and none was written.But there was a real propagation gap in this repo, on the same call:
find()'s raw$expand/$searchbranch (rawFindWithPopulate) bypasses the client and hand-rolled its own error, stamping onlystatus. That made the branch a list takes whenever it expands a lookup or runs a search the one path where a denial arrived anonymous. Fixed at that producer rather than worked around downstream.3. The 405 sibling — confirmed same class, but it did NOT flow the same path.
is404Errormatches only 404, soOBJECT_API_METHOD_NOT_ALLOWEDwas never swallowed: it already reached the error panel, and was classifiednetwork— "check your connection and try again" for a condition no retry can change. Same class, two different wrong outcomes, one repair.Both denials are pure functions of the object's metadata (
apiAccessDenialFromEnable) — no user, no permission, no context — so neither is transient or per-user, which is exactly the case where a silent empty state is most misleading.The fix
@object-ui/data-objectstack—find()no longer degrades the twoenable-block denials to an empty result, and does not memoise them either (absorbing one would pin the object to "empty" for the rest of the session). Discrimination is on thecode, never the status: a missing collection, a missing record and a disabled object are all 404. New exportsisApiAccessDeniedError()/API_ACCESS_DENIED_CODES. The raw branch now carriescode+httpStatus.@object-ui/plugin-list—classifyLoadErrorgains anapi-disabledkind, checked first and on the code alone. Its panel says the object is not exposed through the API and that this is a setting on the object rather than a permission, and it offers no Retry — every retry re-fetches the identical refusal, which is the same wrong advice as "check your connection", just spelled as a control.enmirrors (the fix(i18n): the drifted detail row mirrors the en pack, and the three defaults maps get a gate (#4401) #4409 map-mirror gate is live), translated in all ten locale packs forall-locales-key-parity.plugin-listis deliberately backend-agnostic and does not depend on any adapter, so neither package's suite can see the composition. It is pinned once inapp-shell, which already depends on both.Red-first — both directions, measured
Taking both halves out (
git checkoutof the sources, nevergit stash) and re-running:Defect pin, verbatim. A real adapter over a stubbed transport answering HTTP 404 +
code: 'OBJECT_API_DISABLED', feeding a realListViewcarrying the Setup page'smanagedByempty-state override:The composition test failed pre-fix with
AssertionError: expected null not to be null— there was no error state in the DOM at all.The 405 failed pre-fix differently, as measured:
AssertionError: expected 'network' to be 'api-disabled'. It reached the panel and was called a network outage.At the adapter, pre-fix:
AssertionError: promise resolved "{ data: [], total: +0 }" instead of rejecting(8 failed / 7 passed of 15).Controls — green on BOTH sides, never red:
OBJECT_NOT_FOUNDstill degrades to empty and is still memoisedRECORD_NOT_FOUNDis not an enable-block denialforbidden, 401unauthorized, 400rejectedThe card's binding control holds: a genuinely empty object still renders the ordinary empty state. Empty is the overwhelmingly common case and the one that state exists for.
Verification
pnpm exec vitest run packages/data-objectstack/ packages/plugin-list/ packages/i18n/→ 110 files, 1779 tests passedpnpm exec vitest run packages/app-shell/→ 361 files, 3466 passed, 1 skipped (hosts the fix(i18n): the drifted detail row mirrors the en pack, and the three defaults maps get a gate (#4401) #4409 map-mirror gate and the composition pin)type-check(bothtsccommands where the package defines them) fordata-objectstack,plugin-list,i18n,app-shell→ all exit 0, after building each one's dependency closure firstcheck:control-bytes,check:i18n-keys,check:i18n-drift,changeset:check,check-changeset-presence→ all greeneslintover the touched trees → 0 errorsNote for review
One assertion of mine was wrong on the first run and was corrected rather than the copy: I had asserted the panel text contains no
/permission/iat all, but the copy deliberately says "not a permission" — that disclaimer is the honest distinction between this state and the 403 panel. The test now asserts the disclaimer and the absence of the forbidden panel's own wording.Generated by Claude Code