Skip to content

fix(plugin-charts): route ObjectChart's option-color probe through the host apiFetch - #4122

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4114-objectchart-apifetch
Aug 10, 2026
Merged

fix(plugin-charts): route ObjectChart's option-color probe through the host apiFetch#4122
yinlianghui merged 2 commits into
mainfrom
claude/issue-4114-objectchart-apifetch

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4114

Branched off origin/main @ ce4b9e8af — PR #4115 (issue #4106) had already merged when this started, so this is not stacked; its four test files and their recording doubles are on main.

Premise: verified, still valid

Both bare reads are exactly where the issue said, at the unchanged line numbers on ce4b9e8af:

344: const defRes = await fetch(`/api/v1/meta/dataset/${encodeURIComponent(schema.dataset)}`, reqOpts);
352: const schemaRes = await fetch(`/api/v1/meta/object/${encodeURIComponent(objectName)}`, reqOpts);

and the injection point is genuinely in scope — const context = useContext(SchemaRendererContext); at line 253, already used one line later for dataSource. Nothing had drifted.

The fix

const doFetch = apiFetch ?? fetch; at the top of the effect, both call sites through it — the idiom useRecordEditable.ts:74 implements and useRecordEditable.ts:32 documents ("the same channel provider: 'api' view sources use, not the bare global one"), matching ObjectGantt.tsx:461-468 and useViewData.ts:100-107.

Two details taken from the references rather than invented:

  • The context is read directly, not via useSchemaContext() — that hook throws with no provider mounted, and a standalone chart embed has to keep rendering. Same reason useRecordEditable gives at its own context read.
  • apiFetch joins the effect's dependency array, as it does in useRecordEditable's. This does not re-open the refetch-loop hazard the surrounding code guards against: ObjectChart's own setState cannot re-render the provider, so the context value — and with it the apiFetch identity — is stable across the effect's own updates.

The sole in-repo host holds it at module scope (ConsoleShell.tsx:187:245), so the identity is stable there too.

Behavior boundary — the fallback half is pinned, not assumed

Standalone embeds keep working on the global fetch, and there are two distinct no-channel shapes, both now pinned:

  1. no provider at all;
  2. a provider that supplies dataSource but no apiFetch — the shape every existing suite mounts, e.g. ObjectChart.elementDataSource.test.tsx. Presence of a host is not presence of a channel.

Tests

