Skip to content

fix(app-shell): 把 System Settings 指向系统枢纽 /apps/setup/system,而非空态自己的裸 URL - #3608

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3590-settings-btn-target
Aug 7, 2026
Merged

fix(app-shell): 把 System Settings 指向系统枢纽 /apps/setup/system,而非空态自己的裸 URL#3608
yinlianghui merged 1 commit into
mainfrom
claude/issue-3590-settings-btn-target

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3590

问题

AppContent 只在 isSystemRoute 成立时挂载系统枢纽,而 isSystemRoute = location.pathname.includes('/system')。裸 /apps/setupisSetupRoute 外不满足任何伪路由判定,于是零应用部署下直接落回 !activeApp && !isCreateAppRoute && !isSystemRoute && !isMetadataRoute 这条守卫 —— 它就是「No Apps Configured」空态自己的 URL,所以空态上的 go-to-settings-btn 点完只是把同一屏重渲染了一遍。

/apps/setup/system两个分支里都能解析:无 activeApp 时走 extraRoutesNoApp,有 activeApp 时走 extraRoutes —— console 把同一份 systemRoutes(首项即 path="system"SystemHubPage)同时传给了这两个 prop。

三个调用点,逐点测量与处置

1. console/AppContent.tsxgo-to-settings-btn(主修点,已改)
该按钮只在空态分支里渲染,而空态只在无 activeApp 时出现 —— 也就是说它的旧目标恒等于它自身所在屏的 URL。100% 死键,无歧义。

2. layout/AppSidebar.tsxsystemFallbackNavigation.sys-settings(已改)
该簇只在 activeApp 为假时渲染,而此处 activeApp = matched || activeApps[0],仅当部署里没有任何 active+visible 应用时才为假 —— 恰好就是零应用部署。也就是说,这个簇的头项在这个簇唯一存在的场景里是死的。簇内其余 8 项本就全部拼 /apps/setup/system/...,头项是唯一的异类。新测试直接断言其渲染出的 href。

3. layout/UnifiedSidebar.tsxhomeNavigation Administration 簇里的 sys-settings(已改,但当前休眠 —— 见下)
语义与 2 相同:该簇的代码注释写明它是为「fresh env(还没有 app)在 /home 上仍有真菜单」而加的,而 resolveLandingPath([]) 正是把零应用用户送到 /home。同一个头项、同一个裸 URL、同一条死路,因此同改。

⚠️ 越界测量披露:改的过程中量到该项今天根本渲染不出来UnifiedSidebar 只有一个 context === 'app' && activeApp 三元(约 437 行);只有 app 分支渲染 NavigationRenderer(即会递归进 type: 'group' 子项的那个),home 分支是手写的 homeNavigation.map(item => Link to={item.url || '/home'})不递归。于是整个 9 项 Administration 组被压成一个链接,而组本身没有 url,回退到 /home —— 管理员已经站着的那一页。本 PR 仍然把常量改对,好让日后修「压平」的人不会连带发布一条死的 /apps/setup 链接;测试文件里为此写的是一条诚实的 MEASUREMENT 钉,钉住「压平」这个事实本身,而不是去断言一个永远渲染不出的 href。压平问题已另单记录(#3609)。

测试

扩展 #3588 新增的 console/__tests__/AppContent.noAppsCta.test.tsx(未另起文件),并新增一个侧边栏目标测试文件 layout/__tests__/systemNavSettingsTarget.test.tsx

被重写的那条钉:原 leaves the sibling go-to-settings CTA on its absolute /apps/setup target(断言点击后 pathname === '/apps/setup'create-first-app-btn 仍在)。它的注释本就写明「pinning, not blessing」—— 存在的意义只是证明 #3573 没碰这颗按钮。本 PR 整条替换为断言落到系统枢纽,并额外要求空态必须消失(只断 URL 的话,一个什么都不渲染的目标同样会绿)。

harness 里按 console 宿主的写法接上了 extraRoutesNoApp,所以「点击 → 枢纽真的挂载」是端到端量出来的,不是只比对字符串。

反向验证(方向在跑之前先写下):把三处源码改动 git apply -R 掉、只保留新测试 —— 预测 3 红 6 绿,实测完全一致:

× AppSidebar: the no-active-app fallback cluster heads at the system hub
× sibling go-to-settings CTA opens the system hub instead of looping onto this same empty state
× reaches the SAME system hub from a deeper splat URL (absolute target, depth-independent)
TestingLibraryElementError: Unable to find an element by: [data-testid="system-hub-page"]
expect(element).toHaveAttribute("href", "/apps/setup/system")
 Test Files  2 failed (2)
      Tests  3 failed | 6 passed (9)

其中 UnifiedSidebar 那条 MEASUREMENT 钉按预测保持绿色:它钉的是「home 分支压平组」,与 URL 常量无关 —— 这正是 UnifiedSidebar 那处改动休眠的原因。把它报成红色会是编造。

改动后:

 Test Files  2 passed (2)      # 定向两个文件
      Tests  9 passed (9)

 Test Files  41 passed (41)    # packages/app-shell/src/{console,layout} 全量
      Tests  215 passed (215)

pnpm --filter @object-ui/app-shell type-check   # tsc --noEmit + typetests,无输出即通过
node scripts/check-control-bytes.mjs            # OK (3640 tracked text files)

越界发现(只报不改,已另立 unassigned 单)

备注

SystemHubPage 自身标了 @deprecated(「新管理面应做成 nav 项而非手写卡片」)。这不影响本次改向:它仍是 system 路由上唯一挂载的页面,也是整个 sys-* 簇共同的前缀;而零应用恰恰是「元数据驱动左侧菜单」尚不存在、因而这个枢纽正当其用的场景。

…f the bare setup URL (#3590)

The system hub mounts only under `isSystemRoute`, which keys on a `/system`
path segment. A bare `/apps/setup` matched no pseudo-route except
`isSetupRoute`, so on a zero-app deployment it fell straight back into the
"No Apps Configured" guard — it IS that empty state's own URL, and the
empty state's `go-to-settings-btn` re-rendered the screen it sits on.

Retargets three call sites at `/apps/setup/system`, which resolves in both
branches (`extraRoutesNoApp` with no active app, `extraRoutes` once one
exists), and which every sibling entry of both `sys-*` clusters already
prefixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 7, 2026 3:49pm

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CAE30s93.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 480.72KB 105.64KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 137.51KB 35.11KB
fields (index.js) 230.87KB 56.83KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.03KB 28.88KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 186.61KB 49.34KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: 零应用空态的另一颗按钮 go-to-settings-btn 同样是死键——/apps/setup 渲染的正是这个空态本身

2 participants