Skip to content

fix(plugin-dashboard): stop authoring the retired widget-level actionUrl key (objectstack#7129) - #4058

Merged
os-help merged 1 commit into
mainfrom
claude/issue-7129-widget-actionurl-producer
Aug 10, 2026
Merged

fix(plugin-dashboard): stop authoring the retired widget-level actionUrl key (objectstack#7129)#4058
os-help merged 1 commit into
mainfrom
claude/issue-7129-widget-actionurl-producer

Conversation

@os-help

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#7129

What was wrong

actionUrl / actionType / actionIcon were retired at the widget level in @objectstack/spec 17.0.0-rc.3 (objectstack#5010, ADR-0049 D2). They are retiredKey tombstones: DashboardWidgetSchema types them never and refuses any value, so authoring one is a tsc error and a parse error.

plugin-dashboard was still producing the widget-level key. All verified on objectui origin/main @ 11c1e71e, not a working tree:

  1. WidgetConfigPanel.tsx:391 — a Behavior-group authoring field labelled "Click-through URL", bound to actionUrl.
  2. DashboardWithConfig.tsx:114actionUrl: widget.actionUrl ?? '' seeded into every widget config handed to the panel.
  3. WidgetConfigPanel.tsxsanitizeDraftForType — the only scrub between panel draft and persistence, and it deletes LEGACY_ANALYTICS_KEYS (dataset-shape keys) only.

The defect is wider than the issue body measured

The issue and its triage thread frame this as "the panel offers a field that authors a tombstoned key". Measured here, the seed at site 2 makes it unconditional: because selectedWidgetConfig always sets actionUrl (to '' when absent), useConfigDraft starts from it and the scrub does not remove it, so every save from the widget panel emitted actionUrl: '' — including a save where the author merely renamed a widget and never opened the Behavior group. The reverse-verification run below shows exactly that: actionUrl in the persisted payload of a test that touches only the title field.

The field was inert in the other direction too

Zero-hit result, falsified as the card requires: no dashboard widget renderer reads widget.actionUrl anywhere in objectui. Falsifier — colorVariant, the sibling key that travels the identical seed to panel-field to draft to scrub path — resolves to a real consumer chain (DashboardWithConfig.tsx:113, WidgetConfigPanel.tsx:407, colorVariants.ts, DatasetWidget.tsx:722), so the sweep does find consumers when they exist. Also checked for the blind spots named on the card: no bracket or dynamic access anywhere in packages/apps/examples/e2e, and no cross-line concatenation candidates. DashboardRenderer's 14 actionUrl occurrences remain header.actions[]-scoped, so the ledger's claim about the renderer half still holds.

So the control was a lying control twice over: the URL an author typed was never navigated to, and the value was refused by the spec on the way in.

What changed

  • WidgetConfigPanel.tsx — the Behavior section is removed (it held exactly this one field), replaced by a comment recording why it must not come back. sanitizeDraftForType now also scrubs all three retired keys, as defence in depth for stored widgets that already carry them and for hosts driving WidgetConfigPanel directly.
  • DashboardWithConfig.tsx — the actionUrl seed is removed.
  • packages/types/src/complex.ts — the DashboardWidgetSchema docblock listed the three keys among those that "flow in from the spec" alongside live keys like colorVariant. They do flow in, as ?: never. The prose now says so, and records the asymmetry that let these producers survive the 2026-08-04 renderer-side sweep: authoring one is a tsc error, but reading one still type-checks as never | undefined.

I took the removal route rather than only widening the scrub, per the dispatch's preference: a UI field whose value is silently scrubbed on save is exactly the lenient-consumer shape this campaign removes. I found no live non-tombstoned consumer that would argue for keeping the field.

actionType / actionIcon were checked separately, as the card asked: neither is authored by the panel nor read by DashboardWithConfig, so no field removal was invented for them. They are covered by the scrub and by the new pin only.

Verification

All commands run from the repo root (the repo's vitest guard rejects the per-package form).

Executed the spec parse rather than reading itDashboardWidgetSchema.safeParse against the real @objectstack/spec 17.0.0-rc.5:

PASS baseline (no action keys)
FAIL actionUrl: '' (what an untouched panel save emits) :: actionUrl=invalid_type
FAIL actionUrl: 'https://x' :: actionUrl=invalid_type
FAIL actionType: 'url' :: actionType=invalid_type
FAIL actionIcon: 'Link' :: actionIcon=invalid_type
PASS falsifier: known-present neighbour colorVariant: 'blue'
FAIL falsifier: bogus key nonsenseKey: 1 :: =unrecognized_keys

The two falsifiers matter: colorVariant passing shows the schema is not rejecting everything, and the bogus key failing with a different code (unrecognized_keys, not invalid_type) shows the actionUrl failures are the tombstone firing rather than generic strictness.

Reverse verification. Predicted direction: plain red — the new pins assert the absence of a producer this PR removes, so restoring it must break them. Taking the fix out with git checkout origin/main on the two source files (never git stash):

 x drops all three, keeping the live sibling
x drops the empty-string spelling too
x renders no Behavior section and no field bound to a retired key
x emits none of the retired keys on save, even when handed a config carrying them
x hands the host a config the spec accepts, with the retired keys absent
Tests 5 failed | 1 passed (6)

5 of 6 red as predicted. The single case that stayed green is the intended control — the "spec really does refuse these keys" premise block, which does not depend on this change. The DashboardWithConfig failure is the direct evidence for the unconditional-seed claim above: expected { id: 'w1', title: 'Revenue v2', ... } to not have property "actionUrl", in a test that only edits the title.

One honest correction to my own first draft: the round-trip assertion initially failed because the panel emits the documented flattened shape (layout.w becomes layoutW), which the spec refuses as unrecognized_keys. That is the panel's documented host-facing intermediate shape, not a defect this PR introduces; the test now un-flattens before parsing. It did surface a separate question about that contract, filed rather than fixed here (see below).

Targeted suites, all green:

pnpm exec vitest run packages/plugin-dashboard/ -> Test Files 32 passed (32) Tests 276 passed (276)
pnpm exec vitest run packages/types/ -> Test Files 29 passed (29) Tests 388 passed (388)
pnpm --filter @object-ui/plugin-dashboard --filter @object-ui/types type-check
packages/types type-check: Done
packages/plugin-dashboard type-check: Done
pnpm --filter @object-ui/plugin-dashboard --filter @object-ui/types lint
241 problems (0 errors, 241 warnings) -- all pre-existing no-explicit-any
eslint (the 4 changed files)
35 problems (0 errors, 35 warnings) -- all pre-existing; the new test file emits none
control-character self-scan over the 5 changed files -> clean

Dependency closure built first (pnpm --filter '@object-ui/plugin-dashboard^...' build) so tsc read rebuilt .d.ts rather than stale artefacts.

Out of scope, filed separately

objectstack#7193DashboardWithConfig forwards the panel's flattened layoutW/layoutH verbatim to onWidgetSave, and the spec refuses both as unrecognized_keys. Surfaced by the test correction described above. Whether it bites depends on the out-of-repo Studio host (objectui has no in-repo consumer of DashboardWithConfig), so it is filed observation-class rather than fixed here.


Generated by Claude Code

…Url key (objectstack#7129)
`actionUrl`/`actionType`/`actionIcon` are retiredKey tombstones at the widget
level since @objectstack/spec 17.0.0-rc.3 (objectstack#5010, ADR-0049 D2) — the
spec types them `never` and refuses any value. Two producers in plugin-dashboard
still emitted the widget-level key:
- WidgetConfigPanel's Behavior group offered a "Click-through URL" field bound
to `actionUrl`, a control with no consumer on the render side at all.
- DashboardWithConfig seeded `actionUrl: widget.actionUrl ?? ''` into every
widget config, so EVERY panel save persisted `actionUrl: ''` — a parse error —
even when the author never opened the Behavior group.
Removes both, scrubs all three keys in sanitizeDraftForType as defence in depth,
and corrects the @object-ui/types docblock that listed them as ordinary
inherited keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016R9de1FqP7NvwKvqXi92Gh
@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 2:28am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-sdSQ5WY0.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)483.72KB106.71KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)228.51KB56.69KB
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.84KB10.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.49KB17.48KB
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)113.37KB27.40KB
plugin-gantt (index.js)162.79KB39.67KB
plugin-grid (index.js)187.97KB49.79KB
plugin-kanban (index.js)48.53KB13.38KB
plugin-list (index.js)109.73KB26.55KB
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

@os-help
os-help marked this pull request as ready for review August 10, 2026 02:42
@os-help
os-help added this pull request to the merge queueAug 10, 2026
Merged via the queue into main with commit c1e1e6bAug 10, 2026
21 checks passed
@os-help
os-help deleted the claude/issue-7129-widget-actionurl-producer branch August 10, 2026 02:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-help@claude