Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

objectui: 39 个普通 button 元素缺 type="button"(objectui#3344 家族的总体面);已一次一个修了三轮,建议用 lint 规则收口 —— observation #4045

Closed

Description

@os-zhuang

Migrated from objectstack-ai/objectstack#7074 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-08-09T13:50:13Z. The full prior thread — including triage rulings and hold/restart conditions — remains on the source issue and MUST be read before acting on this card.


发现于 objectstack#6952 的实施(objectui PR objectstack-ai/objectstack#3948,给 UserFilters 三处补 type="button")。不阻塞那一单,它已按范围修完;这是顺手量出来的家族总体面,单独立案。

事实(实测脚本逐个解析 JSX 开标签,非阅读推断)

objectui origin/main = ebb579dbb。统计口径:packages/*/src + apps/*/src 下的 .tsx,排除 __tests__/、*.test.tsx、src/ui/(Shadcn 上游禁改区);把每个 button 开标签完整收集到第一个深度为 0 的 >,剔除注释行里谈论 button 的文字(SelectField.tsx 那段 objectstack-ai/objectstack#3306 注释就会被误计),再判断有无 type=。

普通 button 元素、完全没有 type 的:39 处

文件 行
apps/console/src/pages/developer/ApiConsolePage.tsx 236, 285, 312, 323, 358, 391, 440, 470, 481, 497
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx 913, 1658, 1720, 2279, 2333, 3000, 3044, 3486
apps/console/src/components/schema/objectDetailWidgets.tsx 150, 158, 173, 475
apps/console/src/components/PerformanceDashboard.tsx 132, 166, 173
packages/app-shell/src/views/MetadataInspector.tsx 92, 105
packages/components/src/debug/DebugPanel.tsx 275, 288
packages/plugin-designer/src/components/ConfirmDialog.tsx 84, 90
packages/components/src/renderers/overlay/drawer.tsx 38
packages/components/src/renderers/data-display/tree-view.tsx 48
packages/components/src/custom/navigation-overlay.tsx 528
packages/plugin-designer/src/components/PropertyEditor.tsx 86
packages/plugin-designer/src/components/VersionHistory.tsx 94
packages/app-shell/src/console/organizations/OrganizationsPage.tsx 237
packages/app-shell/src/console/organizations/manage/InvitationsPage.tsx 152
packages/app-shell/src/console/organizations/manage/OrganizationLayout.tsx 94

另有 2 处是 Radix *Trigger asChild 的子元素、自己没声明 type(apps/console/src/pages/developer/ApiConsolePage.tsx:274、packages/app-shell/src/layout/AppSwitcher.tsx:49)—— 见下节,这两处今天由 Radix 兜住,风险等级与上表 39 处不同。

顺带纠正一个家族内流传的错误机制说法(这条对下一张卡最有用)

objectstack#6952 正文(以及 objectui PR objectstack-ai/objectstack#3926 在 filter-tab-add 上的注释)称「Radix 的 PopoverTrigger 不 preventDefault,所以触发器保持 HTML 的 submit 默认」。实测不成立:PopoverTrigger 渲染的是 Primitive.button 且自带 type: "button"(@radix-ui/react-popover@1.1.23,dist/index.mjs:89),Slot 会把它并到「自己没声明 type」的子元素上。把 type="button" 从 UserFilters 两个 PopoverTrigger 子元素上撤掉,渲染出来仍然是 type="button";只有普通 button 撤掉后读到 null。

推论,直接影响 objectstack#6952 里被「另裁」的那半张卡(让 @object-ui/components 的 PopoverTrigger 原语自带默认 type):Radix 今天已经这么做了,那张卡若只针对 Popover 触发器基本是 no-op。真正没人兜底的是普通 button —— 也就是上表那 39 处。packages/components/src/custom/combobox.tsx:74-80 的注释本来就把这件事写准了(「Radix … happens to supply type="button" via its Slot today, but that is an upstream implementation detail — declare the contract locally」),是后续引用把它改错了。

为什么标 observation、不进 pm:queue

没有证明任何一处今天被用户踩到。 这 39 处的可达性是逐点的,我没有逐点验证「它此刻确实渲染在 form 元素内部」。分两档,交给 triage 定级:

  • apps/console / app-shell / plugin-designer 的页面级按钮(30 处):挂载点固定,和 objectstack#6952 里 UserFilters 的处境类似 —— 除非有人把那块组合进 form,否则休眠。
  • @object-ui/components 的 renderer(drawer.tsx:38、tree-view.tsx:48、navigation-overlay.tsx:528):这三处的休眠论据明显更弱。它们是由 JSON 元数据任意组合的渲染器 —— schema 作者完全可以把 tree-view 或 drawer 放进一个 form 里,而这正是 objectstack#6952 预告的「组合变化即触发」。这一档我倾向于比页面级按钮更值得看一眼,但同样没实测,不自己定级(objectstack#4949:立单时的严重度判断两个方向都不可靠)。

修的话是什么形状(两个选项,取舍归维护者)

一次一个地补属性已经走了三轮(objectui#3344 的 combobox → objectstack#5236 / PR objectstack-ai/objectstack#3926 的 add 触发器 → objectstack#6952 / PR objectstack-ai/objectstack#3948 的三处),每轮都只覆盖当时被看见的那几个,population 还剩 39。所以问题不在这 39 个属性,而在没有任何机制阻止第 40 个被写出来。

  • A. 机械强制(推荐): 加一条 lint 规则,把「button 元素必须声明 type」变成 error。本仓已有自己的插件与配套 RuleTester(eslint-rules/,如 no-synthetic-event-trigger.js、no-dynamic-import-in-test-hook.js),照那个形状写一条即可;也可以直接引 eslint-plugin-react 的 button-has-type(注意:本仓目前只装了 eslint-plugin-react-hooks / -refresh,基座 eslint-plugin-react 没装,引它是新增依赖)。规则落地后 39 处一次性修完、且第 40 个写不出来。
  • B. 只补这 39 处属性: 便宜,但把「不犯错」继续押在作者记性上 —— 第四轮同类立单只是时间问题。

两轴评估:长期健康度上 A 才消除缺陷类,B 只消除缺陷实例;让 AI 写的代码难写错上差距更大 —— 这些 UI 是 AI 大量生成的,lint error 在写出来的那一刻就拒绝,而 B 依赖每个作者(人或 AI)记得一个不在类型系统里的 HTML 冷知识。A 的成本是一条规则 + 一次 39 处的机械修改,B 之后还会再付一次。

参考

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    Contributor

    CLAIM + RULING — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4045-button-type-lint, one dev agent, one PR, Fixes #4045.

    Ruling (delegated, veto window open): option A — mechanical enforcement, per the card's own two-axis analysis: a lint rule ends the defect CLASS where three per-instance rounds only ended instances, and for AI-generated UI the write-time rejection is the difference that matters. Shape: the in-repo custom rule following the eslint-rules/ precedent (rule + RuleTester like no-synthetic-event-trigger.js) — NOT the eslint-plugin-react route, which adds a base-plugin dependency for one rule. The 39 sites are fixed in the same PR under the rule's own red (the #4118 red is the rule firing on the unfixed tree). Card cautions binding on the dev: the prior thread on objectstack#7074 MUST be read first; the population table is from an old branch point (ebb579dbb) — re-measure (some files, e.g. objectDetailWidgets.tsx, have since been DELETED by #4365); the Radix-mechanism correction in the card (triggers already get type from Primitive.button) governs — the 2 Radix-child sites are NOT in scope, and the src/ui/ Shadcn zone stays excluded per the card's own counting rules.

    Dispatch window verified: no in-flight task holds console/app-shell/components/plugin-designer source (#4425-P1 is test-only, #4437 is console/ai — not in the population, #4352 is react's spec-bridge).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions