Repository navigation
Commit bf658d1
feat(spec): a report nav item without reportName opens the app's report index; the console stops injecting All reports (#27566)
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBARQoNehDR2CAhzzKsfZvvA)_
Fixes #27525
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none edited. Four governed hits teach a `report` nav item
and all stay true for a named report; none teaches the index form, and
none teaches that `reportName` is required:
`skills/objectstack-ui/SKILL.md:171` (the `report` row lists
`reportName`, `label`, `icon`, "Link to a report"),
`skills/objectstack-ui/rules/navigation.md:38` (a named example),
`skills/objectui/guides/app-composition.md:21` (`{type:'report',
reportName}` routes to `/report/:name`) and `:34` (emit the typed target
field for the chosen type). Named, not edited.
ADR-0087: a widening, not a retirement. Nothing an author wrote before
parses differently.
**The claim's `Metadata-protocol: none` is measured as `added`.** The
order says "as measured", and AGENTS.md's kinds put "a new accepted
value" under `added`: `{ type: 'report' }` with no `reportName` was
refused and is now accepted. The changesets carry `added`, and every
package the PR moves is graded `minor`: `@objectstack/spec`,
`@objectstack/cli` (help text only, but the PR-wide `added` sets its
floor), `@object-ui/layout`, `@object-ui/app-shell`, `@object-ui/i18n`
and `@object-ui/types`.
**Migration** (in the `@object-ui/app-shell` changeset): an app that
relied on the injected row adds one entry to its navigation where the
index belongs: `{ id: 'nav_reports', type: 'report' }`.
## 维护者速读
- 改了什么:导航项 `type: 'report'` 不写 `reportName`
时表示「本应用的报表索引」(`/apps/APP/reports`);控制台不再给每个应用自动加「全部报表」行,只有导航里声明了这一项才画出来;Studio
与元数据编辑器的导航检查器都把未选报表的报表项显示为「报表索引」,选它会删掉 `reportName` 键而不是存 `''`;`os
explain app`、两页文档和 QA 清单同步改写。
- 为什么改:维护者定了「加上才有」:索引入口由作者自己加。原来每个非 Studio 应用都被注入一行,没有报表的应用也有;而 spec
又不允许作者自己声明一个索引项。
- 风险与回滚:放宽接受集,原来能解析的导航照旧;依赖注入行的应用会少一个入口,迁移是加一行 `{ type: 'report'
}`,仓内自带报表的 showcase 与 todo 示例已加。回滚即还原本 PR。
-
先看:`ui/packages/app-shell/src/layout/UnifiedSidebar.tsx`(删掉的注入块)、`ui/packages/layout/src/NavigationRenderer.tsx`(`resolveHref`
的 `report` 分支与无标签时的 `reportIndex`
询问)、`ui/packages/app-shell/src/views/metadata-admin/previews/AppNavCanvas.tsx`(`NAV_TARGET_OPTIONAL`,保存不再丢掉索引项)。
- 两分钟验证:`cd ui && pnpm exec vitest run
packages/app-shell/src/layout/__tests__/UnifiedSidebar.reportIndexEntry-27525.test.tsx`,应见
6 个用例全绿:未声明时无「All reports」行,声明后链接到 `/apps/crm/reports` 且在该路由高亮。
- 需要你做的:无,席位落地。
## What changes
**Measured on `origin/main` `57c51aab` (before).**
1. Spec: `NavigationItemSchema.safeParse({ id: 'nav_reports', type:
'report' })`:
```
bare report item:
[{"expected":"string","code":"invalid_type","path":["reportName"],"message":"Invalid
input: expected string, received undefined"}]
named report item: ACCEPTED
empty-string reportName: ACCEPTED
```
2. Console: `UnifiedSidebar.tsx` drew a static row under `{!isStudioApp
&& (…)}` after the app's `NavigationRenderer`: a `Link` to
`${basePath}/reports` with `data-testid="sidebar-report-index"`, text
`sidebar.allReports` ("All reports" / 「全部报表」), for every non-Studio app,
whatever its navigation or its reports. Pinned on main by
`UnifiedSidebar.reportIndexLink-13020.test.tsx` ("links the app's report
index, though the navigation names no report").
3. ⌘K: **the card's premise does not hold on main.**
`CommandPalette.tsx` already lists reports beyond the navigation:
`useAppReportEntries(activeApp, open && !inStudio, gates)`
(`views/appReportIndex.ts`, objectui#13020 / #13273 /
objectstack#26892), the reports the app ships (its package's own, plus
those its served navigation names), limited by `mayOpenReport` to the
ones whose datasets read objects the member may read, and by the
navigation's gates. That is the same list `AppReportIndexView` shows.
Pinned on main by `AppReportIndex-13020.test.tsx` ("⌘K offers the app's
reports beyond its navigation": a report no entry names is offered to a
reader; one on an unreadable object, a joined report with such a block,
and another package's report are not). The HotCRM comment the card
quotes predates it. So Target 3 needed no new reader; this PR reuses
that one and pins it under an index-only navigation (below).
**After (head `dd0bd483`; unchanged at `19f4cf83`).** The same probe
answers `bare report item: ACCEPTED`; the other two lines are unchanged.
### The spec (`packages/spec/src/ui/app.zod.ts`)
`ReportNavItemSchema.reportName` is `z.string().optional()`. Its
describe: "Target report name. Omit it and the entry opens the app's
report index (the reports the app ships that the member may open)
instead of one report." The variant's TSDoc states the two targets and
that the console draws an index entry only where the navigation declares
one. The unknown-key teaching at `navItemSurface` (`reportname` → "type:
'report' (with reportName)" on every other variant) stays true and is
pinned. `stack.zod.ts`'s cross-check of a named `reportName` against
`reports` reads `typeof nav.reportName === 'string'`, so an absent name
skips it; lint `validate-nav-target-refs` likewise skips an absent
target. Regenerated: `content/docs/references/ui/app.mdx` (five
`reportName` rows go from ✅ to optional with the new text).
`check:generated` reports the other 13 artifacts current,
`check:authorable-surface` included: the base lists keys
(`ui/ReportNavItem:reportName` stays), not required-ness, so it does not
move. No metadata-form row or form translation names `reportName`, so no
bundle moves.
### Readers of a nav item's `reportName`, measured
| Reader | Absent `reportName`, before | Change |
|:---|:---|:---|
| `layout` `resolveHref` (sidebar link, mobile tab bar, ⌘K, Studio
preview, active-row election) | `#`, a dead entry | `BASE/reports`; the
active-row election reads the same answer, so the entry lights on the
index route (exact match) and not on `report/NAME` |
| `layout` `inheritedNavItemLabel` (label-less entry) | the entry's `id`
| asks the host's resolver for the new `{ kind: 'reportIndex' }` target,
else the `id` |
| `app-shell` `useNavTargetLabel` (the console's resolver) | n/a |
answers `sidebar.allReports` ("All reports" / 「全部报表」), the injected
row's own text |
| `app-shell` `UnifiedSidebar` | injected an index row for every
non-Studio app | injects nothing; the declared entry is drawn by
`NavigationRenderer` |
| `app-shell` `CommandPalette` | n/a (an absent name would put
`undefined` in the set of nav-named reports and in the item value) | the
index entry is offered as itself; only a non-empty `reportName` counts
as nav-named; the value never holds `undefined` |
| `app-shell` `appReportIndex` (`reportNamesIn`) | already skipped a
non-string name | unchanged |
| `app-shell` `AppHeader` (breadcrumb) | reads the route, not the nav
item | unchanged |
| Studio nav-item inspector (`StudioDesignSurface`) | "unbound", with
the "left out of the saved navigation" hint | not unbound; the report
picker's empty face reads 「报表索引」 / "Report index", with a hint saying
what it opens; picking it removes `reportName` |
| Studio save (`AppNavCanvas` `navPayloadOf`) | left the entry out of
the save | kept (`NAV_TARGET_OPTIONAL`); the file's own parity pin (left
out exactly when the spec refuses it bare) now judges `report` taken
bare |
| metadata-admin `AppNavInspector` | the combo field, empty; it offered
reports only, so once a report was picked there was no way back to the
index | the report combo leads with a "Report index" row (new optional
`emptyLabel` on `InspectorComboField`), and picking it REMOVES
`reportName`, as the Studio does, never writing `''`; the 「报表索引」 hint
shows while `reportName` is absent; its live preview shows
`/apps/APP/reports` |
| metadata-admin `AppPreview` | dropped a label-less, childless entry
with no target | keeps it: the target reads `reports` |
| `navSurface` `bindTarget` (what the Studio rail opens) | `null` |
unchanged: the index is no metadata item to design |
### Shipped apps
Every non-Studio app showed the injected row. Read as the maintainer's
「加上才有」: the apps this repo ships that **ship reports** get the entry, so
none loses a working index; an app with no report had an index that said
"No reports", and gets nothing.
- `examples/app-showcase` (four reports, all named in its navigation):
`{ id: 'nav_report_index', type: 'report', icon: 'bar-chart-3' }`,
appended last in its reports group so no existing index path moves
(`nav-and-detail-grants.test.ts` pins `navigation[3].children[11]`).
- `examples/app-todo` (ships `task.report.ts`, which its navigation
never named, so the injected row was its only door): `{ id:
'nav_reports', type: 'report', icon: 'bar-chart-3' }` in its Analytics
group.
- `examples/app-crm`, `examples/app-multi-package`: ship no report,
unchanged. No package under `packages/` ships a report.
Both new entries carry no `label`, so they show the console's own name
for the index and need no translation key.
## Tests
- **Spec pin** (`packages/spec/src/ui/app.test.ts`, replacing "should
reject report nav item without reportName"): a bare `report` item parses
on the variant, through `NavigationItemSchema` and inside an
`AppSchema`, and holds no `reportName` after parse; an unknown key on it
is still refused, and `reportName` on a `page` item still answers "type:
'report' (with reportName)"; the describe names the report index.
- **Console** (`UnifiedSidebar.reportIndexEntry-27525.test.tsx`, the
13020 sidebar file moved and rewritten, since its first assertion is now
the opposite behaviour): (a) no injected row on an app whose navigation
declares no index entry (no "All reports" link, no `/apps/crm/reports`
href, no `sidebar-report-index`); (b) a declared label-less index entry
links `/apps/crm/reports` named "All reports", a labelled one shows its
label; (c) it is the active row on `/apps/crm/reports` and the named
report entry is not; (d) drawing it reads no report list; CONTROL: a
named report entry still routes to `/apps/crm/report/crm_pipeline`.
- **⌘K** (`AppReportIndex-13020.test.tsx`, new block): under a
navigation that declares only `{ type: 'report' }` and names no report,
the palette finds "Win Rate" and "Pipeline", and not "Case Aging"
(unreadable object), "Mixed Blocks" (a block on it) or "Headcount"
(another package); it offers the index entry itself, and its value holds
no `undefined`.
- **Studio**: `StudioDesignSurface.navItemTypes-11790` (a report entry
is born as the index, parses, is sent, shows the index hint and no "left
out" hint, the picker offers `Report index` first; a pick writes
`reportName`; the index face removes it again),
`StudioDesignSurface.sharedSelect-11865` (`reportBorn` and a new
`reportBound` row on the index face), `AppNavCanvas.navPayload-11776`
(bare `report` taken and sent).
- **metadata-admin** (`AppNavInspector.reportIndexClear-27525.test.tsx`,
new): picking "Report index" on a named report entry writes it with no
`reportName` key; the trigger names the index while the key is absent;
CONTROL: picking a report writes its name; CONTROL: a `page` entry's
combo offers no empty row. Ablated (the empty pick routed back to
`patch({ reportName: '' })`): the first case goes red, the other three
stay green; restored.
- Updated for the removed row: `UnifiedSidebar.iconModeLabelFocus-12795`
(the tab walk no longer ends on "All reports") and
`UnifiedSidebar.favoritesMenu-12427` (no row to exclude).
<details><summary>Runs</summary>
All on the shared container, under `scripts/pm/os-verify-lock.sh`.
- Spec, at `0b6a152e` (pre-merge): `pnpm --filter @objectstack/spec
test` → `Test Files 751 passed (751)`, `Tests 22330 passed | 1 todo`;
`test:repo` → `Test Files 86 passed (86)`, `Tests 1432 passed`;
`typecheck` → `VERDICT command-exit 0` (`check:test-typecheck: OK`).
- Spec, after merging `origin/main` (head `dd0bd483`): `build` +
`check:generated` → "All 14 generated artifacts are up to date"; `test`
→ `Test Files 752 passed (752)`, `Tests 22341 passed | 1 todo`.
- ui, 138 files that import or pin the edited modules (the sidebar, ⌘K,
report index, Studio nav inspector and save, metadata-admin nav
inspector / canvas / preview, `@object-ui/layout`, the
`@object-ui/types` nav and mirror-parity suites), `cd ui && pnpm exec
vitest run --maxWorkers=3 FILES`: first run 4 red, all four pinning the
old behaviour (a bare report entry unbound / left out of the save / not
taken bare); updated as listed above. Final, on `dd0bd483`: `Test Files
138 passed (138)`, `Tests 1755 passed (1755)`. Narrowed to these files
on purpose; CI's Test UI runs the rest.
- ui type-check, `@object-ui/types`, `@object-ui/layout`,
`@object-ui/i18n`, `@object-ui/app-shell`: all four `type-check: Done`,
`VERDICT command-exit 0`. The full `turbo run build --concurrency=2`
(115/115 tasks, the console's `tsc` included) is green too.
</details>
<details><summary>Ablation</summary>
One ablation, of the behaviour the console pins test: `resolveHref`'s
index answer put back to `'#'` with `scripts/ablation-replace.mjs`
(`ui/packages/layout/src/NavigationRenderer.tsx`, anchor hit 1 → 0, blob
`9f866973` → `f4b35fdd`), then the sidebar and report-index files run
(the ui suites alias `@object-ui/layout` to its source, so no build sits
between): `Tests 3 failed | 13 passed`: (b) both "draws a declared index
entry" cases and (c) "the active row" went red. Restored: blob
`9f866973` == HEAD, `git diff HEAD` empty. The ⌘K index-entry pin stayed
green under it, as expected: it asserts the entry is offered, not where
it goes. (a) "no injected row" is the inverse of the assertion `main`
pinned (`UnifiedSidebar.reportIndexLink-13020.test.tsx`: "links the
app's report index, though the navigation names no report"), so it is
red against `main`'s sidebar by construction.
</details>
## Acceptance notes
- `reportName: ''` was accepted by the spec before and still is (this PR
widens absence only). The console read `''` as falsy and answered `#`;
it now answers the index, the same as absence.
- The mobile tab bar (`AppSchemaRenderer`) takes no label resolver, so a
label-less index entry there shows its `id`, as every other label-less
entry already does in that host.
- `reportName: ''` stays accepted by the spec and refused by
`defineStack` when the app declares reports; this PR does not rule on
it. Both inspectors now reach the index by removing the key, so neither
writes `''`.
- Teaching text (added to the file surface by the claim amendment,
comment 6497681948): `os explain app`
(`packages/cli/src/commands/explain.ts`) now says a report item may omit
`reportName` to open the index; `content/docs/ui/apps.mdx` (Report
Navigation, and the label-fallback paragraph) and
`content/docs/ui/reports.mdx` document `{ type: 'report' }` and say the
console adds no index entry of its own.
- QA ledger: `docs/qa/platform-checklist/areas/platform-core.json` still
told runners the console adds the "All reports" row to every app.
`platform-core.nav-surfaces-render` (r8: `steps[2]`, `acceptance[1]`
clause and verify) and `platform-core.builtin-apps-nav-render` (r10:
`steps[9]`) now say the index is a declared, served entry, each with a
`history` row; `check:platform-checklist` OK. History rows that record
the old row are left as written.
- The skills' index form (`skills/objectstack-ui/SKILL.md:171`,
`rules/navigation.md:38`,
`skills/objectui/guides/app-composition.md:21,34`) is carded separately
by seat 3.
- The Studio's "+" still sends a new report entry into the
create-a-report flow; an index entry is made by choosing the `report`
type on an entry and leaving the report unpicked.
## Verification
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **108** commands at `01ed76af` and
the same 108 at `0b6a152e`. Run serially; first pass 103 exit 0, `pnpm
--dir ui exec vitest run scripts/` exit 1 (two 15 s sweep timeouts
beside a locked run: `check-vi-mock-inherit.test.ts`,
`js-comment-mask-returns-interpolation-9424.test.ts`; rerun alone: `Test
Files 2 passed (2)`, `Tests 91 passed`; the rest of that run: `Tests
4899 passed`), and three exit 3 (prerequisite: no dist) that went to
exit 0 after the full build: `check:skill-examples` ("274 prose examples
type-check"), `check:dual-build-cjs-loads` ("112 published require entry
point(s) across 69 package(s) load"), `check:i18n-coverage` ("OK (14
config(s), 527 baselined untranslated string(s), none new)").
- `dispatch-gates --ran`: "108 derived, 108 run, 0 NOT-MEASURED, 0
UNRUN".
- After the changeset grades moved to `minor` and the merge: every
changeset gate in the list again (`check-changeset-no-major --base
origin/main`, `check-empty-changeset` and its self-test,
`check:changeset-gate-self-tests`,
`check:pm-changeset-deadline-census`): exit 0.
`GITHUB_EVENT_NAME=pull_request node
scripts/check-changeset-no-major.mjs --base origin/main --event` (this
body): exit 0, "LEVEL AXIS: this PR declares clause-② `yes (widening)`
and metadata-protocol `added`, and no package … it moves is graded
`patch`".
- `gen:authorable-surface-base` refuses on this shallow clone (it cannot
establish the anchor's ancestry); `check:authorable-surface` is green
inside `check:generated`, and the base, a key list, has no entry this
change moves.
- Round 2 (head `19f4cf83`; `origin/main` `72b08eec` merged at
`fad30718`). The teaching-text paths derive **85** commands, run
serially: 83 exit 0, and `check:docs` (exit 1) and
`check:skill-examples` (exit 3) refused because the spec build predated
the merge; both exit 0 after `pnpm --filter @objectstack/spec build`.
The inspector paths derive **34**, all exit 0. Over the whole branch the
list is now **111**: the 108 above plus `check:i18n`,
`check:i18n-walk-parity` and `check:platform-checklist`, all green.
`dispatch-gates --ran`: "111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN".
`cd packages/cli && pnpm test`: `Test Files 529 passed | 1 skipped
(530)`, `Tests 6602 passed | 9 skipped`. ui metadata-admin inspectors:
`Test Files 205 passed (205)`, `Tests 2786 passed | 1 skipped`.
`@object-ui/app-shell` `type-check`: exit 0.
- Not run locally: the dogfood battery (no `!` in the title and no
`(narrowing)` arm); the repo-wide CI families. CI's check-runs are the
record for those.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzhEW5k2a4Vd5KSupfu)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 144702c commit bf658d1
36 files changed
Lines changed: 531 additions & 108 deletions
File tree
- .changeset
- content/docs
- references/ui
- ui
- docs/qa/platform-checklist/areas
- examples
- app-showcase/src/ui/apps
- app-todo/src/apps
- packages
- cli/src/commands
- spec/src/ui
- ui/packages
- app-shell/src
- chrome
- hooks
- layout
- __tests__
- views
- __tests__
- metadata-admin
- inspectors
- previews
- studio-design
- i18n/src/namespaces/studio
- layout/src
- types/src
- zod
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
197 | | - | |
| 197 | + | |
198 | 198 | | |
199 | 199 | | |
200 | 200 | | |
| |||
588 | 588 | | |
589 | 589 | | |
590 | 590 | | |
591 | | - | |
| 591 | + | |
592 | 592 | | |
593 | 593 | | |
594 | 594 | | |
| |||
787 | 787 | | |
788 | 788 | | |
789 | 789 | | |
790 | | - | |
| 790 | + | |
791 | 791 | | |
792 | 792 | | |
793 | 793 | | |
| |||
1039 | 1039 | | |
1040 | 1040 | | |
1041 | 1041 | | |
1042 | | - | |
| 1042 | + | |
1043 | 1043 | | |
1044 | 1044 | | |
1045 | 1045 | | |
| |||
1294 | 1294 | | |
1295 | 1295 | | |
1296 | 1296 | | |
1297 | | - | |
| 1297 | + | |
1298 | 1298 | | |
1299 | 1299 | | |
1300 | 1300 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
173 | 188 | | |
174 | 189 | | |
175 | 190 | | |
| |||
290 | 305 | | |
291 | 306 | | |
292 | 307 | | |
293 | | - | |
| 308 | + | |
| 309 | + | |
294 | 310 | | |
295 | 311 | | |
296 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
303 | 308 | | |
304 | 309 | | |
305 | 310 | | |
| |||
0 commit comments