Repository navigation
Commit c8097a9
fix(app-shell): list a report in the index and ⌘K only when the navigation shows the member its entry (#27060)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8SfRz7pGa2P99NAztqiusfy)_
Fixes #26892
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:应用的「全部报表」页和 ⌘K 面板,现在只列出侧边栏会给当前成员看的导航项所指向的报表;导航没有指到的报表照旧列出。⌘K
面板的导航项(对象、仪表盘、页面、报表)也改用侧边栏同一套成员门控。
- **为什么改**:HotCRM 里销售和客服在「全部报表」里看到导航对他们隐藏的报表(如商机报表),点开只有「没有数据」,误以为没数据。
- **风险与回滚**:风险低到中:只在 `app-shell` 前端过滤,复用侧边栏现成的门控,不新增请求;⌘K 面板原本只看叶子项的
`visible`,现在也看 `requiredPermissions`、能力门控和分组的
`visible`,所以有些成员在面板里看到的项会变少(与侧边栏一致)。回滚即还原本 PR。
- **先看**:`views/appReportIndex.ts` 的
`navigationShowsReport`;`chrome/CommandPalette.tsx` 的 `navItems` 与新的
`AppCommandPaletteBody`;新测试 `AppReportIndex.navGates-26892.test.tsx`。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx`,预期
16 个用例全绿。
- **需要你做的**:无,席位落地。
## What changes
**The rule kept.** A report that one or more entries of the app's
navigation name is listed by the report index (`/apps/APP/reports`) and
by the palette's "reports beyond the navigation" only when the member is
shown **at least one** of those entries, under the sidebar's own gates:
`navEntriesShownToMember` over `useNavMemberGates()`. An entry hidden by
its `visible`, its `requiredPermissions`, a runtime-capability gate
(`requiresObject` / `requiresService`), the object-read prune, a group
those gates hide, or an API `dataSource` read that has not answered
(pending counts as hidden, as in the sidebar) does not count. A report
**no** entry names keeps today's read rule alone, so the index stays its
only door (the objectui#13020 reason for the index). "At least one, not
every": a member shown any entry naming the report can already open it
from the sidebar, so hiding it from the index would only remove a second
door to something they can reach.
Against the card's three personas: the service agent's four opportunity
reports are named by entries the navigation hides from them (`visible:
OPPORTUNITY_AUDIENCE`, per the card), so they leave the index and the
palette; the sales rep's three case reports leave too when HotCRM's
navigation names them behind an audience gate the rep fails, which the
card's "sidebar shows 7" reading implies; the marketing user's index
already dropped the case reports through the dataset read rule, which is
unchanged. HotCRM's source is not in this session's reach: these are
readings of the card against the rule, pinned here on fixtures of the
same shapes, not a run against the HotCRM seed. A HotCRM report named by
**no** entry keeps listing, by design.
Commits, in order:
1. `views/appReportIndex.ts`: new exported `navigationShowsReport(app,
gates)`; `useAppReportEntries(app, enabled, gates)` takes the member's
gates and filters by it before `mayOpenReport`. `AppReportIndexView`
passes `useNavMemberGates()`; the palette passes its gates.
2. `chrome/CommandPalette.tsx` (surface widened by the seat's order
amendment): the palette's `navItems` were `flattenNavigation(nav)`
filtered by each leaf's own `visible` only, so an entry hidden by
`requiredPermissions`, a capability gate or its group's `visible` was
still offered through the palette's navigation door, a nav-named report
included. They are now `flattenNavigation(navEntriesShownToMember(nav,
gates))`. **This also gates the palette's object, dashboard and page
entries, and the record-search scope (`searchableObjectNames`, read from
`navItems`), the way the sidebar gates them.** The palette's own
`useNavVisibility(activeApp)` reader is gone: it now has exactly the one
binding reader `useNavMemberGates()` builds. Two doc sentences in
`hooks/useNavMemberGates.ts` that named the palette as calling
`useNavVisibility` directly are corrected; commit 5 adds an optional
parameter to `useNavMemberGates`.
3. The gates are built in a new `AppCommandPaletteBody` wrapper and
handed down, `null` in the `studio` scope: that scope has no app and
reads nothing until it opens (pinned by
`CommandPalette.studioScope-11863`), and building the gates in the
shared body read the object list while closed. `useAppReportEntries`
answers no entry without gates.
4. Test-only: the new suite's adapter double is typed instead of `any`.
5. `useNavMemberGates` takes an optional app, the frame's by default;
the palette passes the app it lists, so Home (expression scope binds no
app, palette lists the host app) gates the host app's bindings and
package scope. The one existing-export change: an added optional
parameter, no behaviour change for any existing caller.
No server change, no new request, no spec change.
## Tests
New `views/__tests__/AppReportIndex.navGates-26892.test.tsx` (18 cases),
through the real `MetadataProvider`, `MePermissionsProvider`,
`ExpressionProvider` and `I18nProvider`. The package owns Alpha, Beta
and Gamma (named by entries) and Delta (named by none), all on a
readable dataset, so the navigation is the only gate in play:
- index: Gamma's entry `visible: false`, or `requiredPermissions` the
member lacks, or inside a `visible: false` group, or bound to an API
read still pending ⇒ Alpha, Beta, Delta; controls: the permission
granted, `visible: true`, a second shown entry beside a hidden one, and
a navigation naming no report ⇒ all four;
- palette, both doors: the same `visible: false`, pending,
`requiredPermissions` and hidden-group cases ⇒ no Gamma; the granted
control offers it once; an object entry with `requiredPermissions` the
member lacks is not offered;
- one read per binding: with a sidebar stand-in (`useNavMemberGates` +
`navEntriesShownToMember`, the sidebar's walk) mounted beside the open
palette, an entry's API `dataSource` URL is requested exactly once and
both show the entry;
- Home shape (h): the frame's expression scope binds no app (`{}`) while
the palette lists the host app with a report entry bound to an API read:
the entry is shown once the read answers (`total` 1) and requested
exactly once; control: `total` 2 stays hidden.
Changed: `AppReportIndex-13020.test.tsx`'s control "reads the report and
dataset lists, never the object registry". Its adapter double now
records a package-scoped `getItems` with its scope, because the index's
gates read the app's object scope (`object?packageId=…`), the sidebar's
own read in the console; the unscoped `object` registry read is still
pinned absent, and the scoped read is now pinned present.
## Acceptance notes
- The palette's wider gating (commit 2) is a behaviour change for every
palette entry type, not only reports: a member now sees in ⌘K exactly
the navigation entries the sidebar shows them. Approved as a bounded
in-place fix by the seat (order amendment on the card), in its own
commit.
- The index and the palette read the gates' reads (the app's object
scope, each entry's API binding) through the same readers as the
sidebar; in the console the sidebar is mounted on every app page, so
these are not new requests.
- Like the rest of this module, the navigation read is the app's
`navigation` (with module contributions merged in); `areas[].navigation`
is not walked, so a report named only inside an area is treated as named
by no entry, as `appShippedReports` already treats it. Observation only,
not measured against a producer.
## Verification
All runs after merging `origin/main` (`cb8988ee`) into the branch; head
`ad2bc8e9` (commit 5 added on `e8cfd011`). Dependency closure `pnpm
--workspace-concurrency=2 --filter '@object-ui/app-shell^...' build`
under the verify lock.
<details><summary>Tests, type-check, lint</summary>
- `pnpm --dir ui exec vitest run --maxWorkers=2` over the 140 app-shell
test files that import the palette, the report index, `ConsoleLayout`,
`AppContent`, `HomeLayout`, `sharedUserFeeds` or `useNavMemberGates`
(the new suite included), on `ad2bc8e9`: Test Files 140 passed (140),
Tests 1050 passed (1050), lock VERDICT command-exit 0. Narrowed from the
package's 1804 files to those importers; the full `Test UI` run is CI's.
- `pnpm --filter @object-ui/app-shell type-check` (both `tsc --noEmit`
and `tsconfig.test.json`): exit 0.
- `pnpm --dir ui exec eslint` on the six changed files: exit 0, 0
errors; warning counts per modified file equal their `origin/main`
counts, the new suite has none.
- `pnpm --dir ui run check:new-line-citations`: 0 new citations, exit 0.
</details>
<details><summary>Ablations (scripts/ablation-replace.mjs, restored to
the HEAD blob each time)</summary>
1. `appReportIndex.ts`: `shownByNavigation(report.name as string) &&
mayOpenReport(` replaced by `mayOpenReport(` plus a marker (the index
gate dropped): 8 cases red (index `visible: false`,
`requiredPermissions`, hidden group, pending; palette `visible: false`,
pending, `requiredPermissions`, hidden group, which all come back
through the "beyond the navigation" door), 8 controls green. Restored:
blob `41159b265975` equals HEAD, `git diff HEAD` empty. A first attempt
was refused by the tool before running (its replacement text already
occurred in the file), so it measured nothing.
2. `CommandPalette.tsx`:
`flattenNavigation(navEntriesShownToMember(activeApp?.navigation || [],
gates))` replaced by `flattenNavigation(activeApp?.navigation || [])`
(the palette's navigation gate dropped): 5 palette cases red (`visible:
false`, pending, `requiredPermissions`, hidden group, the gated object
entry), 11 green. Restored: blob `2e5450c4f0d7` equals HEAD, `git diff
HEAD` empty.
3. `CommandPalette.tsx`: `useNavMemberGates(props.activeApp ??
undefined)` replaced by `useNavMemberGates()` (the palette gating the
frame's app): both Home-shape (h) cases red, then restored to the HEAD
blob, on `ad2bc8e9`.
Ablations 1 and 2 ran on head `c5ed8f31` (before the test-typing commit
and the merge of `origin/main`, neither of which touches the two ablated
files).
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `ad2bc8e9` derived 24 commands; each ran
with its exit captured before any pipe (`pnpm --dir ui exec vitest run
scripts/` under the verify lock: Test Files 176 passed, 2 skipped).
`--ran` reconciliation: 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN.
Model tier: no path-derived mandate.
- `node scripts/check-closing-keyword-parity.mjs` exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` exit 0
- `node scripts/check-comment-mask-corpus.mjs` exit 0
- `node scripts/check-sdui-manifest.mjs` exit 0
- `node scripts/check-sdui-manifest.mjs --self-test` exit 0
- `pnpm --dir ui exec vitest run scripts/` exit 0
- `pnpm --filter @objectstack/spec run check:duration-unit-keys` exit 0
- `pnpm check:cross-package-test-inputs` exit 0
- `pnpm check:dispatcher-error-vocabulary` exit 0
- `pnpm check:driver-memory-census` exit 0
- `pnpm check:engine-double-contract` exit 0
- `pnpm check:gitlink-declared` exit 0
- `pnpm check:nul-bytes` exit 0
- `pnpm check:published-files` exit 0
- `pnpm check:query-options-erasure` exit 0
- `pnpm check:refd-timer-probe` exit 0
- `pnpm check:skill-identifier-liveness` exit 0
- `pnpm check:test-source-alias` exit 0
- `pnpm check:tier-file-adoption` exit 0
- `pnpm check:type-check-coverage` exit 0
- `pnpm check:type-check-debt` exit 0
- `pnpm check:undeclared-dep-imports` exit 0
- `pnpm check:watch-hint-literal` exit 0
- `pnpm check:where-matcher` exit 0
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the changed files: 0 errors, no
warning added.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. Nothing published changes (`@object-ui/app-shell` is `private:
true`): `skip-changeset`; both declaration lines are filled in above.
- [x] 6. `packages/spec` not touched.
- [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_01JQwxbZNdnTTr8WJsM98Mxh
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbZNdnTTr8WJsM98Mxh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e3470ed commit c8097a9
6 files changed
Lines changed: 544 additions & 38 deletions
File tree
- ui/packages/app-shell/src
- chrome
- hooks
- views
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | | - | |
| 55 | + | |
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
147 | | - | |
| 147 | + | |
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
152 | | - | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
153 | 170 | | |
154 | 171 | | |
155 | 172 | | |
156 | 173 | | |
157 | 174 | | |
158 | 175 | | |
159 | 176 | | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | 177 | | |
165 | 178 | | |
166 | 179 | | |
| |||
204 | 217 | | |
205 | 218 | | |
206 | 219 | | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
215 | 231 | | |
216 | 232 | | |
217 | 233 | | |
| |||
232 | 248 | | |
233 | 249 | | |
234 | 250 | | |
235 | | - | |
| 251 | + | |
236 | 252 | | |
237 | 253 | | |
238 | 254 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
62 | | - | |
63 | | - | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| |||
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | | - | |
258 | | - | |
| 257 | + | |
| 258 | + | |
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
| |||
308 | 308 | | |
309 | 309 | | |
310 | 310 | | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
311 | 316 | | |
312 | | - | |
313 | | - | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
314 | 320 | | |
315 | 321 | | |
316 | 322 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
| 25 | + | |
23 | 26 | | |
24 | 27 | | |
25 | 28 | | |
26 | 29 | | |
27 | 30 | | |
28 | 31 | | |
29 | | - | |
| 32 | + | |
| 33 | + | |
30 | 34 | | |
31 | 35 | | |
32 | 36 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
111 | | - | |
112 | | - | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
113 | 116 | | |
114 | 117 | | |
115 | 118 | | |
| |||
202 | 205 | | |
203 | 206 | | |
204 | 207 | | |
| 208 | + | |
| 209 | + | |
205 | 210 | | |
206 | 211 | | |
207 | 212 | | |
| |||
0 commit comments