Skip to content

fix(app-shell): no Retry for an api-disabled attachments list - #4707

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4693-api-disabled-attachments
Aug 15, 2026
Merged

fix(app-shell): no Retry for an api-disabled attachments list#4707
yinlianghui merged 1 commit into
mainfrom
claude/issue-4693-api-disabled-attachments

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4693

What

RecordAttachmentsPanel offered a Retry for a sys_attachment list read that
failed with OBJECT_API_DISABLED (404, enable.apiEnabled: false) or
OBJECT_API_METHOD_NOT_ALLOWED (405, the operation absent from
enable.apiMethods). Both are pure functions of the object's metadata — no
user, no session, no request body — so every retry of every persona
re-fetches the identical refusal. The button was the same wrong advice
("try again") that ListView's error panel already stops giving for list
reads, one surface over (issue #4408).

The panel gains a fifth status, api-unavailable: no Retry button, honest
copy ("The attachments list is not available on this object.", new
detail.attachmentsApiUnavailable key, synced to all ten locale packs). The
pre-existing denied (authorization, issue #4269 / PR #4685) and
unavailable (network/5xx/expired-session, issue #4684) states keep their
exact meaning and affordances — this only inserts one new fork ahead of them.

Classifier home: @object-ui/react, not a new dependency edge either way

Followed the ruling's preferred half of the card's suggested shape: lifted
ListView.classifyLoadError out of packages/plugin-list/src/ListView.tsx's
module scope into a shared home, rather than growing a second,
independently-hand-rolled api-disabled check on the panel.

Checked the function itself before picking a home: it is a pure classifier —
takes an unknown error, returns a LoadErrorKind string, never renders
anything. All the i18n/copy is owned separately by each caller (ListView's
own title/message-per-kind ternaries; the panel's own single sentence per
status). So there is no list-specific text tangled into the function, and the
whole thing could move as-is — no split into "kernel vs. copy" was needed.

Landed it in @object-ui/react (packages/react/src/utils/error-message.ts,
next to the existing isPermissionError the panel already imports from
there), not @object-ui/data-objectstack or a new export from plugin-list:

  • Zero new dependency edges either direction. Both
    @object-ui/plugin-list and @object-ui/app-shell already depend on
    @object-ui/react (confirmed in both package.jsons before touching
    anything) — plugin-list already imports SchemaRenderer/
    useNavigationOverlay from it, app-shell's RecordAttachmentsPanel
    already imports isPermissionError from it. Landing in
    @object-ui/data-objectstack would have needed a brand-new dependency
    edge from plugin-list (not currently a consumer — several sibling
    plugin-* packages are, but not this one), for zero benefit over reusing
    an edge both sides already have.
  • Sibling to the function it most resembles.isPermissionError is the
    same shape of thing — classify an error into a UI-relevant verdict — and
    already lives there; RecordAttachmentsPanel was already calling it from
    the same import line I'm extending.
  • No cycle risk.@object-ui/react already depends on
    @object-ui/data-objectstack (declared in package.json, and already used
    by AppShellContext.tsx), so classifyLoadError can delegate its
    api-disabled check to the already-exported isApiAccessDeniedError
    there — removing a second, independently-maintained copy of the same
    OBJECT_API_DISABLED/OBJECT_API_METHOD_NOT_ALLOWED code list that
    ListView used to hand-roll as its own local Set.

ListView's own behavior is byte-for-byte unchanged — it now imports the
function it used to define locally, and its existing
ListView.loadErrorKind.test.tsx suite (unedited) is the pin for that.

Truth table (what I actually ran, not what the card presumed)

All four via RecordAttachmentsPanel.test.tsx, real dataSource.find()
rejections (recorded-double style — no global fetch mock, no swallowed
console error):

FailureStatusRetry?
403 PERMISSION_DENIEDdeniedno (unchanged pin)
500 / TypeError('Failed to fetch')unavailableyes (positive control — this card narrows one code pair, not the general case)
404 OBJECT_API_DISABLEDapi-unavailableno (new)
405 OBJECT_API_METHOD_NOT_ALLOWEDapi-unavailableno (new)

Plus: code-alone classification (no numeric status), no-leak assertions (no
status code / server text in the rendered sentence, matching the sibling
states' bar from issue #2532), and Upload-affordance withdrawal under the new
state. classifyLoadError itself gets a direct unit-level pin at its new home
in error-message.test.ts (api-disabled / forbidden / unauthorized /
rejected / network, plus the message-embedded-status and 403-over-400
ordering cases already pinned for ListView).

Locales

detail.attachmentsApiUnavailable added to all ten packs (en, zh, ar,
de, es, fr, ja, ko, pt, ru), following the pattern the sibling
attachmentsAccessDenied/attachmentsLoadFailed keys already use in the
same file. all-locales-key-parity, check-i18n-call-site-keys and
check-i18n-en-drift all green (output quoted below).

Changeset

One changeset, patch for all four touched publishable packages
(@object-ui/app-shell, @object-ui/i18n, @object-ui/plugin-list,
@object-ui/react) — judged by publish status (none is private), not by
how user-visible each individual package's slice looks.

Gates (all at 7e1701bbd, the commit this PR carries)

  • pnpm exec turbo run build --filter='@object-ui/app-shell...' --filter='@object-ui/plugin-list...' — 29/29 tasks successful (full dependency closure: types, core, i18n, data-objectstack, react, components, plugin-list, app-shell, …).
  • pnpm exec turbo run type-check --filter='@object-ui/app-shell' --filter='@object-ui/plugin-list' --filter='@object-ui/react' --filter='@object-ui/i18n' --filter='@object-ui/data-objectstack' — 34/34 successful.
  • pnpm exec turbo run lint --filter='@object-ui/app-shell' --filter='@object-ui/plugin-list' --filter='@object-ui/react' --filter='@object-ui/i18n' — 5/5 successful, 0 errors (pre-existing warnings only, none in the files this PR touches beyond a pre-existing react-hooks/set-state-in-effect warning on an untouched line the diff merely shifted).
  • pnpm exec vitest run packages/react/ — 593 passed (44 files).
  • pnpm exec vitest run packages/plugin-list/ — 549 passed (40 files).
  • pnpm exec vitest run packages/app-shell/src/views/__tests__/ — 106 passed (11 files).
  • pnpm exec vitest run packages/i18n/ — 804 passed (45 files).
  • node scripts/check-i18n-call-site-keys.mjs — every call-site key resolves, every inline default matches its en value.
  • node scripts/check-i18n-en-drift.mjs — 0 en values changed, 1 key added (parity-covered).
  • node scripts/check-control-bytes.mjs — OK.
  • node scripts/check-phantom-dependencies.mjs — every import declared by the package that publishes it (confirms the "zero new dependency edges" claim above).
  • node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs / check-changeset-fixed.mjs — all green.
  • node scripts/check-lint-coverage.mjs / check-type-check-coverage.mjs — all green.

Generated by Claude Code


Generated by Claude Code

RecordAttachmentsPanel offered a Retry for OBJECT_API_DISABLED (404) and
OBJECT_API_METHOD_NOT_ALLOWED (405) sys_attachment reads, both pure functions
of the object's `enable` block — no persona and no retry changes the answer.
The panel gains a fifth status, api-unavailable: no Retry, honest copy ("The
attachments list is not available on this object.", new
detail.attachmentsApiUnavailable key in all ten locale packs). denied and
unavailable keep their pre-existing meaning and affordances.
Lifts ListView.classifyLoadError out of plugin-list's module scope into
@object-ui/react (classifyLoadError, LoadErrorKind) so both surfaces consume
one classification instead of re-deriving it; ListView's own behavior is
unchanged (same function, now imported). The classifier now delegates its
api-disabled check to isApiAccessDeniedError (@object-ui/data-objectstack),
removing a second, independently-maintained copy of the same code list.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnQd8iMMUwXQEV1crFmQiQ
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 15, 2026 12:57pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-CWaHiCE9.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)489.91KB108.67KB
core (index.js)3.79KB1.52KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)163.56KB44.83KB
fields (index.js)230.37KB57.17KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)64.44KB18.28KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)121.90KB31.76KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.93KB60.01KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.72KB27.70KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)192.10KB51.24KB
plugin-kanban (index.js)52.74KB14.53KB
plugin-list (index.js)111.14KB26.92KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.38KB11.09KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review verdict: ACCEPT (reviewer of record, PM session session_01RnQd8iMMUwXQEV1crFmQiQ).

