Skip to content

fix(plugin-list): allowAddTab 的「新增 tab」按钮接上会话级行为(objectstack#5236) - #3926

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os5236-addtab-session
Aug 9, 2026
Merged

fix(plugin-list): allowAddTab 的「新增 tab」按钮接上会话级行为(objectstack#5236)#3926
yinlianghui merged 1 commit into
mainfrom
claude/issue-os5236-addtab-session

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5236

背景

packages/plugin-list/src/UserFilters.tsxTabFilters 里,allowAddTab 渲染的「新增 tab」按钮(data-testid="filter-tab-add")带 hover 态与 title="Add filter tab",却没有 onClick,组件也没有任何 onAddTab / addTab 回调入参 —— 一个看起来完全可点、点下去什么都不发生的控件。它比「没有这个按钮」更糟:把「未实现」伪装成「点了没反应的 bug」。

objectstack#5073 把该键提升进 spec 的 UserFiltersSchema 之后这更要紧:键在 JSON Schema、Studio SchemaForm、参考文档三处可见,AI 作者会照着 schema 写 allowAddTab: true,而运行时不兑现 —— declared ≠ enforced 的 UI 变体。

前提复核(对 origin/main 实测,非阅读推断):premise 成立,只是行号漂了 —— 按钮现在在 UserFilters.tsx:778(issue 正文写的 :742)。全仓 git grep origin/main 命中:filter-tab-add 仅 1 处(那个按钮),onAddTab 零命中,allowAddTab 4 处(prop 声明、传入、渲染 + packages/types 的 zod 镜像与其测试),确认无任何回调入参存在。

本 PR 做了什么(维护者裁决 A1,评论 5200253516)

  • 命名输入:点击 add 按钮弹出 Popover(复用该组件筛选 chip 与 More 溢出已在用的 Popover 原语,贴合现有交互习惯),内含名称输入框 + 「Add tab」确认按钮;空白名不可确认,回车等价于点确认。
  • 快照当前筛选:确认后把「此刻已应用的筛选条件」快照进新 tab 并立即选中。tabs 模式下这就是该组件承载的全部筛选状态(组件不拥有第二个筛选面),所以新 tab 复现的正是用户按下 Add 时看到的那批记录。每条 condition 逐条拷贝,预设日后变动不会串到会话级 tab 上。
  • 与预设同栏:新 tab 渲染在同一栏预设之后,可自由选中/切换。
  • 删除入口(带上了):会话级 tab 带一个 x;预设不带 —— 那是元数据。删掉当前选中的会话级 tab 时,按初次挂载的同一优先级回落到作者默认 tab 并重新发出其条件,不会留下「无选中 tab、却仍套用着已删 tab 条件」的状态。实现成本确实低(一个 span 包两个 button,预设那条渲染分支一字未动)。

会话级语义:选了组件态,不是 sessionStorage(理由)

⛔ 不写 sys_metadata、不发任何 API、不碰 web storage —— ADR-0047:end-user 的筛选选择 session-scoped、从不落成元数据,渲染器保持元数据读方

sessionStorage 是可用的,刻意没用:UserFilters 拿不到 object/view 身份,它能自己编出的任何 storage key 都会被同一浏览器 tab 里的所有列表共用,把一个列表的临时 tab 显示到另一个列表的栏上 —— 那是个真 bug,换来的只是刷新后还在。要跨挂载存续,该归已经握着筛选选择会话通道(onSelectionsChangeuf_* URL 参数)、且能按视图分键的宿主;那需要给 UserFiltersProps 加公共入参,是裁决没做的契约决定,不该顺手塞进本单。

合成 tab id(__session_1__,沿用该文件为自己发明的 __all__ 拼法)与其它 tab 切换一样经 onSelectionsChange 上报,宿主镜像的真实性不被撒谎;宿主把它持久到 uf__tab 后下次挂载还回来时,既有的 id 校验找不到该 tab,回落作者默认 —— 会话级 tab 没有走后门跨挂载存活的路径。这条降级被测试钉住了。

测试

新增 packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx(12 例):

  1. 点 add 出命名输入 → 输入名字 → 确认:新 tab 出现在同栏、label 已 trim、处于选中态、onSelectionsChange{ _tab: ['__session_1__'] }onFilterChange 最后一次调用等于创建时刻已应用的条件;空白名时确认按钮 disabled。
  2. 快照是存下来的、不是重算的:切走到别的预设再切回来,重新发出被捕获的那批条件。
  3. 以「All records」为当前 tab 时快照出空条件集。
  4. 回车确认;连加两个 tab 各得自己的 id。
  5. 零元数据写入:mock dataSource(create/update/delete/find/findOne)+ fetch + Storage.prototype.setItem 全部断言 0 次调用。该例断言新 tab 确实出现,再断言这些 0 —— 否则「加不出 tab」也能让 0 断言空转通过(objectstack PR #5046 的空转陷阱)。
  6. 会话级存续语义:unmount 后重新 render(并模拟宿主把 _tab: ['__session_1__'] 从 URL 还回来)→ 会话级 tab 不在了,活动 tab 回落作者默认,重新发出默认条件。
  7. 删除:删当前选中的 → 回落默认并重新发条件;删非选中的 → 已应用条件一动不动(onFilterChange 调用次数不变);预设没有删除入口。
  8. 既有行为回归:allowAddTab: false缺省时都完全不渲染该控件(筛选栏本体照常渲染)。

命令与实测输出:

pnpm exec vitest run packages/plugin-list/src/__tests__/UserFilters.addTab.test.tsx --maxWorkers=2
  Test Files  1 passed (1)       Tests  12 passed (12)

pnpm exec vitest run packages/plugin-list/ --maxWorkers=2
  Test Files  24 passed (24)     Tests  376 passed (376)

pnpm exec turbo run type-check --concurrency=2
  Tasks:    78 successful, 78 total

反向验证(方向是预测的:红)

UserFilters.tsx 整体还原成 origin/main 的死按钮版本再跑新测试:9 红 / 3 绿,与预测一致。

× clicking add opens a naming input; confirming snapshots the applied filters ...
× the snapshot is stored, not recomputed ...
× calls no data-source method, no fetch, and no web storage
× drops the added tab and falls back to the author default ...
TestingLibraryElementError: Unable to find an element by: [data-testid="filter-tab-add-content"]
 Tests  9 failed | 3 passed (12)

三个绿的是两条 allowAddTab 关闭回归(本就该两向都绿,它们钉的是既有行为)+「预设没有删除入口」那条(负向断言,两向都绿;它防的是将来给预设误加删除入口,不是钉本次接线)。这里如实记一笔:零元数据写入那条是红的,红在它自己的非空转前置断言上 —— 正是它该红的位置。

范围之外(未做,已确认)

  • spec 侧 UserFiltersSchema.allowAddTab.describe() 从「仅承诺渲染入口」升回真实语义 —— 裁决已归 spec 车道,渲染器不依赖该文案。本仓 packages/types 的 zod 镜像写的是 'Allow adding new tabs',与新行为无冲突,未动。
  • 没碰 app-shell/src/views/metadata-admin/widgets.tsx 的 Studio 编辑期 addTab(filter-mode-add-tab)—— 那是另一族。

Changeset

.changeset/userfilters-allowaddtab-session-tabs-os5236.md,@object-ui/plugin-list: patch。定 patch 而非 minor 的理由:公共 API 面零变化(UserFiltersProps 一字未动,没有新增入参),用户可见的变化是一个已发布但失灵的控件开始按其 title 与 spec 声明工作,属行为补全/修复;且按仓内约定 breaking 也只标 minor、major 被 CI 禁掉,patch 与本仓「fix(...) + patch」的既有惯例一致。


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

`TabFilters` 里 `allowAddTab` 渲染的按钮带 hover 态与 `title="Add filter tab"`,
却没有 `onClick`,组件也没有任何新增 tab 的回调入参 —— 一个看起来完全可点、点下去
什么都不发生的控件,它把「没实现」伪装成「点了没反应的 bug」。objectstack#5073 把
该键提升进 spec 的 `UserFiltersSchema` 之后这更要紧:键在 JSON Schema、Studio
SchemaForm 与参考文档三处可见,作者照着写 `allowAddTab: true`,运行时不兑现。

按维护者裁决 A1 落地:点击弹出命名输入(复用该组件筛选 chip 与 More 溢出已在用的
Popover 原语),确认后把「此刻已应用的筛选条件」快照成一个新 tab,与预设同栏展示
并立即选中。会话级 tab 带删除入口;预设不带 —— 那是元数据。删掉当前选中的会话级
tab 时按初次挂载同一优先级回落到作者默认 tab,不会留下「无选中 tab 但仍套用着已删
tab 条件」的状态。

会话级语义选的是**组件态**,不写 `sys_metadata`、不发任何 API、不碰 web storage
(ADR-0047:end-user 的筛选选择 session-scoped、从不落成元数据)。`sessionStorage`
是可用的,刻意没用:`UserFilters` 拿不到 object/view 身份,它能自己编出的任何 key
都会被同一浏览器 tab 里的所有列表共用,把一个列表的临时 tab 显示到另一个列表的栏
上。真要跨挂载存续,归已经握着筛选选择会话通道(`onSelectionsChange` → `uf_*` URL
参数)、且能按视图分键的宿主。合成 tab id 与其它 tab 切换一样经 `onSelectionsChange`
上报,宿主镜像进 URL 后下次挂载还回来时,既有的 id 校验找不到该 tab,回落作者默认。

公共 API 未变:`UserFiltersProps` 一字未动,`allowAddTab: false` 与缺省仍然完全不
渲染该控件。

Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 9, 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 9, 2026 6:14am

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-DUA-Fz4p.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) 482.53KB 106.37KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 230.97KB 56.74KB
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) 27.59KB 6.63KB
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.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.63KB 59.02KB
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) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 108.07KB 26.13KB
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

