Skip to content

feat(plugin-dashboard): a dataset KPI tile opens the records behind its number - #30634

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-pezzx8
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-pezzx8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread
Fixes #30080

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:看板上绑定数据集的数字卡片(KPI)现在可以点开(鼠标或回车/空格),打开和图表、表格同一个下钻抽屉,列出这个数字背后的记录。
  • 为什么改:经理看到「在途商机总额」后想知道是哪些商机,之前只能去列表手动重设条件,还不一定对得上。裁决 A 要求卡片直接打开对应记录,对不上的不给点击。
  • 风险与回滚:只改 plugin-dashboard 的数字卡片分支;列表筛选 = 卡片筛选 ∧ 日期窗口 ∧ 服务器给的度量范围(drillScopes,来自已合并的 Drill-through lists more records than the number when a dataset or one of its measures declares a filter: the analytics answer carries no per-measure drill scope #30526)。度量没有范围条目(派生度量等)的卡片保持原样、不可点击。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-dashboard/src/metricDrill.ts(筛选怎么拼、什么时候不给点击);DatasetWidget.tsx 的 metric 分支(点击和抽屉放在卡片外侧);tests/DatasetWidget.test.tsx 末尾的 metric tile drill 组。
  • 两分钟验证:打开示例 CRM 的销售漏斗看板,点「在途商机总额」,抽屉里 4 条商机的金额合计应正好是卡片上的 710,000(种子数据)。
  • 需要你做的:无,席位落地。

What changes

  • ui/packages/plugin-dashboard/src/metricDrill.ts (new): metricDrillFilter composes the widget filter (dashboard filters already merged in), the answer's date window (drillRanges[0]) and the tile measure's drillScopes entry through the shared drill sinks (composeDrillFilter, buildDatasetDrillFilter). It answers null, meaning no click, when the answer names no object, when the measure has no drillScopes entry, or when the sink refuses the filter (FilterOperatorError). readMeasureDrillScope reads one measure's entry off the answer. Neither is exported from the package entry.
  • DatasetWidget.tsx, metric branch only: when the filter is not null the tile gets role="button", tabIndex=0, Enter/Space, the test id dataset-metric-drill and a hover/focus ring, and opens the existing DrillDownDrawer titled by the tile's heading. With no filter, the markup is the previous markup. The drawer renders as the tile's sibling, never its child, so events from inside the portal never reach the tile's handlers. The package-local DatasetResult type gains drillScopes.
  • One root changeset (@object-ui/plugin-dashboard: patch).

Why

Ruling A (comment 6631202970): a dashboard tile opens the matching records through the report drill path, and a tile whose filter cannot be expressed as a list filter offers no click. A tile's number also carries the dataset's own filter and the measure's own filter, which only the server knows; #30526 (PR #30596) added the per-measure drillScopes sidecar this reads, so the list is the set of records the number counts.

Tests

  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-dashboard/ on 8f48513: Test Files 203 passed (203), Tests 1948 passed, 6 skipped (VERDICT command-exit 0).
  • New pins: __tests__/metricDrill.test.ts (composition, every no-click arm, the entry reader) and a top-level describe('DatasetWidget metric tile drill') in the shared DatasetWidget.test.tsx harness: no drillScopes, no entry for the tile measure, scope AND widget filter on click (asserts the find object and $filter), an empty entry by Enter key, and the date window bound.
  • pnpm --filter @object-ui/plugin-dashboard run type-check (tsc --noEmit and tsc -p tsconfig.test.json): VERDICT command-exit 0.
Ablation (one, on the scope leg)

node scripts/ablation-replace.mjs replaced const scoped = hasScope ? composeDrillFilter(runtimeFilter, measureScope) : runtimeFilter; with a form that drops the measure scope (anchor 1 -> 0, blob bca877be1179 -> 4a067a960ce7). The two files' run then failed 3 tests: keeps the measure scope AND the widget filter, never only one of them, keeps the measure scope when the widget declares no filter, opens the widget filter AND the measure scope on click (Tests 3 failed, 72 passed). Restored with git checkout HEAD --; hash-object after equals the HEAD blob bca877be1179. ui vitest resolves @object-ui/* to source, so no dist leg.

Acceptance notes

Verification

Live: example CRM pipeline_dashboard, zh-CN, 1440 wide

Before (main fa9c0aa, report 6660360476): the three metric tiles had no pointer, no drill test id, and a click opened nothing.

After (8f48513, objectstack dev --fresh example CRM + Console vite dev server), each answer carried a drillScopes entry for the tile measure (total_amount and avg_amount: amount not null). Three tiles carry dataset-metric-drill; each click opened the drawer, and the listed records match the number:

  • 在途商机总额 710,000: 4 records (stages proposal, qualification, prospecting), amount sum 710,000.
  • 本季度赢单 235,000: 3 records, all closed_won, amount sum 235,000.
  • 平均成交额(同比) 81,000: 5 records, all closed_won, amount sum 405,000 = 5 x 81,000.
Gates on 8f48513

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 35; each ran with its exit code captured before any pipe, all 35 exit 0, plus pnpm --dir ui run check:spec-symbols exit 0. --ran reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Families CI runs are read from this PR's check-runs.

Branch: claude/project-thread-pezzx8 is the card thread's platform-assigned branch (claim 6666212190), not the claude/issue-30080-* convention.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgWjsoG4R5yUYFw3T6s


Generated by Claude Code

…e number

A dataset-bound metric tile opens the list its number counts through the
shared drill drawer, keyboard included, and offers no click whenever the
list cannot be proven to match the number. The tile's number also carries
the dataset's own scope and the measure's own filter, which only the
server knows, so the scope is read from a server sidecar that is not on
the wire yet: until it lands, every tile keeps today's no-click markup.

Co-Authored-By: Claude <noreply@anthropic.com>
…ts number

The metric tile reads its measure's drillScopes entry off the dataset
answer and opens the shared drill drawer with the widget filter AND the
date window AND that scope, so the list matches the number. A measure
with no entry offers no click. The drawer is the tile's sibling, so a
click or a key typed inside it never reaches the tile's handlers.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

维护者速读

席位核对 — ACCEPT · head 8f48513

  • 形态:5 文件 +260/−8(新 metricDrill.ts、DatasetWidget.tsx 的 metric 分支与包内 DatasetResult 类型、两份测试、一个 changeset,@object-ui/plugin-dashboard patch)。check-governed-merges --pr 30634:NOT governed,268 行 ≤ 3000。Fixes #30080,目标 main。全在认领 6666212190 的文件面内;图表、表格、报表下钻(Report, chart and table drills list more records than the number for a filtered measure: they do not read the answer's per-measure drillScopes #30615)、MetricWidget.tsx 与 packages/** 未动。分支是续做的 claude/project-thread-pezzx8,WIP 2fc2a28c 之上合了一次 main(0e39caca)。
  • 契约:Clause-② no,Metadata-protocol none,与认领一致。metricDrillFilter 和 readMeasureDrillScope 都不进包入口(git grep metricDrill 8f485138 -- ui/packages/plugin-dashboard/src/index.ts 无命中);没有新的看板键,只读 PR feat(spec,service-analytics): a dataset answer says which records each measure counted, so a drill lists what the number counted #30596 已发布的 AnalyticsResult.drillScopes。
  • 前提实测:dev 报告 6667056033。席位对照 diff:没有对象、没有条目、或 composeDrillFilter / buildDatasetDrillFilter 抛 FilterOperatorError 时一律返回 null,卡片不给点击,从不退回更宽的过滤,这正是裁决要的“对不上就不给点击”;{} 条目只带组件过滤和日期窗口。抽屉渲染成卡片的兄弟节点,portal 里的点击和空格不会冒泡回卡片。本地实测 pipeline_dashboard 三张卡片都能点开且数目对上:710,000 = 4 条合计,235,000 = 3 条合计,81,000 = 405,000 / 5。plugin-dashboard 全包 203 文件 1948 例绿,type-check 0;消融去掉度量范围后 3 例转红,已还原;派生门禁 35/35 exit 0,check:spec-symbols 0,--ran 0 NOT-MEASURED。
  • 门禁 check-runs:head 上 55 个,44 success,11 skipped(Build/Test Core、Dogfood、Temporal Conformance、Docs、UI Published Artifacts、两个 opt-in 作业与 schedule 门,ui-only diff 的预期跳过),0 红。
  • 接受的偏差:派生度量不给点击只由组件测试钉住,没有实测(树里没有带派生度量卡片的看板)。
  • 范围外去向:图表、表格、报表下钻同样只带分桶和组件过滤,已立 Report, chart and table drills list more records than the number for a filtered measure: they do not read the answer's per-measure drillScopes #30615,不在本卡。
  • 落地条款:中继 pr_ready + automerge_enable。合并即关卡(Fixes),席位同一笔摘掉 pm:dispatched。

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

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it?

1 participant