Skip to content

Commit 9d1a465

Browse files
[maintainer] refactor(app-shell): move the Interfaces nav rail out of StudioDesignSurface.tsx (slice 3) (#30014)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBCE4YXYCrA6G1yrthEDVvNJ)_ Refs #29988 ## 维护者速读 - 改了什么:把 Studio「界面」支柱左侧的导航栏(导航树及分组、自定义套件贡献的行、页面/仪表盘/报表/视图的「不在导航里」分区、两个栏位 context、类型名与类型图标,共 442 行)原样搬进同目录新文件 `InterfacesNavRail.tsx`,主文件 11,205 → 10,757 行;顺带把两条测试注释里还指着旧文件的出处改指 `PackageSwitcher.tsx`。 - 为什么改:#29988 按裁定 #24586 A 逐片只搬不改,把这个文件拆到 1 万行以下;这是第 3 片(接缝表第 3 行),第 1、2 片是 #30002、#30008。 - 风险与回滚:无行为变化,搬走的两段代码逐字节一致,只多了 import 和十一个 `export`;对外类型声明 701 个文件中 700 个逐字节不变,另 1 个(`ErrorBoundary.d.ts`)只是联合类型成员顺序不同;回滚即 revert 本 PR。 - 先看:`StudioDesignSurface.tsx` 的 import 段;`InterfacesNavRail.tsx` 的 import 段与十一个 `export`;两个按路径读取的测试各加的一行。 - 两分钟验证:在本分支跑下方「Verification」里的两条 `cmp` 命令,预期都无任何输出(逐字节相同)。 - 需要你做的:无,席位落地。 Clause-②: no Metadata-protocol: none ## Seam table Carried forward from #30008. Base lines below are at `origin/main` `eee41490` (this PR's base; `StudioDesignSurface.tsx` is byte-identical at `73a3ef9f`, where #30008 landed), where `ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx` is 11,205 lines. "Left" is the file's line count after that row and every row above it: rows 1–3 are measured, the rest are estimates (lines removed, less about two import lines per slice). Every seam is module-scope; no seam closes over another component's state. | # | Seam | Base lines | Size | Reads from the rest of the module · hooks it calls | Imported by | Risk | Left | |---|---|---|---|---|---|---|---| | 1 | Nav item inspector (`StudioNavItemInspector`, nav target helpers, `StudioPicker`) | landed | 978 | — | — | **landed in #30002** | 11,884 (measured) | | 2 | `PackageSwitcher` with `PACKAGE_LIST_TOAST_ID` and `STUDIO_LANDING_PATH` | landed | 663 | — | — | **landed in #30008** | 11,205 (measured) | | **3** | **Interfaces nav tree rail: `NavTree`, `NavTreeGroup`, `ContributedNavRows`, `OffNavSections`, the two rail contexts, `navKindName`, `navOpenedItems`, with `KIND_ICON` / `navIcon`** | 663–679, 2249–2673 | **442** (17 + 425) | nothing · `useMetadataLocale`, `useNavTargetLabel`, `useOpenContributor` | `InterfacesPillar` only | low: self-contained | **10,757** (measured, this PR) | | 4 | Flow rail helpers: `FlowRuntimeState` … `readFlowCopiedArrival`, incl. `FlowStatusDot`, glyph tables, `pairIdOf` | 8061–8408 | 348 | nothing · no hooks | `AutomationsPillar`; `AccessPillar` (`pairIdOf`); 2 tests import `FlowStatusDot` | low: pure helpers plus one prop-only component; carries the spec-symbol ledger's `FlowRuntimeState` site | ≈ 10,411 | | 5 | Pillar kit: `useHeldEditReport`, `confirmDropUnsavedWork`, `findTakenIdentifier`, `CreateTakenNotice`, `StudioHeldNotice`, the "Show me" focus helpers, `announceDraftSaved`, `StudioReadOnlyLink`, `StudioBaseCustomization` | 508–647, 695–773, 786–816, 818–865, 2681–2706 | 320 | nothing · `useStudioTabRowFold` | all four pillars and `StudioDesignSurfaceBody` | low: five non-contiguous blocks; lets rows 7–10 move without an import cycle | ≈ 10,093 | | 6 | Data pillar helpers (`nextFieldName` … `formPreviewSource`, the OWD option keys) | 447–464, 5583–5823 | 259 | nothing · no hooks | `DataPillar` only; 1 test imports `renderStudioGridList` | low: pure helpers | ≈ 9,836 (under 10,000) | | 7 | `AccessPillar` with its arrival reader and `StudioNameLabel` | 2223–2247, 10262–11203 | 967 | rows 4, 5 · 11 hooks | `StudioDesignSurfaceBody`; 7 test files | medium: whole stateful component; after rows 4–5 | ≈ 8,871 | | 8 | `AutomationsPillar` | 8410–10260 | 1,851 | rows 4, 5 (and `StudioPicker`, row 1) · 21 hooks | `StudioDesignSurfaceBody`; 23+ test files | medium: after rows 4–5 | ≈ 7,022 | | 9 | `DataPillar` | 5827–8059 | 2,233 | rows 2, 5, 6 (and `StudioPicker`) · 17 hooks | `StudioDesignSurfaceBody`; 38+ test files | medium: holds three `clickableNonControlCensus-13461` rows keyed by this file | ≈ 4,791 | | 10 | `InterfacesPillar` with `interfacesSaveBody`, `leafKeyOf`, `customizeItemKey` | 488–506, 2678–2679, 2708–5581 | 2,895 | rows 1, 3, 5 · 23 hooks | `StudioDesignSurfaceBody`; 29+ test files | medium: close to the 3,000-line mark; holds one census row | ≈ 1,898 | | 11 | `StudioDesignSurface` / `StudioDesignSurfaceBody`, `PILLARS`, `METADATA_AREA_TAB`, `PILLAR_FOR_SURFACE_TYPE`, publish-refusal and backfill helpers | 423–446, 465–487, 649–661, 775–784, 867–2221 | ≈ 1,425 | rows 2, 5, 7–10 · 11 hooks | `index.ts`, the console's lazy Studio route, 51 test files | stays: this is the file | — | Unchanged from #30002: nothing below the whole-component level of rows 7–11 moves without a logic change, and none of it is needed, because rows 3–6 bring the file under 10,000. Serial order: rows 4–6 in any order, then rows 7–10 only after row 5. ## What changes - `ui/packages/app-shell/src/views/studio-design/InterfacesNavRail.tsx` (new): row 3, base lines 663–679 and 2249–2673, each byte for byte, under a copyright line, a module comment and the imports the two blocks read. The block-level edits are `export` on the eleven names `InterfacesPillar` reads: `navIcon`, `navKindName`, `InterfacesRailMarksContext`, `InterfacesNavContributionsContext`, `ContributedNavRows`, `NavTree`, `OFF_NAV_TYPES`, `OffNavType`, `itemKeyOf`, `navOpenedItems`, `OffNavSections`. `KIND_ICON`, `navTreeHoldsActive`, `NavTreeGroup` and `OffNavSection` are read only inside the module and stay unexported. The two contexts move as they were (`React.createContext` with the same defaults); `InterfacesPillar` still provides them, so no logic changed. - `ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx`: both blocks are gone. It imports the eleven names from the new module beside the slice 2 import, and drops the import bindings only the rail used (`Badge`, `Separator`, `FileText`, `BarChart3`, `Table2`, `Folder`, `Compass`, `Link as LinkIcon`, `Puzzle`, `BookOpen`, `Minus`, `listViewTargetOf`, `isSameSurface`, `inMenuOrder`, `isNavEntryType`, `NavTargetLabelResolver`, `RailCustomizations`, `NavContributionPlacement`, `TopLevelContributedNav`). Nothing in the row was exported before, and no file outside `InterfacesPillar` reads any of it (the other mentions are comments in `servedNavText.ts`, `AppNavCanvas.tsx` and `StudioDesignSurface.railLabels.test.tsx`), so there is no re-export. - By-path readers (ruling 6597555122 item 3): - `ui/packages/app-shell/src/__tests__/homeRecoveryRedirectsFollowDeclaration-7373.test.ts`: the Studio site's `alsoReads` gains `InterfacesNavRail.tsx`. - `ui/scripts/__tests__/spec-symbol-ledger-app-shell-7265.test.ts`: `SITES` gains `InterfacesNavRail.tsx`. - Two test comments now name the module that holds the text they cite, which moved in slice 2 (#30008 flagged them): `PackagesPage.confirmFlows-11784.test.tsx` ("`PackageSwitcher`'s eviction in `PackageSwitcher.tsx`") and `packages-io.inflightShare-11797.test.tsx` ("the `PACKAGE_LIST_TOAST_ID` doc in `PackageSwitcher.tsx`"). Comment text only. - `.changeset/29988-studio-interfaces-nav-rail-module.md`: `@object-ui/app-shell` patch, "internal: move-only". ## Tests No behaviour changes, so no new behaviour test. The existing suites run unchanged; results are in Verification. ## Acceptance notes - **Readers re-grepped.** `git grep` of `StudioDesignSurface.tsx` / `studio-design/StudioDesignSurface` over `scripts/**`, `ui/scripts/**` and every ui test file finds the same by-path readers as slices 1–2. The two whole-text scanners gained the new module (above). `clickableNonControlCensus-13461` keys rows by file and handler; none of its rows is a rail handler (the rail's clicks are on `StudioRailRow`, a control), so it needs no edit. `openPathObjectReaders-12811` classifies readers of `useMetadata().objects`; the rail reads none. The directory walkers (`noRetiredSetupLink-28578`, `noSetupMetadataLink-28578`, `CreateItemDialog.oneLabel-11787`) pick the new file up by themselves. No gate under `scripts/**` names the file. - **Checklist rows re-read, none moved.** Every `StudioDesignSurface.tsx` citation under `docs/qa/` was read against the moved blocks, and none cites rail text: the Interfaces citations name the pillar's app load, its save into the pair and its unsaved-work guard, and the dashboards citation quotes the file's header comment ("Interfaces: the real App navigation tree"), all of which stay. So `docs/qa/` is untouched and no revision row is added; `check-platform-checklist` still exits 0 full and with `--changed-from origin/main`. - **Serial check.** No open PR touched `StudioDesignSurface.tsx`, the new module, either extended reader or the two commented test files when the seat claimed (round 3 Claim) or right before this push (see Verification). The branch was restarted from `origin/main` after #30008 merged and is rebased on `eee41490`, which touches only `packages/spec/`. ## Verification <details><summary>Move-only proof: byte-compare and the moved-lines diff</summary> ``` B=ui/packages/app-shell/src/views/studio-design git show eee4149:$B/StudioDesignSurface.tsx | sed -n 663,679p > /tmp/blockA git show eee4149:$B/StudioDesignSurface.tsx | sed -n 2249,2673p > /tmp/blockB sed -n 47,63p $B/InterfacesNavRail.tsx | sed 's/^export const navIcon = /const navIcon = /' | cmp /tmp/blockA - tail -n +65 $B/InterfacesNavRail.tsx | sed -E 's/^export (function|const|type) /\1 /' | cmp /tmp/blockB - ``` Both silent (identical): block A 17 lines, sha256 `44978201e446c1bb…`; block B 425 lines, sha256 `73d3528850a14d73…`. `git diff --color=always --color-moved=zebra eee4149 HEAD -- ui/packages/app-shell/src/views/studio-design/`: 440 lines removed and 440 added are coloured as moved (the block lines less the eleven `export` lines, plus import specifier lines that recur). Apart from import lines, the new module's header comment and the `packages-io` comment line, every line NOT coloured as moved: ``` +export const navIcon = (type?: string): LucideIcon => KIND_ICON[type ?? ''] ?? Compass; + +export function navKindName(node: Pick<NavNode, 'type' | 'viewName'>, locale: string): string { +export const InterfacesRailMarksContext = React.createContext<RailCustomizations | null>(null); +export const InterfacesNavContributionsContext = React.createContext<NavContributionPlacement>({ byGroup: new Map(), top: [] }); +export function ContributedNavRows({ +export function NavTree({ +export const OFF_NAV_TYPES = ['page', 'dashboard', 'report', 'view'] as const; +export type OffNavType = (typeof OFF_NAV_TYPES)[number]; +export const itemKeyOf = (type: string, name: string): string => `${type}:${name}`; +export function navOpenedItems(nodes: readonly NavNode[], locale: string): Set<string> { +export function OffNavSections({ -const navIcon = (type?: string): LucideIcon => KIND_ICON[type ?? ''] ?? Compass; - -function navKindName(node: Pick<NavNode, 'type' | 'viewName'>, locale: string): string { -const InterfacesRailMarksContext = React.createContext<RailCustomizations | null>(null); -const InterfacesNavContributionsContext = React.createContext<NavContributionPlacement>({ byGroup: new Map(), top: [] }); -function ContributedNavRows({ -function NavTree({ -const OFF_NAV_TYPES = ['page', 'dashboard', 'report', 'view'] as const; -type OffNavType = (typeof OFF_NAV_TYPES)[number]; -const itemKeyOf = (type: string, name: string): string => `${type}:${name}`; -function navOpenedItems(nodes: readonly NavNode[], locale: string): Set<string> { -function OffNavSections({ - ``` (the added blank line joins the two blocks in the new module; the removed blank lines are the separators after each cut block). </details> <details><summary>Declaration files (ruling 6348891160's bar)</summary> Base declarations emitted from the base source into a scratch `outDir` under the lock, tree restored (`git diff HEAD` empty). Of the 701 base `.d.ts` files, 700 are byte-identical at head, `StudioDesignSurface.d.ts` included (sha256 `d0f9665b1e9ed6fc…`, the same as at slice 2): the rail was never exported, so no declaration moved. The one difference is `chrome/ErrorBoundary.d.ts`, where the members of the `render()` return union print in another order; with every union's members sorted (the ruling's normalisation) the two files are identical. The one addition is `views/studio-design/InterfacesNavRail.d.ts` / `.js`, reachable from no entry in the package's `exports` map (`"."` only). Declared-surface digest of `dist/index.d.ts` plus `StudioDesignSurface.d.ts`, root path masked: 245 exports, `ce63368a1cf8becf…` on both sides. </details> <details><summary>Build, type-check, lint and tests (head 376a48cf; the rebased head 36954c6 has a byte-identical ui/ and changeset, checked with git diff --quiet)</summary> All under scripts/pm/os-verify-lock.sh, one locked chain: VERDICT command-exit 0. - npx turbo run build --filter='@object-ui/app-shell...' --concurrency=2: exit 0. - pnpm --dir ui --filter @object-ui/app-shell type-check (tsc --noEmit, then tsconfig.test.json): exit 0. - pnpm --dir ui run type-check:scripts: exit 0. - npx eslint on the two source files, the 7373 test and PackagesPage.confirmFlows-11784, from packages/app-shell: exit 0. - pnpm --dir ui exec vitest run --maxWorkers=3 packages/app-shell/src/views/studio-design/ plus the 7373 test, PermissionMatrixEditor.saveSurface-12725, builtinDesigners.lateArrival-11939, observableRegistries-11939, PackagesPage.confirmFlows-11784, clickableNonControlCensus-13461 and spec-symbol-ledger-app-shell-7265: exit 0, Test Files 329 passed (329), Tests 2446 passed (2446). - Narrowed, and declared: the full @object-ui/app-shell suite does not finish locally on the shared box; the run covers every test in the moved file's directory and every test outside it that imports the file, reads it by path or names the moved rail. CI's Test UI runs the rest. - node scripts/check-platform-checklist.mjs: exit 0; with --changed-from origin/main: exit 0. </details> <details><summary>Ablation of the two extended readers (one-time, through scripts/ablation-replace.mjs; tree restored, git diff HEAD empty)</summary> The moved rail holds neither reader's needle, so each leg plants one in the new module and then takes the module out of the reader's list. | Leg | Mutation | Expected | Observed | |---|---|---|---| | B0 | none: the 7373 test as in this PR | green | exit 0: 15 passed | | B1 | a `'/home'` literal planted in InterfacesNavRail.tsx, alsoReads as in this PR | red on the Studio site | exit 1: "does not hard-code the launcher path" fails for the Studio site; 1 failed, 14 passed | | B1b | same mutation, InterfacesNavRail.tsx removed from the Studio site's alsoReads | green, i.e. blind | exit 0: 15 passed | | B2 | a local interface ObjectLike added to InterfacesNavRail.tsx, SITES as in this PR | red | exit 1: "rule 1 finds no spec-named local declaration in ...InterfacesNavRail.tsx" fails; 1 failed, 21 passed | | B2b | same mutation, the InterfacesNavRail.tsx SITES row removed | green, i.e. blind | exit 0: 21 passed | Each mutation's landing was proven by the tool (anchor count 1 to 0, blob hashes) and by a grep count of the changed text. </details> <details><summary>Derived gates: node scripts/pm/dispatch-gates.mjs --commands, run at b4129e71, whose tree head 36954c6 keeps (the amend rewrote only the commit trailer) (35 commands)</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 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-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.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:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: 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:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: 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 ``` node scripts/pm/dispatch-gates.mjs --ran: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED". No docs/adr or docs/qa path changed, so the ADR-link and platform-checklist families are not derived this time; check-platform-checklist was still run by hand (Build, type-check, lint and tests, above). </details> <details><summary>Serial check before push</summary> The 17 open PRs at 2026-11-04T05:50Z: each PR's changed files were read (seven PRs from their fetched heads against `origin/main`, three-dot; #29979, #29866 and the eight `pm:mailbox` PRs through the PR file list or their fetched head; every mailbox PR lists no file). None touches StudioDesignSurface.tsx, InterfacesNavRail.tsx, homeRecoveryRedirectsFollowDeclaration-7373.test.ts, spec-symbol-ledger-app-shell-7265.test.ts, PackagesPage.confirmFlows-11784.test.tsx or packages-io.inflightShare-11797.test.tsx. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzedfGwVcebpN5t3ETE)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzedfGwVcebpN5t3ETE Co-authored-by: Claude <noreply@anthropic.com>
1 parent eee4149 commit 9d1a465

7 files changed

Lines changed: 518 additions & 465 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
internal: move-only — the Studio Interfaces pillar's navigation rail moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 3)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
No behaviour change. The rail Studio's Interfaces pillar draws for an app's navigation (the nav tree and its groups, the rows a customization package contributes, the off-nav sections for pages, dashboards, reports and views, the two rail contexts, the kind names and kind icons) now lives in `views/studio-design/InterfacesNavRail.tsx`. The moved code is byte-identical apart from its imports and eleven `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged.

‎ui/packages/app-shell/src/__tests__/homeRecoveryRedirectsFollowDeclaration-7373.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const RECOVERY_SITES: ReadonlyArray<{
139139
alsoReads: [
140140
'packages/app-shell/src/views/studio-design/StudioNavItemInspector.tsx',
141141
'packages/app-shell/src/views/studio-design/PackageSwitcher.tsx',
142+
'packages/app-shell/src/views/studio-design/InterfacesNavRail.tsx',
142143
],
143144
site: 'Studio: deleted-package eviction + header Home',
144145
expression: /const homePath = useHomePath\(\)/,

‎ui/packages/app-shell/src/views/metadata-admin/PackagesPage.confirmFlows-11784.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ describe('PackageDetailSheet — no native dialog on these three paths (objectui
392392
describe('Console Packages page — a delete there stays on the page (objectui#11784)', () => {
393393
/**
394394
* Studio's return to its landing after a delete is the Studio HOST's
395-
* (`PackageSwitcher`'s eviction in `StudioDesignSurface.tsx`), not the
395+
* (`PackageSwitcher`'s eviction in `PackageSwitcher.tsx`), not the
396396
* sheet's. The same sheet serves this console page, whose `onChanged` only
397397
* reloads the list; `/studio` is not a destination here.
398398
*/

0 commit comments

Comments
 (0)