Skip to content

fix(showcase): scope the Delivery Operations status filter to the task widgets - #7612

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7568-ops-dashboard-filter-bindings
Aug 11, 2026
Merged

fix(showcase): scope the Delivery Operations status filter to the task widgets#7612
os-zhuang merged 1 commit into
mainfrom
claude/issue-7568-ops-dashboard-filter-bindings

Conversation

@claude

@claudeclaudeBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes#7568

Showcase authoring fix. The global-filter engine is untouched — it did exactly what the metadata told it to.

What was wrong

showcase_ops_dashboard declared its dashboard-scoped filter as a bare field: 'status' carrying the task vocabulary (backlog / todo / in_progress / in_review / done), and the project-bound widgets declared no filterBindings. A widget with no bindings inherits a dashboard filter on its own object's like-named field, and showcase_project also has a status — with a disjoint vocabulary (planned / active / on_hold / completed / cancelled).

So the inherited binding was field-valid and value-empty: the query was well-formed, the backend answered 200 OK, and the widget rendered a zero. packages/lint's dashboard-filter-field-unknown rule cannot see this — it checks that the effective field exists, which it does.

Which fix, and why

Both admissible options were considered:

shapeverdict
Aper-widget filterBindings on the project widgetschosen
Bscope the filter with scope: 'widget' + targetWidgetsrejected