Verified against this PR, not the report: all 17 changed files account for exactly the classifier lift (3 files in @object-ui/react), ListView consuming it with its local definition and both hand-rolled code Sets deleted, the panel's fifth state plus truth-table tests, ten locale packs, and one changeset whose frontmatter covers precisely the four touched publishable packages. Body's only closing keyword is Fixes #4693. Independent checks I ran myself on main: the deleted classifyLoadError had no code references outside ListView.tsx (the two other grep hits are prose mentions in comments — see nit below), and packages/react/package.json already declares @object-ui/data-objectstack, confirming the zero-new-edge and no-cycle claims. The truth table carries both positive controls (500 keeps its Retry; 403 still reaches denied), and the api-disabled check running before the permission split is pinned by the code-alone case. Local gate union is pinned to 7e1701bbd, matching this PR's current head.

Non-blocking nit, recorded rather than actioned (out of this diff's files): packages/core/src/utils/filter-converter.ts:44 and packages/data-objectstack/src/filter-entry-translation.test.ts:165 both say "plugin-list's classifyLoadError" in prose comments — after this lift the attribution is @object-ui/react. Two words of comment drift, not worth widening the PR; whoever next touches those files can correct it.

Will flip ready + enable auto-merge once Lint, Type Check, and Test shards 1–4 conclude success on this head.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 15, 2026 13:10
@yinlianghui
yinlianghui added this pull request to the merge queueAug 15, 2026
Merged via the queue into main with commit 516663dAug 15, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4693-api-disabled-attachments branch August 15, 2026 13:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RecordAttachmentsPanel offers a Retry for an api-disabled sys_attachment, which no retry can fix

2 participants

@yinlianghui@claude