Skip to content

fix(plugin-report,plugin-dashboard): report, chart and table drills list the records a filtered measure counted - #30651

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-8u6mg4
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-8u6mg4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread
Fixes #30615

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:报表(表格行、小计行、矩阵单元格)和看板(图表、表格、交叉表)的下钻,现在除了点中的分组和组件/报表筛选,还会叠加服务器给的该度量范围(drillScopes),点开列出的就是数字统计的那些记录。
  • 为什么改:带条件的度量(如「已付款发票数」)以前点开会列出整个分组的记录,数字是 0 却列出 4 张发票,读者会不再信任看板。
  • 风险与回滚:只动 plugin-report 和 plugin-dashboard 的下钻;没有范围条目的度量(派生度量等)和没有 drillScopes 的应答不再给下钻;一行里多个度量范围不一致时改为按单元格下钻。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-dashboard/src/DatasetWidget.tsx 的 openDrill、表格行/单元格和 chartDrillMeasure;ui/packages/plugin-report/src/DatasetReportRenderer.tsx 的 drill / drillCell;两个新的小文件 measureDrill.ts、measureDrillScope.ts。
  • 两分钟验证:打开 app-showcase 的 Revenue Pulse (filtered) 看板,在 Paid Rate by Status 表的 Sent 行点 Paid Invoices(0),抽屉应为空;点同一行的 Invoices(4),应列出 4 张 sent 发票;Paid Rate 单元格不可点。
  • 需要你做的:无,席位落地。

