Skip to content

fix(plugin-list): UserFilters buttons declare type="button" (objectstack#6952) - #3948

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6952-userfilters-button-type
Aug 9, 2026
Merged

fix(plugin-list): UserFilters buttons declare type="button" (objectstack#6952)#3948
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6952-userfilters-button-type

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#6952

改了什么

packages/plugin-list/src/UserFilters.tsx 里 issue 点名的三处 button 元素补上 type="button"(以当前 origin/main = ebb579dbb 实况逐一排查,行号已漂移):

现行号是什么改前实际渲染的 type处置
:511DropdownFilters 的 chip 触发器(filter-badge-*)"button"(Radix 供给)补显式声明
:672溢出触发器(user-filters-more)"button"(Radix 供给)补显式声明
:855tabs 模式预设 tab(filter-tab-*)null(即 submit)真修
:883:891:913会话 tab / 移除 / add 触发器"button"已有,不动

全文件 git grep 共 6 个 button 元素,改后 6/6 都显式声明 type="button"

重要:issue 前提只成立三分之一(实测,非阅读推断)

issue 正文断言三处「HTML 默认 submit,在 form 元素内会提交表单」。反向验证推翻了其中两处:把三行 type="button" 撤掉重跑,5 条新测试只红 2 条 —— chip 与溢出触发器仍然读到 type="button"

原因在 Radix:两者都是 PopoverTrigger asChild 的子元素,而 PopoverTrigger 自己渲染的就是 Primitive.button 且带 type: "button"(node_modules/.pnpm/@radix-ui+react-popover@1.1.23/.../dist/index.mjs:89),Slot 会把它并到「自己没声明 type」的子元素上。只有预设 tab 是普通 button、没人替它供给,改前确实渲染成 submit。

所以本 PR = 一处真实(休眠)缺陷 + 两处把契约从上游实现细节搬进本地源码。后者正是 objectui#3344 在 Combobox 触发器上写下的理由(packages/components/src/custom/combobox.tsx:74-80,那段注释的措辞本来就是准确的)。

顺带纠正一行注释:PR #3926 在 add 触发器上写的「a Radix trigger keeps the HTML default of submit」是错的,而这句错误说法正是 issue 前提的来源(#3344 的准确注释 → #3926 的错误改写 → #6952 的立单前提 → 我第一版草稿也照抄了)。留着它下一个读者会再推一遍同样的错误结论,故就地改正,零行为影响。

⛔ 未做(issue 正文提到、PM 明确另裁):让 @object-ui/componentsPopoverTrigger 原语自带默认 type。另外补一条对那张设计卡有用的实测结论 —— Radix 的 PopoverTrigger 今天已经这么做了,所以那张卡若只针对 Popover 触发器,实际是 no-op;真正没人兜底的是普通 button(如本单的预设 tab)。

休眠性

今日唯一挂载点是 ListView 工具条,不在 form 元素内,线上无人踩到 —— 与 issue 标 finding 的判断一致。新测试把「组合变化后依然不提交」钉住。

测试

packages/plugin-list/src/__tests__/UserFilters.test.tsx 就近追加一个 describe(5 条):三处各一条(断言 getAttribute('type') === 'button',并在 form 元素内点击、断言 onSubmit 未被调用),一条 chip 清除 × 的行为(× 在 button 内部且只 stopPropagation(),挡不住 submit 按钮的 activation behaviour,靠的是外层 button 自己的 type),再加一条扫描式断言:两种模式下渲染出的每一个 button 都必须是 type="button" —— 将来新加的 button 不需要谁记得来扩列表。

pnpm exec vitest run packages/plugin-list/ --maxWorkers=2
Test Files 25 passed (25)
Tests 394 passed (394)
pnpm exec turbo run type-check --concurrency=2
Tasks: 78 successful, 78 total
pnpm --filter @object-ui/plugin-list lint
0 errors(348 条既有 warning)
node scripts/check-control-bytes.mjs ✅ OK
node scripts/check-changeset-no-major.mjs ✅ OK

反向验证(撤掉三行 type="button",方向如上所述与模板预期不同,如实记录):

× preset tab buttons declare type="button" ...
AssertionError: expected null to be 'button'
× no rendered UserFilters button is left at the submit default (sweep, both modes)
AssertionError: expected [ null, null, null ] to deeply equal [ 'button', 'button', 'button' ]
Tests 2 failed | 14 passed (16)

changeset:@object-ui/plugin-list patch。


Generated by Claude Code

…ack#6952)
An HTML <button> defaults to type="submit" inside a <form>. Three UserFilters
buttons declared no type: the dropdown chip trigger (filter-badge-*), the
overflow trigger (user-filters-more) and the preset tab (filter-tab-*). All
three now declare it, joining the session-tab buttons objectstack#5236 already
covered.
Only one of the three was actually at risk, measured by reverting the change:
the chip and overflow triggers are PopoverTrigger asChild children, and Radix's
PopoverTrigger renders Primitive.button type="button", which its Slot merges
onto a child declaring no type — both already rendered as `button`. The plain
preset tab button read `null`, i.e. submit. So this is one real (dormant) fix
plus two contracts moved out of an upstream implementation detail into local
source, the same reasoning objectui#3344 wrote onto the Combobox trigger.
Dormant rather than live: the only mount point today is ListView's toolbar,
which is not inside a form. The new tests assert every rendered UserFilters
button in both modes, so a future button cannot land at the submit default and
an upstream Radix change surfaces here instead of in a user's form.
Also corrects the in-file comment claiming "a Radix trigger keeps the HTML
default of submit" — the inaccuracy that propagated into objectstack#6952's
premise.
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 9, 2026 1:47pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DkmeuFNi.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
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)482.64KB106.39KB
core (index.js)3.00KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)231.13KB56.80KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
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)6.96KB1.98KB
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)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.02KB30.47KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)236.63KB59.02KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.63KB49.66KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)109.73KB26.55KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)20.15KB6.72KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
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 (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

验收通过,前提修正采纳(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#6952

CI 亲读终态:20 检查全 completed、0 失败。要点:

  • premise_still_valid: false 是本单最有价值的产出:反向验证实测三处只有预设 tab 真渲染 submit;chip 与 overflow 两处是 PopoverTrigger asChild,Radix 已注入 type="button"(源码行号在案)。修复仍然全部落地(1 个真休眠缺陷 + 2 个把契约从上游实现细节挪进本地源码),外加两模式全按钮 sweep 钉防未来新增按钮落回 submit 默认。
  • 错误前提的传播链被斩断:PR3926 那句「Radix trigger 保持 HTML 默认 submit」的错误注释(Combobox trigger 按钮缺省 type —— 在表单内点击会以 submit 语义提交表单 #3344 准确 → fix(plugin-list): allowAddTab 的「新增 tab」按钮接上会话级行为(objectstack#5236) #3926 错误转述 → #6952 前提)已随本 PR 纠正 —— 修正发在公开位置,合规。
  • 连带裁定失效声明:此前搁置的「PopoverTrigger 原语默认 type」设计卡按实测为 no-op(Radix 已做),不再立卡;未覆盖人群是裸 button,由 finding objectstack#7074(39 处普查 + lint 规则 vs 第四轮手补的两轴分析,家族已三轮逐个补)承接,归分诊轮定级 —— 该单的 lint 化方向值得优先考虑。

Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 14:02
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit cb5e32dAug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os6952-userfilters-button-type branch August 9, 2026 14:02
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…bjectstack-ai#3831) (objectstack-ai#3908)
`record:related_list.add.picker.filter` 被 spec 声明为「Restrict which records
the picker offers」,渲染器从未读过它:`RelatedList` 挂 `RecordPickerDialog` 时传
`objectName` / `title` / `displayField` / `columns` / `cellRenderer` /
`fieldsMeta` / `multiple` / `onSelect` / `onSelectRecords`,没有任何 filter。作者
写下「只允许指派 active 的岗位」「只允许挂未过期的许可」,拿到的是 `picker.object`
的全部记录;选中后直接建链接行或改父,`os validate` / `os build` 全绿,运行时零诊断。
现在它按原样传给 `baseFilter` —— 不是 `lookupFilters`,后者会把条件渲染成用户可编辑
的筛选栏行,等于把作者的硬性限制降级成建议。
## 为什么改到了 packages/fields
`baseFilter` 声明为 `Record<string, any>`、以对象展开合并,这个形状服务依赖型 lookup
链(objectstack-ai#2215)恰好正确,却根本装不下 spec 的 `ViewFilterRule[]`:TS 接受数组塞进该槽位
(数组满足 `any` 的字符串索引),展开把它压成 `{"0": rule, "1": rule}`,查询于是去过滤
名为 `0` / `1` 的列 —— 类型全绿、查询错误、无任何诊断。绕开它只剩「在 objectui 里再写
一份 spec-operator 词汇表」一条路,而这份词汇表已有两份(spec 的 `AST_OPERATOR_MAP`、
data-objectstack 的 `FILTER_OPERATOR_ALIASES`),objectstack-ai#3948 就是两份的代价。
所以槽位按结构判别(`Array.isArray`)接受两种形状,判别子是精确的而非启发式的 ——
AST 节点必是数组、规则必是普通对象,与 `toFilterNode` 同一谓词:
- 记录形式保持键覆盖语义**逐字节不变**:级联父值必须**替换**同字段上过期的
`lookupFilters` 条目,而不是与之求交(`account = 'stale' AND account = 'a1'` 会返回
零行)。`LookupField.dependsOn.test.tsx` 一行未动且保持绿。
- 规则数组经 `mergeFilterNodes`(仓内唯一 filter 下沉口,与 plugin-list 的
`buildEffectiveFilter`、plugin-view 的 ObjectView 共用)下沉,19 个 operator 全部
无损,包括记录形式没有 `$op` 可用的 `before` / `after` / `is_empty` /
`is_not_empty`。不新增第二份词汇表。
槽位类型同时收紧为 `unknown`,`useRecordQuery` 的 filter 类型与空判随之数组感知
(`Object.keys` 对数组返回下标,旧的记录专用判断对 AST 节点只是碰巧成立)。
## 顺带
`RelatedListProps.add.picker.filter` 从 `any` 收紧为 spec 的 `ViewFilterRule[]`;
`record:related_list.add` 的 input description 删掉 objectstack-ai#3808objectstack-ai#3165 先例写入的
KNOWN GAP 句(gap 兑现之后才删),`recordRelatedListInputs.spec-parity.test.ts` 的
那枚钉子随之翻转 —— 现在它反过来在「gap 警告被放回」或「接线被回滚却仍宣称限制生效」
时报红。
反向验证跑了两向,方向先判后跑:摘掉 RelatedList 的直传 → 数组路钉红且点名
`$filter` undefined,无 filter 用例保持绿;把合并回退成旧的对象展开 → 两个文件共
5 枚钉红,且报的是 `{ '0': { field: 'is_active', … } }` 这一**损坏**形态而非缺失,
记录路钉与 objectstack-ai#2215 六枚钉全绿。
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto 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.

objectui: UserFilters 里三个 button 元素缺 type="button"(objectui#3344 家族,当前休眠)—— observation

2 participants

@yinlianghui@claude