Skip to content

feat(components,plugin-detail,app-shell): URL-driven record detail tabs — ?tab= survives remounts (#2257) - #2267

Merged
os-zhuang merged 1 commit into
mainfrom
feat/detail-tab-url-2257
Jul 5, 2026
Merged

feat(components,plugin-detail,app-shell): URL-driven record detail tabs — ?tab= survives remounts (#2257)#2267
os-zhuang merged 1 commit into
mainfrom
feat/detail-tab-url-2257

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

概述

修复 #2257:详情页 tab 条被子树重挂载重置回 Details。按 ADR-0054 C3 把 active tab 变成 URL 可寻址状态(?tab=<value>),同时补齐 framework#2604 验收中缺的最后一项(子表保存后父详情 tab 保留)。

根因(浏览器实测 + StrictMode A/B 对照,结论已更新到 #2257)

active tab 只活在非受控 Radix state 里(PageTabsRendererdefaultValue + legacy DetailView autoTabs),任何子树重挂载即清零。触发面分两类:

  • 生产真实:key={refreshKey} / key={actionRefreshKey} 的"保存/action 后整树重建"模式;
  • 仅 dev(StrictMode):任何 URL search 变化都会重挂载 RecordDetailView —— 同一操作序列在 StrictMode OFF 下零重挂载、tab 保留(mount 探针 A/B 实证),故 ?recordId=/?form= 引发的 tab 丢失是 dev 伪影,生产不受影响。

两类触发 URL 化后全部免疫——状态不在组件里,重挂载无所谓。

变更

  • plugin-detail buildDefaultTabs:tab item 携带稳定语义 value(details / related:<child>(primary 独占 tab)/ related / activity / history)——索引合成值不能当 URL token(related list 增减时会漂移指向别的 tab)。
  • components PageTabsRenderer:优先用 item.value(无则回退索引值,兼容 authored schema);接受宿主 schema.defaultTab(仅当命中真实 tab 才生效)+ schema.onTabChange。components 包保持 router 无感——只暴露口子。
  • app-shell RecordDetailView:从 ?tab= 恢复、onTabChangereplace 写回(切 tab 决不压 history——Back 必须继续承担"关 ?form= overlay"的语义,不能在 tab 间倒带);经纯函数 withPageTabsUrlSync 注入 page 树(clone-on-write,决不改动 authored/memoized 的页面 schema,含 6 个单测)。
  • legacy DetailView autoTabs 同一契约接线(value 校验后才采用)。
  • types:DetailViewSchema.defaultTab/onTabChangePageTabsItem.value 声明。

验证

浏览器端到端(showcase,StrictMode 开启的最坏情况):

  • ✅ 切到 Related → URL 变 ?tab=related
  • ✅ 打开子表 overlay → Esc → tab 仍是 Related(此前必回 Details)
  • ✅ 子表新建保存 → tab 保留 + 子表刷出新记录(framework#2604 验收缺口补齐)
  • ?tab=related 深链直达;?tab=nope 回退 Details

单测 +9(稳定 value × 布局变体 × 注入器身份/不可变);3 包全量 1365 通过;turbo type-check 32/32。changeset 已附(fixed group minor)。

Closes#2257 · Refs framework#2604 · ADR-0054

🤖 Generated with Claude Code

https://claude.ai/code/session_019zKcAtbtxuF9SXYgSzjk9v


Generated by Claude Code

…bs — ?tab= survives remounts (#2257)
Root cause (browser-diagnosed with mount probes, StrictMode A/B):
- The active tab lived ONLY in uncontrolled Radix state (PageTabsRenderer
defaultValue + DetailView autoTabs), so any subtree remount reset it.
- Real production triggers: the refreshKey / actionRefreshKey save-refresh
keys remount the whole detail tree. Dev-only trigger: under StrictMode
ANY URL search change remounts RecordDetailView (zero remounts with
StrictMode off — same sequence verified both ways).
Fix — the tab is URL-addressable state (ADR-0054 C3):
- buildDefaultTabs emits stable semantic values (details /
related:<child> / related / activity / history); index-derived values
can't be URL tokens (they shift when the item list changes).
- PageTabsRenderer honors item.value + host defaultTab (validated) +
onTabChange; index fallback kept for authored schemas.
- RecordDetailView restores from ?tab= and writes back with replace (tab
switches never stack history — Back must keep closing ?form overlays,
not page through tabs), injected via withPageTabsUrlSync (pure,
clone-on-write — authored/memoized page schemas are never mutated).
- Legacy DetailView autoTabs wired to the same defaultTab/onTabChange
contract with value validation.
Browser-verified (showcase, StrictMode ON — worst case): Related tab +
child overlay open/Esc → tab preserved; child create SAVE → tab preserved
AND subtable shows the new record (closes the #2604 acceptance gap);
?tab=related deep link lands on Related; ?tab=nope falls back to Details.
Unit: +9 tests (stable values, injector identity/clone-on-write); suites
1365 green; turbo type-check 32/32.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019zKcAtbtxuF9SXYgSzjk9v
@vercel

vercelBot commented Jul 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredJul 5, 2026 7:06am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)59.5 KB350 KB
Entry fileindex-Dla2CjUw.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)7.51KB2.72KB
app-shell (runtime-config.js)4.72KB1.69KB
app-shell (types.js)0.01KB0.04KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)18.34KB3.80KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)4.79KB1.88KB
auth (LoginForm.js)9.55KB3.36KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.63KB2.15KB
auth (SocialSignInButtons.js)8.89KB3.61KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)26.07KB6.30KB
auth (createAuthenticatedFetch.js)3.93KB1.55KB
auth (index.js)1.75KB0.76KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.29KB0.82KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.42KB0.96KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)437.19KB93.97KB
core (index.js)1.65KB0.59KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)109.66KB27.05KB
fields (index.js)185.40KB45.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.31KB0.67KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)21.15KB4.68KB
i18n (useSafeTranslation.js)2.68KB0.98KB
layout (index.js)36.92KB10.17KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)4.42KB1.27KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)1.77KB0.77KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)5.09KB1.84KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.46KB1.03KB
permissions (evaluator.js)4.00KB1.23KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.50KB0.70KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.10KB12.33KB
plugin-charts (index.js)46.18KB13.02KB
plugin-chatbot (index.js)173.06KB41.28KB
plugin-dashboard (index.js)108.28KB26.86KB
plugin-designer (index.js)213.56KB42.96KB
plugin-detail (index.js)203.63KB49.07KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)102.48KB24.90KB
plugin-gantt (index.js)136.67KB33.88KB
plugin-grid (index.js)165.48KB43.77KB
plugin-kanban (index.js)48.15KB12.94KB
plugin-list (index.js)97.89KB23.09KB
plugin-map (index.js)16.78KB5.23KB
plugin-markdown (index.js)13.65KB4.67KB
plugin-report (index.js)37.83KB9.97KB
plugin-timeline (index.js)25.37KB7.20KB
plugin-tree (index.js)8.21KB2.76KB
plugin-view (index.js)85.78KB20.89KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.55KB0.67KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)3.19KB1.38KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)18.23KB5.97KB
react (index.js)0.76KB0.42KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)2.16KB0.94KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
tenant (TenantContext.js)0.31KB0.25KB
tenant (TenantGuard.js)1.04KB0.43KB
tenant (TenantProvider.js)2.76KB0.98KB
tenant (TenantScopedQuery.js)0.77KB0.44KB
tenant (index.js)0.75KB0.38KB
tenant (resolver.js)2.64KB0.76KB
tenant (useTenant.js)0.50KB0.32KB
tenant (useTenantBranding.js)0.62KB0.39KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)0.77KB0.41KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)1.54KB0.68KB
types (layout.js)0.20KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.26KB1.96KB
types (tenant.js)0.20KB0.18KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)0.75KB0.46KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detail-page tab strip resets to "Details" on any URL search-param change (uncontrolled PageTabsRenderer) — make tab state URL-driven (ADR-0054 C3)

2 participants

@os-zhuang@claude