What changes

  • The drill composes three legs, not two. The list filter is the clicked bucket AND the widget or report filter AND the answer's drillScopes entry for the measure the click stands for (PR feat(spec,service-analytics): a dataset answer says which records each measure counted, so a drill lists what the number counted #30596's sidecar). A {} entry hands the widget or report filter back untouched, so a drill on an unfiltered measure is byte-identical to the drill it was before.
  • What a click stands for. A chart mark: its series' measure (a pivoted chart: its one measure; a mark with no series key: the one measure a single-series chart draws; a MEASURE__compare series: nothing, it has no entry). A cross-tab or matrix cell: its column's measure. A table row, a report line or a subtotal line: every measure it holds.
  • A table row whose measures select different records drills cell by cell. When every measure of the row shares one scope, the row drills as before (dataset-drill-row). Otherwise the row is not a drill target, and each measure cell with an entry is (dataset-drill-cell), drilling that measure's records. This is the case on table_rate_by_status: invoice_count ({}) beside paid_count (status = paid).
  • No list that does not match the number (the [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080 ruling, comment 6631202970). A measure with no entry (derived, or a scope naming a relationship path) is not clickable; neither is anything on an answer that carries an object but no drillScopes; a chart none of whose drawn measures has an entry gets no click handler.
  • ui/packages/plugin-dashboard/src/measureDrill.ts (new, internal): sharedMeasureDrillScope and scopeDrillFilter, reading entries with the merged readMeasureDrillScope from metricDrill.ts (unchanged). ui/packages/plugin-report/src/measureDrillScope.ts (new, internal): the same two plus measureDrillScope, since plugin-report does not depend on plugin-dashboard. Neither is exported from a package entry; @object-ui/core is untouched.
  • One root changeset: @object-ui/plugin-report and @object-ui/plugin-dashboard, patch.

Why

A dataset number is computed under the dataset's filter, the measure's filter and the widget filter. The drills composed only the last one with the bucket, so a filtered measure drilled to a superset. The server now states the missing two per measure (PR #30596); the KPI tile reads them since PR #30634. This PR makes the report, chart, table and cross-tab drills read them too.

What the drill does when the answer has no drillScopes (the reading the claim asked for)

  • Measured on the server: queryDataset sets drillScopes on every dataset answer that has rows (analytics-service.ts, the block after the drill ranges). It is absent on (1) an answer with no rows, where there is nothing to click; (2) a draft-preview answer, which returns before dimensionFields, drillRawRows and drillRanges are set, so the widget's canDrill is already false there, and the Studio report preview passes no onDrill; (3) a cube query answer, which these consumers never request (they call queryDataset only).
  • So on every answer a current server sends, "no drillScopes" never meets a clickable bucket. The consumers treat a missing map like a missing entry: no drill (the tile's rule in PR feat(plugin-dashboard): a dataset KPI tile opens the records behind its number #30634).
  • One exception, kept on purpose: a report answer with no object (a server older than ADR-0021 D2) still hands ReportView its group-key fallback drill, unchanged, as drill: omits objectFilter when the server returns no drill metadata (older server) pins.

Tests

On f9543126 (the head):

Ablations (two, one per package, on the scope leg)

Both through node scripts/ablation-replace.mjs under the verify lock, replacing return composeDrillFilter(runtimeFilter, scope); with return runtimeFilter; (anchor 1 to 0 each), restored to the HEAD blob with git diff HEAD empty. ui vitest resolves @object-ui/* to source, so there is no dist leg.

  • plugin-dashboard/src/measureDrill.ts (blob db75569cbe8d to 902425afed01, re-run on f9543126): Tests 4 failed, 74 passed over DatasetWidget.test.tsx and DatasetWidget.chartBucketIdentity.test.tsx: the filtered bar, the filtered table cell, the one-scope row, the cross-tab cell.
  • plugin-report/src/measureDrillScope.ts (blob 3ada35c7e701 to e47abefae51b): Tests 3 failed, 48 passed over DatasetReportRenderer.test.tsx: the filtered cell, the one-scope line, the matrix cell.

Acceptance notes

  • The drawer's "Open in list" escape hatch still widens a filtered-measure drill (not fixed here, outside the file surface). Measured live on the same cell after this change: the drawer lists 0 records, and "Open in list" navigates to showcase_invoice/data?filter[issued_on][gte]=2026-08-07&filter[issued_on][lte]=2026-11-05&filter[status]=sent, which lists the 4 sent invoices. serializeDrillFilterParams (ui/packages/app-shell/src/views/drillUrlFilters.ts) writes one URL param per field with params.set, so the scope's status = paid is overwritten by the bucket's status = sent, and a list comparand ($in, $nin) is skipped. Reported to the seat for a card.
  • ReportView's group-key fallback is now reached only for a report answer with no object; it is unchanged.
  • The two small helpers are deliberately package-local copies: a shared one belongs in @object-ui/core, which this claim keeps read only.
  • drillRawTotals is not passed through @object-ui/data-objectstack: no totals row or total cell drills on any of these surfaces, so nothing reads it.

Verification

Live: app-showcase Revenue Pulse (filtered), table "Paid Rate by Status", en-US, 1440 wide

objectstack dev --fresh --seed-admin app-showcase plus the Console vite dev server. The answer's drillScopes: { invoice_count: {}, paid_count: { status: 'paid' } }, no paid_rate entry. Rows: Paid (Invoices 3, Paid Invoices 3, Paid Rate 100.0%), Sent (4, 0, 0.0%). The seed has no open status; the Sent row is the card's case.

Before (14c67d44, main with PR #30634): both rows carry dataset-drill-row. Every cell of the Sent row opens the same list, issued_on window AND status = sent: 4 records, all sent, including under Paid Invoices (number 0) and Paid Rate.

After (ca694112; the one later commit f9543126 only drops a duplicate type alias): no row carries dataset-drill-row; Invoices and Paid Invoices cells carry dataset-drill-cell, Paid Rate cells none.

  • Sent / Paid Invoices (0): list filter window AND status = paid AND status = sent: 0 records.
  • Sent / Invoices (4), the unfiltered control: window AND status = sent: 4 records, all sent.
  • Paid / Paid Invoices (3): 3 records, all paid. Paid / Invoices (3): 3 records.
  • Paid Rate (derived): no drawer opens.
Gates on f954312

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 35 families on f9543126 (the same list as on ca694112). Each ran with its exit code captured before any pipe; all 35 exit 0. --ran reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Also pnpm --dir ui run check:spec-symbols: exit 0.

The first pass, on ca694112, was red on one family: pnpm --dir ui exec vitest run scripts/ failed one-authority-per-exported-name-6273.test.ts, because both new helpers declared a type named MeasureDrillScope. f9543126 removes the dashboard's alias (plugin-report keeps the name); the family re-ran green there (Test Files 170 passed, 2 skipped; Tests 4970 passed), and the 34 other families re-ran on f9543126 too.

Families CI runs are read from this PR's check-runs.

Branch: claude/project-thread-8u6mg4 is the card thread's platform-assigned branch (claim 6667494492, amendment 6667568430), not the claude/issue-30615-* 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_01L6bhgUBrAU6n2ZJWk6VmTV

…ureDrillScope type (#30615)

The one-authority-per-exported-name gate refuses a type name declared in
two packages; plugin-report keeps the name, the dashboard helper spells the
record type inline.

Claude-Session: https://claude.ai/code/session_01L6bhgUBrAU6n2ZJWk6VmTV
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation 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

维护者速读

  • 改了什么:报表(表格行、小计行、矩阵单元格)和看板(图表、表格、交叉表)的下钻,除了点中的分组和组件或报表筛选,还叠加服务器按度量给出的范围(drillScopes),点开列出的就是这个数字统计的记录。
  • 为什么改:带条件的度量(比如「已付款发票数」)以前点开列的是整个分组,数字是 0 却列出 4 张发票。
  • 风险与回滚:只改 plugin-report 和 plugin-dashboard 的下钻。没有范围条目的度量(派生度量等)不再给点击;一行里几个度量范围不同时改成逐格下钻;报表遇到没有 object 的旧服务器应答,仍走原来的分组兜底下钻。回滚即撤本 PR。
  • 先看:两个新小文件 plugin-dashboard/src/measureDrill.ts、plugin-report/src/measureDrillScope.ts,再看 DatasetWidget.tsx 的 openDrill 和 DatasetReportRenderer.tsx 的 drill / drillCell。
  • 两分钟验证:app-showcase 的 Revenue Pulse (filtered),Paid Rate by Status 表的 Sent 行,点 Paid Invoices(0)抽屉为空,点 Invoices(4)列出 4 张 sent 发票,Paid Rate 不可点。
  • 需要你做的:无,席位落地。
  • 席位意见:可合。整张卡一次交付(Fixes)。

席位核对 — ACCEPT · head f954312

  • 形态:22 文件 +638/−31(两个新的包内 helper、DatasetWidget.tsx 与 DatasetReportRenderer.tsx 的下钻分支、17 份已有测试补 drillScopes: {} 夹具、三组新测试、一个 changeset,两包 patch)。check-governed-merges --pr 30651:NOT governed,669 行 ≤ 3000。Fixes #30615,目标 main。全在认领 6667494492 的文件面内:@object-ui/core 的 buildDatasetDrillFilter / composeDrillFilter 只读未改,@object-ui/data-objectstack、MetricWidget.tsx、metricDrill.ts([Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080 的磁贴下钻)与 packages/** 未动。
  • 契约:Clause-② no,Metadata-protocol none,与认领一致,ACCEPT 时按认领要求重读:没有新导出(git grep measureDrill f9543126 -- ui/packages/plugin-dashboard/src/index.ts ui/packages/plugin-report/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 已发布的 drillScopes。plugin-report 的 DatasetCapableSource 与 useDatasetRows 只是把这个已有字段接进来。
  • 席位逐行审读:两侧组合都是「分桶 AND 组件/报表筛选 AND 度量范围」,范围先经 scopeDrillFilter 并进筛选,再交给 core 的分桶构造;{} 原样返回筛选,未加条件的度量下钻与改前逐字节一致。所有「判断不了」都返回 undefined 即不给点击,从不退回更宽的筛选:没有条目、条目不是条件对象、行内范围不一致(规范化 JSON 比较,键序无关)、composeDrillFilter 抛 FilterOperatorError。没有精确分桶筛选可携带非空范围时(宿主的 groupKey 兜底只按分桶重建列表),报表一侧直接不下钻,这是对的。看板一侧沿用 main 的 drillObject 前置,没有对象本来就不下钻;__compare 序列没有条目,不给点击。
  • 前提实测:dev 报告 6669355027。实测 app-showcase 的 Sent / Paid Invoices(0)下钻列出 0 条,Invoices(4)列出 4 条,Paid / Paid Invoices(3)列出 3 条 paid,Paid Rate 不开抽屉;改前同一行每格都列出 4 条。plugin-report 33 文件 313 例绿,plugin-dashboard 203 文件 1957 例绿,两包 type-check exit 0。两处消融(每包一处,范围那一腿换成 return runtimeFilter;)分别转红 4 例和 3 例,已还原。
  • 门禁 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 红。mergeable_state clean。
  • 接受的偏差:(1) 一行里度量范围不一致时整行不下钻,改为逐格下钻,这是让列表和数字对上的唯一形状。(2) 两个 helper 是包内副本,没有放进 @object-ui/core,因为认领规定 core 只读;以后合并到 core 要另开卡。(3) 应答有 object 但没有 drillScopes 时不下钻,和 [Decision] Dashboard numbers cannot be opened: should a tile lead to the records behind it? #30080 的磁贴规则一致;dev 已核实当前服务器只在无行、草稿预览和 cube 查询三种应答里省略这个字段,这三种本来就没有可点的分桶。
  • 小瑕疵(不改):metricDrill.ts 的 readMeasureDrillScope 没做 hasOwnProperty 检查(plugin-report 那份做了),只有度量名叫 __proto__ 时会读错,不影响本卡。
  • 范围外去向:抽屉的 "Open in list" 仍会放宽下钻:serializeDrillFilterParams 对同一字段只留后一个值,并跳过 $in / $nin。实测同一格抽屉 0 条,打开列表 4 条。已立 Drill drawer "Open in list" opens a wider list than the drawer when the drill filter has two conditions on one field or an $in #30654,路线是让下钻的出口走 exactRecordListFilter,不在本卡。
  • 落地条款:中继 pr_ready + automerge_enable。合并即关卡(Fixes),席位同一笔摘掉 pm:dispatched;GitHub 没自动关卡就由席位手动关。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 21:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 21:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit f4d568b Nov 5, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-8u6mg4 branch November 5, 2026 21:36
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/l 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.

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

1 participant