Repository navigation
Commit 7b2e557
fix(drill): "Open in list" is offered only when the list can show exactly the drawer's records (#30735)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu4M8GSzQt9g91oqAEXonaaD)_
Fixes #30654
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:仪表盘和报表下钻抽屉里的「在列表中打开」,以及 `drillDown.target: 'navigate'`,现在只有列表页的
URL 能把下钻筛选原样写出来时才给出口;写不全就不给,留在抽屉里。
- 为什么改:下钻筛选 = 点中的分组 ∧ 组件筛选 ∧ 度量的下钻范围。URL 一个参数只能放一个值、也写不了
`$in`,以前会丢掉条件,打开的列表比抽屉里的数字更宽(抽屉 0 张发票,列表 4 张)。
- 风险与回滚:只影响下钻出口是否显示;单条件等值和日期区间下钻的 URL
逐字节不变。布尔值分组的下钻现在也不再给出口(沿用相关列表「查看全部」的同一条精确规则)。回滚即 revert 本 PR。
-
先看:`ui/packages/app-shell/src/views/useOpenRecordList.ts`(`drillRecordListFilter`、`useRecordListDrillNavigation`)、`ui/packages/react/src/context/DrillNavigationContext.tsx`(新可选成员)、`ui/packages/plugin-charts/src/ObjectChart.tsx`
的两处守卫。
- 两分钟验证:app-showcase「Revenue Pulse (filtered)」→「Paid Rate by Status」表格 →
Sent 行的 Paid Invoices 格子:抽屉 0 行,右上角没有「在列表中打开」;同一行的发票数格子仍有,点开 URL 是
`filter[status]=sent` 加组件的日期区间。
- 需要你做的:无,席位落地。
## What changes
- `@object-ui/react`: `DrillNavigationValue` gains an optional
`canOpenRecordList(objectName, filter): boolean`. Absent means the host
claims every filter it is handed has an exact destination, which is how
hosts behaved before.
- `@object-ui/app-shell`: `useRecordListDrillNavigation()` returns `{
openRecordList, canOpenRecordList }` for `DashboardView` and
`ReportView`. Both go through the new pure `drillRecordListFilter`,
which resolves placeholders first (`resolveFilterPlaceholders` with the
same `useFilterScope` scope the navigate arm always used), then asks
`exactRecordListFilter` (reused unchanged, with
`usePermissions().checkField(object, field, 'read')`). `openRecordList`
navigates to the exact filter and does nothing when there is none, so a
caller that skipped the question still never lands on a wider list.
`useOpenRecordList` stays as the navigate half.
- `@object-ui/plugin-dashboard`: `OpenInListButton` renders nothing, and
`DrillDownDrawer`'s `navigateOnly` is false, when `canOpenRecordList`
answers `false`. The drill then stays in the drawer, the same fallback
as a host with no `openRecordList`.
- `@object-ui/plugin-charts`: `ObjectChart`'s navigate arm and its own
drawer-header "Open in list" ask the same question (with `drillFilter`
and `merged` respectively).
- The 13 `DashboardView` / `ReportView` tests that mock
`./useOpenRecordList` now mock `useRecordListDrillNavigation` instead
(mechanical, same `vi.fn()` opener).
- One changeset: patch for the four packages (`Metadata-protocol: none`
⇒ patch; `Clause-②: yes (widening)` for the new optional interface
member, per the claim amendment).
Answers to the claim's two questions:
- A `drillDown.target: 'navigate'` click with no exact destination opens
the in-place drawer (no navigation, no dead click).
- Placeholders are resolved before the exactness check, inside
`drillRecordListFilter`, with the scope `useOpenRecordList` already
read; a `{current_user_id}` that resolves to the same value as a bucket
on the same field is one exact param (pinned).
## Tests
New pins:
- `useOpenRecordList.placeholderScope-9022.test.tsx`, new top-level
describe (6 cases): the repro shape `composeDrillFilter({ status: 'paid'
}, { status: 'sent' })` answers `false` and `openRecordList` does not
navigate; an `$in` scope answers `false`; a plain equality bucket
answers `true` and opens byte-for-byte
`/apps/crm/invoice/data?filter%5Bstatus%5D=sent`; the same condition
twice is one param; placeholders resolve before judging; an unreadable
field answers `false`. The file's existing CONTROL (equality + date
range URL pinned as a whole string) stays green unchanged.
- `DrillDownDrawer.escapeHatch.test.tsx`, new top-level describe (4
cases): host answers `false` ⇒ no button, asked with the drill filter;
`navigate` stays in the drawer; host answers `true` ⇒ button; host
without `canOpenRecordList` ⇒ old behaviour for both the button and
`navigate`.
- `ObjectChart.drillNavigate.test.tsx`, new top-level describe (2
cases): host answers `false` ⇒ drawer, no header button; host answers
`true` ⇒ navigates. The file's existing four cases (no
`canOpenRecordList`) are the unchanged-host control.
Ablation (each on the committed fix, restored to the HEAD blob by
`scripts/ablation-replace.mjs`; the tests import the source relatively,
no `dist/` involved):
| Mutation | Result |
|:---|:---|
| delete the `OpenInListButton` guard | 1 failed / 7 (host answers false
case) |
| `DrillDownDrawer` `navigateOnly` guard → `true` | 1 failed / 7
(navigate stays in drawer) |
| `ObjectChart` navigate guard → `true` | 1 failed / 6 |
| `ObjectChart` header-button guard → `true` | 1 failed / 6 |
| `drillRecordListFilter` returns the resolved filter unjudged | 3
failed / 12 (repro, `$in`, unreadable field) |
<details><summary>Test runs</summary>
On `7643fb85` (before the main merge):
- `vitest run packages/plugin-dashboard/ packages/plugin-charts/
packages/react/` + the 42 app-shell test files that name
`useOpenRecordList`, `DrillNavigation`, `DashboardView`, `ReportView`,
`OpenInList`, `DrillDownDrawer`, `drillUrlFilters`, `recordListPath` or
`exactRecordListFilter`: `Test Files 485 passed (485)`, `Tests 5230
passed | 15 skipped (5245)`.
- `type-check` for `@object-ui/react`, `@object-ui/plugin-dashboard`,
`@object-ui/plugin-charts`, `@object-ui/app-shell`: all four `Done`,
lock `VERDICT command-exit 0`.
Declared narrowing: the rest of app-shell's 2208 test files are left to
CI's `Test UI` (a full local run did not finish inside the box's budget
at 2 workers).
On `7519c053` (after merging `origin/main`, which touched
`ui/packages/core` exports, and a rebuild of the app-shell closure):
plugin-dashboard + plugin-charts full + the same 42 app-shell files,
`Test Files 348 passed (348)`, `Tests 3662 passed | 15 skipped (3677)`;
the four `type-check`s `Done`; lock `VERDICT command-exit 0`.
</details>
## Acceptance notes
- `exactRecordListFilter` is reused unchanged, so its refusals now apply
to drills too: a drill on a boolean bucket (`{ is_paid: true }`), on
`$ne`, on a text operator, or with a placeholder that cannot resolve to
a comparand the route carries no longer offers "Open in list" (the
drawer still lists the records). Before, those opened a list scoped by a
string comparand or with the condition dropped.
- `ReportView`'s group-key fallback drill (older server with no drill
metadata) goes through the same host, so it is judged the same way.
- The PR carries no assignee or reviewer: the seat sets them at
hand-over.
- Branch is the card thread's harness branch
(`claude/project-thread-qgekwo`, claim amendment 6671583364), not
`claude/issue-30654-*`.
## Verification
Live, app-showcase on its own port (`pnpm dev -- --fresh`), Console via
Vite, headless Chromium at 1440 px. "Revenue Pulse (filtered)" → "Paid
Rate by Status", Sent row (`Sent 4 0 0.0%`):
| | Paid Invoices cell drawer | "Open in list" there | Invoice count
cell "Open in list" URL |
|:---|:---|:---|:---|
| before (the seven source files at `origin/main`) | 0 rows | shown |
`/apps/com.example.showcase/showcase_invoice/data?filter%5Bissued_on%5D%5Bgte%5D=2026-08-07&filter%5Bissued_on%5D%5Blte%5D=2026-11-05&filter%5Bstatus%5D=sent`
|
| after | 0 rows | absent | identical string |
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands` on `7643fb85`: 35
families, all `exit 0`. `pnpm --dir ui exec vitest run scripts/` was
killed by the container's memory limit while the live rig ran (exit 1,
SIGKILL, no verdict) and re-run alone: `Test Files 170 passed | 2
skipped (172)`, exit 0. `--ran` with exit codes: "35 derived famil(ies)
accounted for — 35 run, 0 NOT-MEASURED (a DERIVED zero — all 35 recorded
an exit code and none of them is 3)". `check:nul-bytes` exit 0.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgTGQVA597MpPgAhmzK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 49c8568 commit 7b2e557
24 files changed
Lines changed: 422 additions & 50 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views
- plugin-charts/src
- plugin-dashboard/src
- __tests__
- react/src/context
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
49 | 51 | | |
50 | 52 | | |
51 | 53 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
74 | 76 | | |
75 | 77 | | |
76 | 78 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
47 | 49 | | |
48 | 50 | | |
49 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
380 | 380 | | |
381 | 381 | | |
382 | 382 | | |
383 | | - | |
| 383 | + | |
384 | 384 | | |
385 | 385 | | |
386 | 386 | | |
| |||
0 commit comments