Skip to content

fix(plugin-dashboard): GridLayout shows the retired-format placeholder instead of a silent blank chart (#4612) - #4613

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4612-gridlayout-legacy-fallback
Aug 14, 2026
Merged

fix(plugin-dashboard): GridLayout shows the retired-format placeholder instead of a silent blank chart (#4612)#4613
yinlianghui merged 1 commit into
mainfrom
claude/issue-4612-gridlayout-legacy-fallback

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4612

The defect

framework#3320 retired the pre-ADR-0021 inline-analytics widget shape (top-level object + categoryField / valueField / aggregate, pivot rowField / columnField) and shipped a graceful fallback for the stored metadata that still carries it — a visible tile reading "This widget uses a retired data format. Edit it to bind a dataset."

That fallback was applied to DashboardRenderer and to nothing else. DashboardGridLayout had no sentinel at all, so the identical stored widget fell through to its static-data branch with data: []. Same metadata, same product, two outcomes: a rebind prompt on one surface, a silent blank chart on the other — worse for the author than the pre-retirement state, because it carries no chart, no diagnostic and no path to fix. That is verbatim the outcome DashboardRenderer.legacyRetired.test.tsx's own header says must not happen.

One correction to the issue's blast radius. The card says DashboardGridLayout is "used by DashboardWithConfig". Measured on origin/main, it is not — DashboardWithConfig.tsx:210 renders DashboardRenderer, and repo-wide there is no in-repo importer of DashboardGridLayout outside its own package. The real exposure is the two published surfaces: the named export from the package entry, and the dashboard-grid SDUI component type registered as "Dashboard Grid (Editable)" (index.tsx:271-289), which app-shell's own widget sweep renders. The defect and its severity are unchanged; only the named consumer was wrong.

The design — one detector, two consumers

New internal module packages/plugin-dashboard/src/legacyRetiredWidget.ts holds LEGACY_RETIRED_WIDGET_SCHEMA (moved verbatim out of DashboardRenderer) and isLegacyRetiredWidget(). Both surfaces import it; neither restates it. The condition is not hand-copied, because one private copy is precisely why the defect existed — two surfaces and one fix.

The predicate is true when all three hold, in this order: no dataset; no renderer-internal data (widget.data or options.data, || not ??, preserving the original byte for byte); and a top-level object.

DashboardRenderer's observable behaviour is unchanged — its existing legacyRetired suite is untouched and green on both sides of the change.

The one refinement, and why it is inert on the existing surface

The shared predicate states the dataset guard explicitly, which the inline condition did not. On DashboardRenderer that costs nothing observable: datasetBound picks DatasetWidget at the render site regardless of what getComponentSchema returned, so the placeholder was already unreachable for a dataset-bound widget. Verified with a throwaway probe run on both sides of the revert (a widget carrying both dataset and a stale top-level object renders no placeholder, before and after). Naming the condition keeps the predicate true on its own terms for any surface that has no such render fork — which is exactly DashboardGridLayout.

Why the pivot family arm was NOT shared

DashboardRenderer.tsx:712 returns the placeholder for the entire pivot family, unconditionally. Mirroring that arm onto the grid would have been a faithful copy and a regression: it is a statement about what DashboardRenderer can draw — that surface emits no pivot block at all — not about the widget being legacy. DashboardGridLayout does draw pivots, from static data and from the provider: 'object' config, and the ruling's must-not-change note is binding on both surfaces. So the arm stays surface-local (with a comment saying why), and the legacy pivot shape is caught on both surfaces by the shared sentinel via the top-level object it carries — which is what the card's pivot positive case actually exercises.

Red-first — predictions written before the run

Recorded before the pre-fix run, then measured. Positives assert the placeholder absent pre-fix, present post-fix; negatives assert absent on both sides.

# widget predicted pre-fix branch pre-fix post-fix
P1 {type:'bar', object:'invoices', categoryField:'month', valueField:'amount', aggregate:'sum'} series → static → {type:'chart', data: []} RED green
P2 {id:'invoices_by_status', title:'Invoices by Status', type:'bar', object:'invoices', categoryField:'status', aggregate:'count'}byte-for-byte examples/schema-catalog/src/schemas/plugin-dashboard/filtered-dashboard.json widgets[0] same as P1 RED green
P3 {type:'pivot', object:'invoices', rowField:'region', valueField:'amount'} pivot → {type:'pivot', data: []} RED green
P4 {type:'metric', object:'invoices', aggregate:'count'} — same entry's widgets[2] metric → {type:'metric', value:'—'} RED green
P5 verbatim-wording pin on the message placeholder absent RED green
N1 {type:'bar', dataset:'invoices', values:['count']} dataset widget green green
N2 {type:'bar', options:{data:{provider:'object', object:'invoices', aggregate:{…}}}} provider config — a DIFFERENT, LIVE surface green green
N3 {type:'bar', options:{data:[{name:'A', value:1}]}} static-data widget green green
N4 {type:'pivot', options:{data:[…]}} static-data pivot green green

Pre-fix run, verbatim:

❯ packages/plugin-dashboard/src/__tests__/DashboardGridLayout.legacyRetired.test.tsx (9 tests | 5 failed)
  × renders the visible placeholder for a legacy chart widget
  × renders the visible placeholder for a legacy catalog bar widget
  × renders the visible placeholder for a legacy pivot widget
  × renders the visible placeholder for a legacy metric widget
  × states the rebind affordance verbatim, not merely "something is wrong"
 Test Files  1 failed | 1 passed (2)
      Tests  5 failed | 10 passed (15)

The failure output confirms the predicted branch was taken, not merely that text was missing — the rendered node dumped {"type": "chart", "chartType": "bar", "data": [], …}: the silent blank chart itself.

Post-fix, same command: Test Files 2 passed (2) / Tests 15 passed (15).

Reverse verification

git diff to a patch file, git checkout origin/main -- on both consumers, re-run, git apply to restore, sha256sum -c on all four touched files (4/4 OK). No git stash at any point — the stash stack is shared across worktrees.

Reverted (both consumers at origin/main, new module and new suite present):

 ❯ DashboardGridLayout.legacyRetired.test.tsx (9 tests | 5 failed)
 Test Files  1 failed | 2 passed (3)
      Tests  5 failed | 11 passed (16)

The 5 reds are exactly the 5 positives; the 4 negatives, DashboardRenderer's 6, and the dataset-guard probe stay green. The predicted direction is the ordinary one — new pins go red when the fix is removed — and it held.

Must-not-change, proved on both live surfaces

  • options.data provider config — N2, plus DashboardRenderer.legacyRetired.test.tsx's own control. The nested object is read off widgetData, never off the widget top level; the detector requires the absence of any widget-level data before it looks at object, so the two cannot be conflated.
  • Dataset widgets — N1 on the grid, the existing control on the renderer, plus the two-sided probe above.
  • Static-data widgets — N3, and N4 for the static-data pivot the shared arm would have swallowed.
  • DashboardRenderer as a whole — its legacyRetired suite untouched, 6/6 green before and after.

Verification

  • Build closure (dependencies + the package): green.
  • Tests, repo-root vitest, package + consumption radius (packages/plugin-dashboard, app-shell/widget-dom-leak-sweep which renders plugin-dashboard:dashboard-grid, components/inline-locale-label-read-sites): 55 files / 470 tests passed.
  • Type-check sweep, DOWNSTREAM directionturbo run type-check --filter='...@object-ui/plugin-dashboard' (leading dots = the package's consumers): 41 successful, 41 total.
  • .d.ts, dist and tsbuildinfo cleared, rebuilt: byte-identical, sha256 172d66f9… both sides. The shared module is internal-only, not re-exported from the package entry.
  • ESLint, measured both ways: origin/main baseline 328 problems (0 errors, 328 warnings), this branch 327 (0 errors, 327 warnings) — net −1, the as any the extraction removed from DashboardRenderer.
  • Gates: check-control-bytes, check-phantom-dependencies, check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-type-check-coverage, check-lint-coverage, check-doc-links8/8 PASS. Control-byte self-scan over all touched files incl. untracked: clean.

Grading

minor, one changeset. By measurement the published type surface is unchanged (byte-identical .d.ts, internal-only module) — that half is patch. The grade is set by the behaviour move on a published component: DashboardGridLayout / dashboard-grid now renders a placeholder where it rendered a blank, which is the behaviour-move precedent (#4495 / #4271 / #4479). Never major.

Scope

packages/plugin-dashboard/src/** plus one changeset, exactly. Untouched, per the mutual exclusion with the #4600-(d1) seat: examples/schema-catalog/**, content/docs/**, packages/plugin-dashboard/README.md, apps/site/**. content/docs/releases/** never.


Generated by Claude Code

…r instead of a silent blank chart (#4612)

framework#3320 retired the pre-ADR-0021 inline-analytics widget shape and gave
DashboardRenderer a graceful fallback: a visible "This widget uses a retired data
format. Edit it to bind a dataset." tile for the stored metadata that still
carries it. DashboardGridLayout — separately exported, registered as the
`dashboard-grid` SDUI component — had no such sentinel, so the identical widget
fell through to its static-data branch with `data: []`: a silent blank chart with
no diagnostic and no path to fix, the exact outcome the retirement's own test
header says must not happen.

The detector and the placeholder now live in one module (`legacyRetiredWidget.ts`)
consumed by both surfaces, because one private copy is why the defect existed.
DashboardRenderer's observable behaviour is unchanged. The nested
`options.data = { provider: 'object', … }` config, dataset widgets and static-data
widgets are untouched on both surfaces, pinned by four negative controls.

Fixes #4612

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 14, 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)
objectui Ignored Ignored Aug 14, 2026 12:55am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-Dd2P5NKg.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.48KB 108.55KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.23KB 31.62KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.72KB 27.70KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 190.02KB 50.48KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 112.01KB 27.27KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Projects

None yet

2 participants