Copy link
Copy Markdown
Collaborator Author

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#5236(v17,维护者裁决 A1)

实物核验:head b6450565e,3 文件;主 diff 审读通过 —— Popover 命名输入、快照当前已应用筛选、合成 __session_N__ id 与预设防撞、会话 tab 带删除入口而预设不带(元数据不可删)、删除当前选中回落作者默认与初挂载同优先级、restored 的陈旧 _tab 自然降级。CI 亲读终态:20 检查全 completed、0 失败。

验收要点:

  • 会话级语义选组件态而非 sessionStorage 的裁量采纳:UserFilters 无 object/view 身份,自编 storage key 会把一个列表的临时 tab 串到同浏览器 tab 的其它列表栏上;跨挂载存续归握有 uf_* URL 通道、能按视图分键的宿主 —— 那是裁决未做的契约决定,不塞进本单是对的。
  • 「零元数据写入」测试带非空转前置断言(先证 tab 出现,再证 dataSource/fetch/Storage.setItem 全零),避开了空转绿。
  • 新 trigger 补 type="button"(Combobox trigger 按钮缺省 type —— 在表单内点击会以 submit 语义提交表单 #3344 族)顺带正确。
  • dev 自查出 curl 无 token 轮询 check-runs 的假绿并改用授权工具复核 —— 与本 PM 工具库中同款陷阱一致,处置正确。
  • ADR-0047 合规:零 sys_metadata 写入;spec 侧 .describe() 升级半边由 PM 向 spec 车道立联动单。

范围外 finding objectstack#6952(三个既有 button 缺 type)立单规范,冻结期不派。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 06:24
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit cf5be4e Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os5236-addtab-session branch August 9, 2026 06:24
Sign up for free to 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.

objectui: allowAddTab 渲染出的「新增 tab」按钮没有 click handler —— 契约刚声明的能力只交付了一半

2 participants