Skip to content

Commit 1287b2f

Browse files
docs(ui): generate the component reference for all 106 components from sdui.manifest.json; retire the Setup, Console and Studio pages into the metadata pages and rename the section User Interface (#27179)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoD1czdsHPibaHqYrbaZSAWH)_ Fixes #27142 Clause-②: no Metadata-protocol: none Tier H: the diff edits `.github/workflows/lint.yml` (one new gate step), so the maintainer lands it. ## 维护者速读 - **改了什么:** 新增 `scripts/gen-sdui-reference.mjs`,从根目录 `sdui.manifest.json` 生成文档站的「组件参考」(`content/docs/ui/components/`:7 个分类页加 1 个索引页),一次覆盖全部 106 个组件,每个组件列出输入项的类型、可选值、是否必填和说明。lint 新增一步 `pnpm check:sdui-reference`:manifest 变了而参考页没重新生成,这一步就红。 - **为什么改:** 106 个可编写组件里有 89 个过去没有自己的文档页,输入项说明只存在于 manifest 文件里。manifest 由 CI 对照渲染器校验,参考页从它生成,就不会像 17 个手写页那样与渲染器脱节。 - **风险与回滚:** 只新增文档页、一个脚本和一个 lint 步骤,不发布任何包;另把这个目录登记为「生成物」(文档审计范围、合并驱动、role-word 扫描、os-regen-merge 分类四处名单)。回滚 = revert 本 PR。 - **先看:** ① `scripts/gen-sdui-reference.mjs` 的 `pageFor()`(分页规则)和 `cellText()`(MDX 转义);② `.github/workflows/lint.yml` 新增的那一步;③ `scripts/check-role-word.mjs` 的 `SKIP_SUBTREES`。 - **两分钟验证:** 运行 `node scripts/gen-sdui-reference.mjs --check`,应看到 `✓ … matches sdui.manifest.json (106 components on 7 pages)`;或在预览站打开 `/docs/ui/components`,索引表有 106 行。 - **范围扩大(按你 12:23Z 的两条裁定:「删掉」「板块名用 User Interface」):** 删掉 `ui/` 下的 `setup-app`、`console`、`studio` 三页。三页中写给元数据作者的内容并入 `apps`、`views`、`actions`、`automation/approvals` 和 `deployment/validating-metadata`;产品导览统一指向 `build-without-code`。栏目名从「Views & Apps」改为「User Interface」。另加 3 条跳转,并改了 1 条旧跳转,避免形成跳转链。 - **需要你做的:** Tier H(改了 workflow),需要你审批并合并。 ## Wider scope on head `906bbcaa` (maintainer rulings) In the K3 card thread on 2026-10-28 at 12:23Z, Tesla answered 「删掉」 to retiring the three Console-tour pages under `content/docs/ui/` and 「板块名用 User Interface」 to the section name. Head `906bbcaa` (on top of a merge of `main`, `f4815b07`) carries both: | Retired page | Where its author-facing facts went | | :--- | :--- | | `ui/setup-app.mdx` | `ui/apps.mdx`: `navigationContributions` and the Setup group anchors | | `ui/console.mdx` | `ui/views.mdx`: the record overlay and per-user list state. `ui/actions.mdx` gains a troubleshooting line, `automation/approvals.mdx` the record tab, and `deployment/validating-metadata.mdx` `_diagnostics`, the sweep endpoint and runtime predicate failure. The product tour is `build-without-code`. | | `ui/studio.mdx` | the same targets as `console`; the tour is `build-without-code` | Other changes: - `content/docs/ui/meta.json` is titled "User Interface", as is the index `navTitle`. `components` takes the three retired slots. - `apps/docs/redirects.mjs`: `/docs/ui/setup-app` now redirects to `/docs/ui/apps`, and `/docs/ui/console` and `/docs/ui/studio` to `/docs/build-without-code`. `/docs/concepts/setup-app` is re-pointed to `/docs/ui/apps` so that it does not chain. - Two inbound links were fixed (`deployment/cloud-connection`, `permissions/record-view-auditing`), and `handwritten-docs.json` was regenerated. - Local gates on `906bbcaa`: 67 derived; 66 exit 0. `check:skill-examples` is NOT MEASURED: it exited 3 because it needs a full spec build, so CI reads it. - The earlier triage PASS (6467314546) was on `c935c785` and does not cover this head. A delta review is handed to triage. - For K4 #27145: any old objectui.org URL whose target was `/docs/ui/console` or `/docs/ui/studio` (rows in #27170's table) must now aim at `/docs/build-without-code` or the specific `apps`/`views` anchor. Otherwise `check-docs-redirects` reds on a chain. ## What changes - **`scripts/gen-sdui-reference.mjs`** (new). Reads `sdui.manifest.json` and writes `content/docs/ui/components/`. Modes: write (default), `--check` (exit 1 when the committed pages differ, naming the file and the first differing line), `--self-test` (20 rows, a pinned battery roster and floor, and the verdict handshake, copied from `check-agent-model-declared.mjs`). - **Layout: one page per namespace**, with two grouping rules. At 106 entries a page per component would be mostly one-row pages, since 48 of them are HTML elements whose only inputs are `className` and `children`. So: - a component whose `tier` is `html` goes to **HTML Elements** (48); - all `plugin-*` namespaces share **Plugin Blocks** (16), because each holds one to three components; - every other namespace has its own page: **Layout & Basic** (`ui`, 14), **Page Structure** (`page`, 7), **Record** (11), **Elements** (6), **Actions** (4). - A namespace the manifest gains with no page row is refused, with the remedy, so a person writes its title and introduction once. - **Each component** is an `h2` with an explicit id (`action:button` becomes `#action-button`), a line with its namespace, tier and whether it is a container, and an inputs table: name, type (unions, enum values, the array element type and the data binding included), required, description. Manifest descriptions are escaped for MDX: braces, angle brackets and pipes outside code spans, and pipes inside them. - **The index page** links all 106 components to their anchors. - **Every page** carries an AUTO-GENERATED comment and a visible callout that name the regeneration command. - **Absence is loud.** An unknown component key, input key or tier value is refused rather than silently omitted from the reference. So are an empty manifest, a key that is not its component's `type`, and two components whose heading ids collide. - **Drift check wiring.** Root aliases `check:sdui-reference` (`--self-test` then `--check`) and `gen:sdui-reference`. A new `lint.yml` step in job `lint-repo-tooling` (`Lint · repo & tooling gates`) runs `pnpm check:sdui-reference`, placed next to the manifest's own check, so a manifest change and its reference change land in one PR. It needed a workflow edit because every gate in this repo is wired as its own lint step; no existing family runs an arbitrary root alias. - **Navigation.** `content/docs/ui/meta.json` lists `components` after `setup-app`. `content/docs/ui/index.mdx` gains the matching card, which `check-section-landing-index` requires. - **The directory is registered as generated in the four places that keep such a list:** - `scripts/docs-audit/affected-docs.mjs` excludes `content/docs/ui/components/**` from the hand-written audit scope beside `references/`. `handwritten-docs.json` is regenerated and its readme names both trees. The maintainer ruling recorded in that file's header is that prompting authors about generated pages is wrong advice. - `scripts/regen-artifacts.mjs` gets a `REGEN_ARTIFACTS` row, and `.gitattributes` routes the directory to `merge=os-regen`. `check:merge-driver` requires every generator to have a recorded disposition. The pages are rendered whole, so regenerating after a merge loses nothing a person wrote. - `scripts/check-role-word.mjs` adds the directory to `SKIP_SUBTREES`, beside `references/` and `protocol-upgrade/`, for the reason its header gives: the fix site is the input description in the component's registration under `ui/`, which this gate does not scan. - `scripts/pm/os-regen-merge.sh` classifies the directory in `OS_REGEN_CLASSES` as `whole` (commit a6ea420). CI's os-regen-merge self-test requires every `merge=os-regen` path to have a class; `dispatch-gates` did not derive that family locally, so it was first seen red on CI. ## Why A metadata author placing a component finds no page of its own for 89 of the 106 authorable components today. Their inputs and descriptions exist only inside the manifest file. The manifest is regenerated and compared by CI's `Build & E2E UI`, so a reference generated from it cannot drift from the renderer the way the 17 hand pages could. ## Tests - `node scripts/gen-sdui-reference.mjs --self-test`: 20 cases pass, 20 batteries at their floor. - `node scripts/gen-sdui-reference.mjs --check` on the PR head: `✓ … matches sdui.manifest.json (106 components on 7 pages)`. - All 106 manifest types are headings, each on exactly one page. The generator asserts this on every run, and the self-test row "the real manifest renders every component on exactly one page" asserts it against the live manifest. <details><summary>One-time ablation: a hand-edited generated line makes --check exit non-zero</summary> Through `scripts/ablation-replace.mjs` on the committed tree, which drops `yes` from the Required cell of `tabs`'s `defaultValue` row in `ui.mdx`: ```text ablation-replace: anchor x1 -> x0 ablation-replace: blob b00794f -> 657a3cff533551a8703d7fcb0dc33c282c636fc5 ablation-replace: ok mutation landed: anchor 1 -> 0, blob b00794f -> 657a3cff5335 ablation-replace: running: node scripts/gen-sdui-reference.mjs --check ✗ gen-sdui-reference --check: content/docs/ui/components/ is not what sdui.manifest.json generates: - ui.mdx differs from the manifest's rendering, first at line 160: committed: "| `defaultValue` | `string` | | — |" generated: "| `defaultValue` | `string` | yes | — |" ablation-replace: command exited 1 ablation-replace: ok restored: blob == HEAD (b00794f) and `git diff HEAD` is empty ``` </details> ## Acceptance notes **URL map for K4's redirects (#27145).** The old objectui.org pages are under `https://www.objectui.org/docs/...` (`ui/apps/site/lib/source.ts`, `baseUrl: '/docs'`). The new pages are on the objectstack docs site: | Old path (objectui.org) | New path | | :--- | :--- | | `/docs/components` (`components/index`) | `/docs/ui/components` | | `/docs/components/basic/icon` | `/docs/ui/components/ui#icon` | | `/docs/components/basic/image` | `/docs/ui/components/ui#image` | | `/docs/components/basic/span` | `/docs/ui/components/html#span` | | `/docs/components/basic/text` | `/docs/ui/components/ui#text` | | `/docs/components/complex/table` | `/docs/ui/components/html#table` | | `/docs/components/data-display/alert` | `/docs/ui/components/ui#alert` | | `/docs/components/data-display/badge` | `/docs/ui/components/ui#badge` | | `/docs/components/disclosure/accordion` | `/docs/ui/components/ui#accordion` | | `/docs/components/form/button` | `/docs/ui/components/ui#button` | | `/docs/components/form/label` | `/docs/ui/components/html#label` | | `/docs/components/layout/box` | `/docs/ui/components/ui#box` | | `/docs/components/layout/card` | `/docs/ui/components/ui#card` | | `/docs/components/layout/container` | `/docs/ui/components/ui#container` | | `/docs/components/layout/flex` | `/docs/ui/components/ui#flex` | | `/docs/components/layout/grid` | `/docs/ui/components/ui#grid` | | `/docs/components/layout/stack` | `/docs/ui/components/ui#stack` | | `/docs/components/layout/tabs` | `/docs/ui/components/ui#tabs` | `check-docs-redirects` strips a `#fragment` before it resolves a destination, so the anchored targets are legal redirect destinations. **Worked examples the hand pages carry and the manifest lacks.** None of it was copied into the generated pages: - Live demos (`SchemaExample`) on every one of the 17 pages. `form/button` has 15 (variants, sizes, icons, disabled and loading states); `basic/text` has 10; the rest have 1 to 4. - `basic/icon`: the section "`name` is identity, not the glyph", which covers the refused name-as-glyph node and the `migrateIconNodeKeys` converter from `@object-ui/types` for stored metadata. - `layout/box`: "Migrating from `div`". `box` reads `children` only, and an html page's `div` is refused at compile time with a refusal that names `box`. - `basic/span`: the deprecation of `span` for JSON-authored pages, which does not apply to the html tier, and its migration guide. - `complex/table`, `layout/*` and the others: hand-written TypeScript `Schema` interfaces. Where these differ from the manifest, the manifest is the contract. **Observations, not filed:** - `content/docs/ui/pages.mdx` § Components lists `record:chatter`, `element:image`, `element:record_picker` and `element:text_input`, which are not in the public manifest. The manifest carries `record:discussion`, `element:definition-list` and `element:repeater`, which that list omits. That list may be describing the spec's `PageComponentType` rather than the public tier, so this is noted for K2's merge of the UI pages, not filed. - The `object-kanban` input `cardFields` uses the word "role" in its description, a reserved word under ADR-0090 D3. The fix site is that registration's description under `ui/`, outside this card's surface. Triage's contract review (6467314546) asked for a carrier card. The dispatching seat screened it out under the filing gate's business test: the phrase is "the object's `highlightFields` role", which means ADR-0085's semantic field role, not an access-control role. No reader is misled about permissions, and check-role-word's own header says not every hit is a bug (its examples are ARIA `role` and `sys_member.role`). If the word should still change, the next PR that edits `ui/packages/plugin-kanban/src/index.tsx` can say "designation". - The serial constraint with K2 (#27170) stands: both edit `content/docs/ui/meta.json`, so whichever lands second merges `main`. ## Verification CI on head `c935c785` (merges `main` after K2 #27170 landed; `content/docs/ui/meta.json` and the UI landing index list `console`, `studio`, `components` in that order): every required context is green (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Lint UI, Type Check UI, Test UI, Build & E2E UI), Build Docs is green, and `mergeable_state` reads clean. Local, on head `ca0a9747`, which merges `main` at `b8615afa`: - `pnpm --filter @objectstack/docs build`: green (`os-verify-lock` VERDICT command-exit 0). Served with `next start`, `/docs/ui/components/plugins` answers 200 with 16 tables and `id="object-grid"`, and `/docs/ui/components` answers 200 with 107 table rows (106 plus the header). - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran …`: 99 derived families, 98 run green, 1 NOT MEASURED (`check:dual-build-cjs-loads`, exit 3, which needs every published package's dist and so is left to CI). - First-run reds the sweep found, each fixed in this PR and re-run green: `check-section-landing-index` (the landing card), `check:docs-audit-scope` (the generated-tree exclusion), `check:merge-driver` (the `REGEN_ARTIFACTS` row) and `check:role-word` (`SKIP_SUBTREES`). `check:pm-dispatch-gates` was red once because it ran while those edits were in progress; it was re-run on the committed tree and is green. - The gate families CI runs are read from this PR's check-runs, not re-run locally. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No spec change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` is CI's to run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes, so there is no changeset (`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. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L9qibwMyAnTNeas3ANYhyi --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibwMyAnTNeas3ANYhyi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 385aa12 commit 1287b2f

32 files changed

Lines changed: 2600 additions & 596 deletions

‎.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,4 @@ content/docs/references/** merge=os-regen
158158
skills/*/references/_index.md merge=os-regen
159159
skills/objectstack-ui/references/react-blocks.md merge=os-regen
160160
scripts/platform-object-tenancy-census.json merge=os-regen
161+
content/docs/ui/components/** merge=os-regen

‎.github/workflows/lint.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,17 @@ jobs:
10131013
continue-on-error: true
10141014
run: node scripts/check-sdui-manifest.mjs --self-test && node scripts/check-sdui-manifest.mjs
10151015

1016+
# The component reference under content/docs/ui/components/ is generated
1017+
# from that same manifest by scripts/gen-sdui-reference.mjs, so it sits
1018+
# next to the manifest's own check: a PR that changes the manifest and
1019+
# not the reference reds here, and the two land in one PR. Offline and
1020+
# unfiltered for the reason the step above gives. The script header is
1021+
# the authority on the page layout and on what the drift check compares.
1022+
- name: Component reference matches the SDUI manifest
1023+
id: component-reference-matches-the-sdui-manifest
1024+
continue-on-error: true
1025+
run: pnpm check:sdui-reference
1026+
10161027
# pm-dispatch SKILL.md line ratchet (#7341 / #5925 item 7): the PM skill
10171028
# is read in full by every seat session and every Routine fire, so its
10181029
# size is a per-round tax on the whole fleet. The #7341 extraction moved

‎apps/docs/redirects.mjs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ export const docsRedirects = [
6060
['/docs/guides/metadata/dashboard', '/docs/ui/dashboards'],
6161
['/docs/guides/public-forms', '/docs/ui/forms'],
6262
['/docs/guides/metadata/doc', '/docs/ui/doc-pages'],
63-
['/docs/concepts/setup-app', '/docs/ui/setup-app'],
63+
['/docs/concepts/setup-app', '/docs/ui/apps'],
64+
// 2026-10-28: the Setup, Console and Studio pages were retired. Their
65+
// author-facing facts moved into apps, views, actions, approvals and
66+
// validating-metadata; the product tour is build-without-code.
67+
['/docs/ui/setup-app', '/docs/ui/apps'],
68+
['/docs/ui/console', '/docs/build-without-code'],
69+
['/docs/ui/studio', '/docs/build-without-code'],
6470
['/docs/guides/solutions/create-vs-edit-form', '/docs/ui/create-vs-edit-form'],
6571
['/docs/guides/solutions/field-grouping-and-order', '/docs/ui/field-grouping-and-order'],
6672
// The page was renamed role-based -> audience-based (the direction

‎content/docs/automation/approvals.mdx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,15 @@ reassign user-picker) is collected by the generic action dialog with **no
759759
per-action UI code**. New decision capabilities ship as backend metadata, not
760760
hand-written buttons.
761761

762+
On the record itself, a record with approval requests grows an **Approvals** tab
763+
beside Details and Related, with a request-count badge: the current step, decision
764+
progress, the approvers it is waiting on, the merged decision timeline and a remind
765+
button for the submitter. Every viewer who can read the record sees it, not only
766+
approvers. A request opens on its record page through the approvals routes rather
767+
than the data API, so an approver who is not an administrator sees the whole page,
768+
decision actions included, although the data API stays closed to them on the
769+
approval objects.
770+
762771
Each action's visibility is gated by a **server-computed per-viewer block** the
763772
service attaches to every request it returns:
764773

‎content/docs/deployment/cloud-connection.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,6 @@ to cloud-hosted runtimes; a self-hosted server leaves both unset. A set
252252
connection platform-managed: no Cloud Connection page, and no connect or
253253
disconnect.
254254

255-
The Setup app itself, and how plugins add entries to it, is described in
256-
[Setup app](/docs/ui/setup-app). Running the server is covered in
255+
How plugins add entries to the Setup app is described in
256+
[Apps](/docs/ui/apps#contributing-navigation-to-another-app). Running the server is covered in
257257
[Self-hosting](/docs/deployment/self-hosting).

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,73 @@ schema, so a schema error is `os validate`'s verdict to give — but a rule that
777777
can fail the build fails `os lint` too.
778778
</Callout>
779779

780+
## Metadata diagnostics on load
781+
782+
Every metadata item is also validated against its schema when a server loads it, and
783+
the result travels with the item as a `_diagnostics` envelope:
784+
785+
```ts
786+
interface MetadataDiagnostics {
787+
valid: boolean;
788+
errors?: Array<{ path: string; message: string; code?: string }>;
789+
warnings?: Array<{ path: string; message: string; code?: string }>;
790+
}
791+
```
792+
793+
`valid: false` means at least one error, and whatever depends on the item (rendering,
794+
queries, automation) is unsafe to use. Warnings are advisory. `path` is dot-delimited
795+
(`fields.email.type`, `columns.0.bind`). An environment overlay merges first and is
796+
validated after, so the envelope describes the effective item.
797+
798+
`GET /api/v1/meta/diagnostics` sweeps every type and item in one request:
799+
800+
| Query | Default | Effect |
801+
| :--- | :--- | :--- |
802+
| `severity` | `error` | `error` returns invalid items only; `warning` also returns warning-only items |
803+
| `type` | — | One metadata type |
804+
| `package` | — | One package |
805+
806+
The response carries `entries` (each `{ type, name, diagnostics }`), `total`,
807+
`scannedTypes`, `scannedItems` and per-type `stats`. `total === 0` is a usable CI gate
808+
against a running server; add `severity=warning` to hold warnings to the same bar.
809+
Studio shows the same envelope on the metadata directory, each type's list, each
810+
item's edit page and the governance page (`…/metadata/_diagnostics`).
811+
812+
## When a predicate cannot be evaluated at runtime
813+
814+
A predicate that gets past authoring and fails to evaluate (a misspelled root, a type
815+
error, a missing function) never fails silently. The Console warns once per distinct
816+
predicate source, in production builds too, naming the node, the key, the predicate and
817+
the engine's reason:
818+
819+
```text
820+
[ObjectUI] A visibility predicate could not be evaluated - node "record:alert" (id: "a1")
821+
visibleWhen: "nosuchroot.status == 'draft'"
822+
Reason: ... nosuchroot is not defined
823+
```
824+
825+
What the failure does depends on what the predicate guards:
826+
827+
| Predicate | On failure |
828+
| :--- | :--- |
829+
| A field's `visibleWhen`, `readonlyWhen`, `requiredWhen`; a view's `visibleOn`; an option's `visibleWhen`; list conditional formatting | **Fail open**: the rule's safe default applies, so a broken rule never hides a field. A rule that never fires while its field stays visible is the symptom, usually a bare field name where `record.<field>` was meant |
830+
| An object field group's `visibleWhen` | **Fail closed**: the whole group is hidden on the form and the record page |
831+
| A page node's `visibleWhen` / `visible` / `hidden`, and a `page:tabs` item's `visibleWhen` | **Fail closed**: the node or tab is withheld; a withheld related list is not counted in its tab badge |
832+
| A navigation item's or area's `visible` | **Fail open**: the item renders for everyone, reported under `app-shell:visible` |
833+
834+
At **submit** a broken field rule is refused, not passed. A field `visibleWhen` that
835+
cannot be evaluated refuses the submit on the client, with a message naming the field
836+
and the rule, because no server evaluates `visibleWhen`. A broken `requiredWhen` or
837+
`readonlyWhen` is refused by the server, and the form shows that refusal beside the
838+
input. A `visibleWhen` that reads `previous` cannot be evaluated on a create form, so
839+
such a form is always refused.
840+
841+
A **blank** rule (`''`, whitespace, or an envelope whose `source` is blank) is not "no
842+
rule". A blank field rule is refused when it is authored, and one already stored is
843+
refused at submit. A blank gate (`visible`, `hidden`, `disabled`, a page node's
844+
`visibleWhen`) reads as "no gate" at runtime and is reported once with the `[blank]`
845+
reason.
846+
780847
## The workflow
781848

782849
```bash

‎content/docs/permissions/record-view-auditing.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ concerns.
204204
## Reading the trail
205205

206206
`sys_audit_log` ships a **Record Views** list view — filtered to `action: read`,
207-
newest first. In the Setup app, Diagnostics › [Audit Log](/docs/ui/setup-app),
208-
the entry the plugin contributes, opens the audit browser, which offers that
207+
newest first. In the Setup app, Diagnostics › Audit Log ([contributed](/docs/ui/apps#contributing-navigation-to-another-app) by the plugin),
208+
opens the audit browser, which offers that
209209
view as a preset; its "Open as list" link opens the object list on it.
210210

211211
The object is append-only and exposes only `get` and `list` on the data API;

‎content/docs/ui/actions.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ and [Connect an MCP Client](/docs/ai/connect-mcp).
663663
664664
| Symptom | Cause → fix |
665665
|:---|:---|
666-
| Button doesn't appear | `locations` doesn't include the surface; or the `visible` CEL throws (e.g. a bare, unprefixed field name) and fail-closed hides it; or the user fails `requiredPermissions` |
666+
| Button doesn't appear | `locations` doesn't include the surface (empty `locations` is right for a selection-only action that a list view's `bulkActions` places); or the `visible` CEL throws (e.g. a bare, unprefixed field name) and fail-closed hides it; or the user fails `requiredPermissions`, which hides the button at every location with no greyed-out control and no message |
667667
| Click → `Action 'x' … not found` | `target`-style script with no registered handler — add the `registerAction` call in `onEnable` (Path B above) |
668668
| REST → 400 naming the action's `type` | The type has no server dispatch (`url`/`modal`/`form`/`api`) — open its `target` in the client, or call that endpoint directly |
669669
| Appears in UI, missing from `list_actions` (MCP) | `ai.exposed` not `true`, `ai.description` under 40 characters, or the type isn't headless-callable |

‎content/docs/ui/apps.mdx‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,62 @@ in 17.1). Both the app and `branding` are strict: a top-level `logo` or `theme`
749749
refused with a pointer to `branding`, and a misspelled key inside `branding` is
750750
refused rather than dropped.
751751

752+
## Contributing navigation to another app
753+
754+
A package can add menu entries to an app it does not own with
755+
`navigationContributions` (ADR-0029 D7). The Setup app is built this way: it ships
756+
as a shell of empty group anchors, and each capability plugin contributes the
757+
entries for the objects it owns, so the rendered menu always matches the loaded
758+
plugins. A disabled plugin contributes nothing and its slot stays empty.
759+
760+
```typescript
761+
navigationContributions: [
762+
{
763+
app: 'setup',
764+
group: 'group_integrations',
765+
priority: 100,
766+
items: [
767+
{ id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', requiresObject: 'sys_webhook' },
768+
],
769+
},
770+
]
771+
```
772+
773+
| Property | Type | Description |
774+
| :--- | :--- | :--- |
775+
| `app` | `string` | The target app's name, e.g. `setup` |
776+
| `group` | `string` | optional. A group nav item id in the target app; omit it to append at the app's top level |
777+
| `priority` | `number` | Merge order within the group, lower first (default `200`, the object-extender convention) |
778+
| `items` | `NavigationItem[]` | Ordinary [navigation items](#navigation-items), with the usual `requiresObject`, `requiredPermissions` and `visible` gates |
779+
780+
The runtime merges every contribution into the target app's `navigation` by group
781+
id and priority when the app is read, so registration order does not matter.
782+
783+
**A group id the target app does not declare is not refused.** The items are
784+
appended at the app's top level and a `nav_contribution_group_missing` diagnostic
785+
is raised, at `warn` by the runtime and as a warning by `os build` and
786+
`os validate` when both packages are composed into one artifact. Neither fails
787+
the build. The anchor id is the whole contract between the shell and its
788+
contributors, and a typo renders a working menu with the entry one level up, so
789+
read that warning.
790+
791+
The Setup app's anchors:
792+
793+
| Group id | Holds |
794+
| :--- | :--- |
795+
| `group_overview` | System Overview dashboard |
796+
| `group_apps` | Packages and the marketplace |
797+
| `group_people_org` | Users, organizations, business units, teams, invitations |
798+
| `group_access_control` | Positions, permission sets, sharing rules, API keys |
799+
| `group_approvals` | Approvals inbox, requests, history, delegations |
800+
| `group_configuration` | Settings, localization, authentication, email, storage, AI, feature flags, branding |
801+
| `group_diagnostics` | Sessions, notification events, audit log |
802+
| `group_integrations` | Webhooks, cloud connection |
803+
| `group_advanced` | OAuth applications |
804+
805+
Setup is for platform administration and needs `setup.access`. For
806+
project-specific administration pages, declare an app of your own.
807+
752808
## Complete Example
753809

754810
{/* os:check */}

0 commit comments

Comments
 (0)