You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 14c67d4
Browse filesBrowse the repository at this point in the historyBrowse files
feat(plugin-dashboard): a dataset KPI tile opens the records behind its number (#30634)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuWWoy5F2AiXpoRcpmxamz3c)_Fixes#30080
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:看板上绑定数据集的数字卡片(KPI)现在可以点开(鼠标或回车/空格),打开和图表、表格同一个下钻抽屉,列出这个数字背后的记录。
- 为什么改:经理看到「在途商机总额」后想知道是哪些商机,之前只能去列表手动重设条件,还不一定对得上。裁决 A
要求卡片直接打开对应记录,对不上的不给点击。
- 风险与回滚:只改 plugin-dashboard 的数字卡片分支;列表筛选 = 卡片筛选 ∧ 日期窗口 ∧
服务器给的度量范围(drillScopes,来自已合并的 #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.
<details><summary>Ablation (one, on the scope leg)</summary>
`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 bca877b ->
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 bca877b. ui vitest resolves `@object-ui/*` to source, so no
dist leg.
</details>
## Acceptance notes
- The chart, table and report drills still compose only the bucket and
the widget or report filter; reading `drillScopes` there is #30615, not
this PR.
- No derived measure is bound to a tile on `pipeline_dashboard`, so the
derived-measure no-click arm was measured by the widget pin only, not
live.
## Verification
<details><summary>Live: example CRM pipeline_dashboard, zh-CN, 1440
wide</summary>
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.
</details>
<details><summary>Gates on 8f48513</summary>
`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.
</details>
Branch: `claude/project-thread-pezzx8` is the card thread's
platform-assigned branch (claim 6666212190), not the
`claude/issue-30080-*` convention.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgWjsoG4R5yUYFw3T6s
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgWjsoG4R5yUYFw3T6s)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(plugin-dashboard): a dataset-bound KPI tile opens the records behind its number
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
A dashboard metric tile bound to a dataset now opens the same drill drawer its charts and tables already open, by click or by Enter/Space. The list is the widget's filter (the dashboard's filters included) AND the answer's date window AND the measure's `drillScopes` entry, which carries the dataset's own filter and the measure's own filter, so the records listed are the records the number counts. A tile whose measure has no `drillScopes` entry (a derived measure, or a scope that names a relationship path) offers no click, as does a tile whose filter the list cannot express; its markup is unchanged. Nothing to author: the click follows from the dataset answer.
0 commit comments