Skip to content

fix(plugin-dashboard): optionsFrom filters commit the raw value, not the display label (#4465) - #4504

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4465-optionsfrom-raw-value
Aug 12, 2026
Merged

fix(plugin-dashboard): optionsFrom filters commit the raw value, not the display label (#4465)#4504
yinlianghui merged 1 commit into
mainfrom
claude/issue-4465-optionsfrom-raw-value

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Closes #4465

Defect

A dashboard global filter sourced from optionsFrom committed the rendered LABEL as its value, so every widget it drives queried for a value no record carries and repainted to "No rows".

The option source is a server GROUP BY, and that response carries both forms of every grouped value:

  • rows — server-resolved display labels: [{status: 'Backlog'}, …, {status: 'In Review'}, …]
  • drillRawRows — the index-aligned raw values: [{status: 'backlog'}, …, {status: 'in_review'}, …]

DashboardFilterBar.tsx read r?.[from.valueField] off rows and never consulted drillRawRows, so picking "In Review" broadcast runtimeFilter {status: 'In Review'}. Statically declared options were never affected (Revenue Pulse's region shows EMEA, commits emea).

Fix

Options are paired index-wise — value from drillRawRows[i], label from the displayed row — in one new module-local pairOptionRows helper both option paths call.

The reading discipline is mirrored from the drill path, which consumes this exact response correctly and always has: DatasetWidget's openDrill reads drillRawRows?.[index] at the same index the display row was resolved at, and buildDatasetDrillFilter states why — "the dimension's RAW grouped value … NOT the visible row which carries the display LABEL — a select/lookup label would mis-filter". No helper is shared with it: that one builds an ObjectQL filter keyed by object FIELD, ANDing runtimeFilter and date ranges, which has no overlap with pairing an option list; reshaping it would have meant refactoring the drill path to serve this call site.

Three abstentions, all landing on the previous read rather than on a guess:

Case Behavior
No drillRawRows in the response value and label both from rows, exactly as before
rows and drillRawRows disagree in LENGTH abstain to rows — pairing anyway commits another bucket's raw value, silently wrong in a new way and indistinguishable from a correct filter that legitimately matched nothing; labels-as-values is at least visibly wrong
Raw rows carry none of valueField abstain to rows — a dateGranularity dimension is excluded from drillRawRows by the server (it sends drillRanges instead), so this is the server saying "no raw form of this dimension"

The client-side find fallback is unchanged in behavior: real records already carry the stored value, so it pairs with no raw sidecar.

Red-first — the COMMITTED value

The pre-existing DashboardFilterBar.options.test.tsx asserts only that the option list renders, which is true on both sides of the defect; that gap was its cover. The new DashboardFilterBar.optionsFromRawValue.test.tsx asserts what is committed. Against origin/main's DashboardFilterBar.tsx with the new tests in place: 4 failed | 6 passed (10), verbatim —

FAIL … > broadcasts the RAW value into a bound widget's runtimeFilter
AssertionError: expected { status: 'In Review' } to deeply equal { status: 'in_review' }
- Expected
+ Received
  {
-   "status": "in_review",
+   "status": "In Review",
  }

FAIL … > hands the host the raw value
AssertionError: expected "vi.fn()" to be called with arguments: [ 'st', 'in_review' ]
  1st vi.fn() call:
  [
    "st",
-   "in_review",
+   "In Review",
  ]

FAIL … > pairs BY INDEX — a second position resolves to its own raw value
  [ "st", -"todo", +"To Do" ]

FAIL … > reads the label from `labelField` and the value from the raw row when they differ
  [ "st", -"usr_002", +"Alan Turing" ]

The first case goes through DashboardRenderer end-to-end, so the assertion is the bound widget's real re-query — the card's runtimeFilter {"status":"In Review"} reproduced as a test. Post-fix: 10 passed (10).

The 6 that pass on BOTH sides are the must-not-change pins: the trigger still displays the label and never the raw token (its own test, so it actually runs pre-fix); static metadata-declared options commit emea / display EMEA and never consult the data source; a response with no drillRawRows keeps today's read; the length-mismatch abstention; the no-such-field abstention; and the client-side find path.

Verification

  • pnpm exec vitest run packages/plugin-dashboard/46 files, 381 tests passed (includes the flaky: DatasetWidget.dottedDimensionTable's "issues the ONE read" case asserts a fetch AFTER waiting only on the rendered cell #4487 flake file, green).
  • pnpm --filter @object-ui/plugin-dashboard type-check — clean.
  • Tests are not read by this package's tsconfig (declared TEST_DEBT), so they were checked separately under a temp project lifting the test exclusion: the new file contributes 0 errors (14 pre-existing, unchanged).
  • dist/index.d.ts built from this branch and from origin/main is byte-identical — runtime-only, hence patch.
  • check-control-bytes, check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-type-check-coverage — all green. ESLint on the changed files: 0 errors.

Generated by Claude Code

…the display label (#4465)

A dashboard global filter sourced from `optionsFrom` built its options from a
server GROUP BY, whose response carries both forms of every grouped value:
`rows` holds the server-resolved display LABELS (`{status: 'In Review'}`) and
the index-aligned `drillRawRows` holds the RAW stored values
(`{status: 'in_review'}`). `DashboardFilterBar` read the value off `rows`, so
picking an option broadcast `runtimeFilter {status: 'In Review'}` — a value no
record carries — into every bound widget, and each repainted to "No rows".

Options are now paired index-wise: value from `drillRawRows[i]`, label from the
displayed row. The reading discipline is mirrored from the drill path, which
consumes this exact response correctly (`DatasetWidget`'s `openDrill` reads
`drillRawRows?.[index]`; `buildDatasetDrillFilter` documents that the visible
row carries the label and would mis-filter). No helper is shared with it: that
one builds an ObjectQL filter keyed by object field and has no overlap with
pairing an option list.

Three abstentions all land on the previous read rather than on a guess: no
`drillRawRows`, a length disagreement with `rows` (index pairing would commit
another bucket's raw value — silently wrong in a new way), and raw rows that
carry no such field (a date-bucketed dimension the server excludes by design).

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

vercel Bot commented Aug 12, 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 12, 2026 10:02pm

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-DadZorHm.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) 22.10KB 4.37KB
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) 36.76KB 9.60KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
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) 4.91KB 0.87KB
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.32KB 108.45KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.79KB 41.35KB
fields (index.js) 230.07KB 57.07KB
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.33KB 1.20KB
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.13KB 12.76KB
plugin-charts (index.js) 62.07KB 17.65KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.86KB 31.50KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.03KB 59.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 188.13KB 50.00KB
plugin-kanban (index.js) 48.62KB 13.42KB
plugin-list (index.js) 111.07KB 27.08KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
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) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
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

Copy link
Copy Markdown
Collaborator Author

ACCEPT — step-7 复核 by PM session session_017Qqyix2QcnpUC9XeYVDzx3 (focused review).

  • The drill-path mirror was measured to its three declarations (DatasetWidget's index read, buildDatasetDrillFilter's header rule, the adapter's aligned-by-index contract) and correctly NOT force-shared — one pairing helper in-file serving both option branches is the right altitude.
  • Red-first lands the card's exact repro end-to-end (runtimeFilter {"status":"In Review"}in_review) plus the index pin and the labelField-differs lookup case. The six both-sides pins include the assertion that matters most: the length-mismatch abstention did NOT commit the naive pairing's wrong value — silently-wrong-in-a-new-way explicitly excluded.
  • All three unprompted decisions accepted as measured-better: the dateGranularity abstention (the server's drillRanges shape is "no raw form exists", and without the guard the dropdown would have gone empty — a new failure mode); per-row skip rather than per-row label fallback (the defect one row at a time, refused); dedupe keying on raw (two raw values sharing a label stay two options — strictly more correct, named and verified against the package suite).
  • .d.ts byte-identical → patch stands. CI 20/20 green, foreground-polled to real conclusions. The TEST_DEBT stale-count observation (declared 6 vs measured 14 under the measuring template) is recorded for triage — a bookkeeping gap in the gate, correctly not smuggled into this card's surface.

Flipping ready + arming auto-merge.


Generated by Claude Code


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 12, 2026 22:16
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 844ed3a Aug 12, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4465-optionsfrom-raw-value branch August 12, 2026 22:16
Sign up for free to 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.

A dashboard global filter sourced from optionsFrom commits the display LABEL as its value — every widget it drives silently returns zero rows

2 participants