A, for three reasons.

  1. It is the mechanism this repo has measured working. Revenue Pulse maps region on the invoice widgets to sales_region on the account widgets through filterBindings, verified on the wire and in the echoed SQL in the same QA run. targetWidgets is described in packages/lint/src/validate-widget-bindings.ts as the legacy allow-list that filterBindings overrides, and the browser bundle that would implement scope: 'widget' is not vendored here (packages/console/dist is absent) — so option B could not be driven, only asserted. Demoing an unmeasured capability in the showcase is precisely what Prime Directive chore: version packages #10 forbids.
  2. It is what the Studio widget inspector authors (objectui#2586), so it is the shape a showcase reader should be taught.
  3. The two status vocabularies are disjoint, so there is no project field to re-target onto. The honest binding is an opt-out, not a mapping.

The filter is also given the explicit name task_status. A bare status made the opt-out read as "ignore project status" rather than "this control is about tasks"; with the name, the metadata says what the filter governs and every widget it does not govern says so on its own line. dateRange stays inherited everywhere on purpose — projects do carry created_at, so that filter is meaningful on both sides.

A fifth widget

The issue names four. table_spend is a fifth project-bound widget with the identical defect; it is fixed here too. It went unnoticed because an emptied table reads as "no data", not as a broken filter — a zeroed KPI tile at least looks wrong.

Verification — driven, not read

The dashboard renderer reads published metadata, resolves each dashboard filter to a field per widget, ANDs it with the widget's own filter, and issues one POST /api/v1/analytics/dataset/query per widget. A harness did exactly that against a live showcase backend (pnpm dev -- --fresh, seeded data, sqlite), reading the metadata from GET /api/v1/meta/dashboard/showcase_ops_dashboard.

Scope of the drive, stated plainly: the per-widget resolution is transcribed from effectiveFilterField / dashboardFilterDefs in packages/lint/src/validate-widget-bindings.ts — this repo's own executable mirror of objectui's resolveBoundField. Everything downstream of that (metadata → wire → SQL → number) is the real server. The browser bundle itself is not in this repo and was not exercised; the objectui half remains a checklist item (dashboards.global-filters-rescope).

Widget totals, dateRange held unset in every capture so the status selection is the only variable:

widgetdatasetbefore: pristinebefore: status=in_reviewafter: pristineafter: task_status=in_review
kpi_active_projectsproject2022
kpi_at_riskproject1011
kpi_total_budgetproject1090000010900001090000
col_healthproject5055
table_spendproject5055
kpi_awaiting_reviewtask2222
bar_statustask102102
donut_prioritytask102102
line_createdtask102102

The SQL the server echoed tells the same story. Before, under a selection:

kpi_active_projects: SELECT COUNT(*) AS "project_count" FROM "showcase_project" WHERE (status = $1 AND status = $2)
kpi_at_risk: SELECT COUNT(*) AS "project_count" FROM "showcase_project" WHERE (health = $1 AND status = $2)
col_health: SELECT health AS "health", COUNT(*) AS "project_count" FROM "showcase_project" WHERE status = $1 GROUP BY health

After, under the same selection — the project side carries no status predicate at all, and the task side still composes both filters:

kpi_active_projects: SELECT COUNT(*) AS "project_count" FROM "showcase_project" WHERE status = $1
kpi_at_risk: SELECT COUNT(*) AS "project_count" FROM "showcase_project" WHERE health = $1
col_health: SELECT health AS "health", COUNT(*) AS "project_count" FROM "showcase_project" GROUP BY health
kpi_awaiting_review: SELECT COUNT(*) AS "task_count" FROM "showcase_task" WHERE (status = $1 AND status = $2)
bar_status: SELECT status AS "status", COUNT(*) AS "task_count" FROM "showcase_task" WHERE status = $1 GROUP BY status

The QA checklist's Delivery Operations clause — "the global status filter composes with per-widget filters" — still holds, on the side where it is coherent.

The test pins the consequence, not the key

examples/app-showcase/test/dashboard-filter-vocabulary.test.ts, generic over every showcase dashboard:

  1. every value a global filter offers must be a value its effective field can hold on each widget it reaches — otherwise that selection returns zero rows by construction;
  2. a filter must still reach at least one widget — otherwise the repair for (1) is "opt everybody out", which leaves an inert control on the header bar;
  3. the showcase authoring gap: the ops dashboard's global filter is field:'status' with *task* statuses, and the project widgets carry no filterBindings — so it also lands on showcase_project.status and zeroes those tiles #7568 case named in intent terms: task_status governs the task widgets and no project widget.

Reverse verification, direction predicted before running: revert ops-dashboard.dashboard.ts to main and (1) and (3) go red, (2) stays green — the broken filter did reach widgets, it just could not answer them. Measured exactly that. (1) named all five affected widgets individually, including table_spend:

showcase_ops_dashboard > kpi_active_projects: filter "status" binds to showcase_project.status,
whose values are [planned, active, on_hold, completed, cancelled] — selecting
[backlog, todo, in_progress, in_review, done] can only ever return zero rows.

Restored, all three green.

Gates run locally

  • pnpm --filter @objectstack/example-showcase verify (= os validate + tsc --noEmit + vitest run) — green; 18 files / 171 tests passed. The validate step is what runs validate-widget-bindings, the rule family this card sits next to.
  • eslint --no-inline-config on both changed files — clean.
  • node scripts/check-nul-bytes.mjs — OK, 7046 files scanned.

No changeset: @objectstack/example-showcase is private: true and absent from the fixed group in .changeset/config.json, so this PR releases nothing. Labelled skip-changeset.

No packages/spec change was needed, and no platform behaviour was altered.


Generated by Claude Code

…k widgets
The dashboard-scoped global filter carried the showcase_task status
vocabulary but was declared as a bare `field: 'status'`, and the five
project-bound widgets declared no `filterBindings`. Since a widget
without bindings inherits a dashboard filter on its own object's
like-named field, the filter also landed on showcase_project.status —
whose value domain is disjoint — so those widgets emitted
`WHERE status = 'in_review'` against showcase_project, answered 200 OK
with a zero, and read 0 for any selection.
Name the filter `task_status` for the vocabulary it carries and opt each
project-bound widget out with `filterBindings: { task_status: false }`,
the same per-widget mechanism Revenue Pulse uses to map region ->
sales_region across two objects. dateRange stays inherited: projects do
carry created_at.
Adds a showcase test pinning the consequence rather than the key: every
value a global filter offers must be a value its effective field can
hold on each widget it reaches, and a filter must still reach at least
one widget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0127HHmCr5vd3NudQmW6QiNN
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 11, 2026 8:23am

Request Review

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 11, 2026 08:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit 4ed4160Aug 11, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7568-ops-dashboard-filter-bindings branch August 11, 2026 08:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-zhuang@claude