Four pins added to ObjectChart.optionColors.test.tsx (the file #4115 created for this effect), in a new objectui#4114 block. The routing is read off two separate recorders — the global-fetch stub #4115 already had, plus a standalone recorder handed in as the provider's apiFetch:

  • object-bound chart under a host → the probe is in the host recorder, the global recorder is empty, the request init survives the hop (accept: application/json), and the routed answer reaches the renderer as categoryColors;
  • dataset-bound chart under a host → both hops (:344 then :352, in order) are in the host recorder, global empty, and the dimension labels resolved over the host channel reach the rows;
  • no provider → global recorder holds the probe;
  • provider without apiFetch → global recorder holds the probe.

Both recorders answer the same documents, so the data path stays green either way and the only thing these pins can go red on is the routing itself.

No existing test was changed in meaning. The plugin-charts diff is additive apart from one import line; the four #4115 files are untouched. That follows from the behavior boundary above — every existing mount is a no-apiFetch shape, so the routing change genuinely moves nothing for them. (The second commit only deletes three as any casts from the new pins: SchemaRendererProvider.dataSource is already typed any, and the recorder's fake Response is cast once inside its helper.)

Verification

Repo root, both packages, per AGENTS.md:

pnpm exec vitest run packages/plugin-charts/ packages/plugin-dashboard/ --maxWorkers=2
Test Files 54 passed (54)
Tests 446 passed (446) # 442 on main + 4 new
$ wc -l stderr => 0
$ grep -c ECONNREFUSED stderr => 0

stderr stays entirely empty — #4115's no-escape guarantee survives the routing change.

Reverse verification, direction predicted before running: revert only the product routing (git checkout origin/main -- ObjectChart.tsx, new tests held), and the two host-mounted pins go red while the two fallback pins and all three #4106 pins stay green — because with no channel supplied, pre-fix and post-fix code do the identical thing.

× rides the host apiFetch when a provider supplies one, and never the global fetch
× routes BOTH dataset hops through the host apiFetch
AssertionError: expected [] to deeply equal [ '/api/v1/meta/object/opportunity' ]
AssertionError: expected [] to have a length of 2 but got +0
Tests 2 failed | 5 passed (7)

Exactly as predicted, 2 red / 5 green, and both failures name the host recorder holding nothing — the escape landing in the global recorder instead. Restored afterwards; git diff clean.

Gates: pnpm --filter @object-ui/plugin-charts type-check green (after pnpm --filter '@object-ui/plugin-charts^...' build, the fresh-worktree dependency closure). eslint on both changed files 0 errors; the test file is at 2 warnings, both pre-existing #4115 lines — the new tests add zero. check-changeset-presence / check-changeset-no-major / check-control-bytes green, plus a control-byte self-scan over the three changed files.

Changeset

@object-ui/plugin-chartspatch — a real behavior fix on a released package, not internal-only, so an empty-frontmatter declaration would be wrong here.

Out of scope, filed

#4121plugin-dashboard's DatasetWidget.tsx:589 reads the same /api/v1/meta/object/… document off the global fetch, for the same colors and labels, with the same silent symptom. #4114's body anticipated this twin. Not folded in: different package, different component, and it needs its own decision about where the context read goes (DatasetWidget does not consult SchemaRendererContext at all today).


Generated by Claude Code

…e host apiFetch (#4114)
ObjectChart's category option-color / dimension-label effect made both of its
metadata reads — GET /api/v1/meta/dataset/<dataset> (:344) and
GET /api/v1/meta/object/<object> (:352) — on the bare global fetch, so a hosted
console's authenticated channel (SchemaRendererContext.apiFetch, supplied at
ConsoleShell.tsx:245) was bypassed: no Authorization/tenant headers, no base-URL
rewrite, no draft-preview params. The effect is best-effort and swallows every
failure, so the symptom was silent — semantic option colors and dataset dimension
labels never applied and the chart fell back to the positional theme palette and
raw stored values.
Both reads now go through `apiFetch ?? fetch`, the documented convention
(useRecordEditable.ts:32, ObjectGantt.tsx:461-468, useViewData.ts:100-107).
Standalone embeds keep working: no provider, or a provider that supplies no
apiFetch, still uses the global fetch.
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…routing pins
`SchemaRendererProvider.dataSource` is already typed `any`, and the recorder's
fake Response can be cast once inside the helper as `ApiFetch` — so the four new
tests add zero `no-explicit-any` warnings.
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 10, 2026 11:12am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-Cy4r-2tR.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
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)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
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)4.91KB0.87KB
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)485.06KB107.21KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)140.66KB36.25KB
fields (index.js)229.40KB56.93KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.87KB10.80KB
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.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.50KB30.66KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)237.80KB59.48KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)162.81KB39.67KB
plugin-grid (index.js)188.04KB49.91KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.04KB26.67KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
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)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
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 (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)2.71KB1.34KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 11:26
@yinlianghui
yinlianghui added this pull request to the merge queueAug 10, 2026
Merged via the queue into main with commit bcd3e02Aug 10, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4114-objectchart-apifetch branch August 10, 2026 11:26
github-merge-queueBot pushed a commit that referenced this pull request Aug 10, 2026
…gh the host apiFetch (#4134)
The widget's `GET /api/v1/meta/object/{object}` read — which feeds
`buildOptionColorMap` (per-category colors for the first dimension) and
`buildDimensionLabelMap` (the value -> label maps for the axis/series) — went
out on the bare global `fetch`, bypassing whatever the host supplies on
`SchemaRendererContext.apiFetch`. The effect is best-effort and swallows every
failure, so a hosted console got no error at all: semantic colors and option
labels simply never applied.
DatasetWidget takes its `dataSource` as a prop and read no context, so the
channel is added here as a direct `useContext(SchemaRendererContext)` read (not
`useSchemaContext()`, which throws with no provider mounted) and used as
`apiFetch ?? fetch`, keeping the global fallback for standalone embeds.
The plugin-dashboard twin of #4114 / PR #4122.
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Co-authored-by: Claude <noreply@anthropic.com>
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.

ObjectChart's option-color / dimension-label probe reads the global fetch, bypassing the host's apiFetch channel

2 participants

@yinlianghui@claude