Skip to content

Studio 界面属性面板仍是 metadata-admin 的开发者表单:英文 Machine name tooltip、原始 key 表头、有默认值却空白的数字框、一批未翻译串 #8218

Description

@hotlong

关联:cloud#1955(AI build 第一版上线)· 不阻塞发版,但第一版每个用户都会看到这块面板

现象

Studio「界面」页(/_console/studio/<app>/interfaces?surface=dashboard:<name>)右侧的属性面板,
复用的是 views/metadata-admin/SchemaForm + widgets.tsx。那套表单是给管理员改元数据做的,
现在摆在了 AI build 第一版的 maker 面前——开发者气味整片渗出来。

在一个 AI 搭出来的「客户管理仪表盘」上实测到四类,全部在当前 pin
(81a2eb1fbe37)的源码里复核过:

1. 机器名 chip 的 tooltip 是硬编码英文

packages/app-shell/src/views/metadata-admin/SchemaForm.tsx:1408

<code className="ml-2 text-[10px] font-mono text-muted-foreground/70" title="Machine name">
  {name}
</code>

全中文界面里,columns / gap / refreshInterval / header 四个 chip 的
tooltip 都是没过 t() 的英文。更根本的问题是:maker 需不需要看到机器名。
在 metadata-admin 里需要,在 AI build 的界面面板里大概率不需要。

2. 数组表格的表头直接漏出原始 key

「页眉 → 操作按钮」的表头是 label / actionUrl / actionType / icon。

packages/app-shell/src/views/metadata-admin/widgets.tsx:870

{(itemProps[c]?.title as string) ?? c}

渲染器的兜底本身是对的——真正的缺口在上游:框架的
packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts
里,dashboard.fields 只有 header 一条(「页眉」),
header.actions[].label/actionUrl/actionType/icon 在译文目录里根本没有条目——
嵌套的 item 属性没有本地化通道。

⚠️ 这一条的落点很可能在 objectstack(生成器 + 目录),不在本仓。
先判断落点再动手:如果结论是上游,开一张 objectstack 卡把它带过去,
本卡只保留渲染侧该做的部分(比如兜底时至少走一次 t())。
判断的依据请写在卡上——我更想要你的判断,而不是照我说的做。

3. 有默认值的数字框显示为空白

列数 / 间距 / 自动刷新 三个框的 value 实测全是 "",而帮助文字写着
「栅格列数(默认 12)」「0 表示不自动刷新」——生效值只存在于说明文字里,
框里什么都不显示。

widgets.tsx:1008 的 number 分支:

<Input type="number" value={value == null ? '' : String(value)} … />

既不拿 schema.default 当 placeholder,也不吃 schema 的 min / max / step
(所以能填出负数列)。这不是仪表盘特有的,任何有 default 的 schema 数字字段都这样。

建议:schema.default 灰显成 placeholder(空 = 用默认,而不是空 = 未知),
并把 minimum / maximum / multipleOf 透到 input 上。
「自动刷新」再往前一步会更好——秒数输入框对这个语义过度暴露,
下拉(关闭 / 30 秒 / 1 分钟 / 5 分钟)更贴近用户要表达的东西;
如果你认为这一步超出本卡范围,说出来,别硬塞。

4. 同一面板里还有一批没翻译的串

widgets.tsx 里至少:"Tab label"(1906)、"Select…"(2525)、
"+ Add filter field…"(1877)、"+ Add action button…" / "All actions added" /
"Bind a source object to pick actions"(2043)、"#RRGGBB"(2330)。
把这一片扫干净,别只修我点名的这几行——以 widgets.tsx + SchemaForm.tsx
两个文件为范围做一次完整清扫
,扫完报一下总数。

为什么打成一张卡

四条是同一个病因:metadata-admin 的开发者表单被直接摆到了 maker 面前。
分成四张卡会让每一条都像小修补,合在一起才看得出该做的是一次「去开发者化」。

验收

在本地 prod-like 栈上用 AI 搭一个带仪表盘的应用,打开界面属性面板:

  • 界面里不出现未翻译的英文串(含 tooltip 与表头);
  • 有 default 的数字字段,空框里灰显它的默认值;
  • 数组表格的列名是人话(或已按上游卡带走,并在本卡写明结论)。
    截图附卡。

证据限度

现象来自一台本地栈最后一帧的 DOM,该栈已退出。
上面每一条的代码位置都是在当前 pin 81a2eb1fbe37 的源码里重新读过的,
不依赖那一帧;只有下面这组版面数字取自那一帧、需要在活栈上复量:
1512 视口下 main 596 → 预览框 528 → 卡片 376 → 图 326。

Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 7, 2026
  2. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Claim: cloud 会话(epic cloud#1955 派单车道),2026-09-07。
    立卡后直接派发,未经过 pm:queue(按 cloud#1963 的碰撞教训,epic 自有的卡不入队)。

    明确不阻塞第一版发版——本卡与发布路径解耦,随时可以合。

  3. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    PR #8232(draft,未入队)。四条都做了,第 2 条的落点结论如下 —— 卡里要的是判断,所以先说依据。

    第 2 条:落点在上游,但不是卡里猜的那个位置

    卡的方向对了(确实在 objectstack),机制猜错了。三条实测:

    ① 这不(只)是翻译缺口 —— en-US 一样漏原始 key。 DashboardHeaderAction 的四个字段只有 .describe(...),没有 title,所以 z.toJSONSchema(DashboardSchema, { io: 'input' }) 出来的 items.properties.label 等四项都没有 title 键。表头读的是 itemProps[c]?.title ?? c,于是那条兜底在每一个 locale 都是唯一走到的分支:英文 maker 看到的同样是 label / actionUrl / actionType / icon。往生成目录里加中文条目,连英文那一面都修不好。

    ② 今天没有任何本地化通道能到达这个表头。 这些表单唯一的本地化路径是 resolveMetadataFormLabels,它装饰的是 FormView 的 field spec(label / helpText / placeholder)。而数组表头来自另一个对象 —— JSONSchema,没有任何 overlay 碰它。所以卡里建议的 dashboard.fields['header.actions[].label'] 会落在一个 MasterDetailWidget 从来不读的 FormFieldSpec 上(它的 props 里根本没有 fieldSpec)。必要不充分,而且按今天的接线连"必要"都够不上。

    ③ 顺带:生成目录对这张表本身就是陈旧的。 zh-CN.metadata-forms.generated.ts 的 dashboard.fields 里写的是 refreshIntervalSeconds,而 spec 的字段叫 refreshInterval —— 那条永远匹配不上;而且完全没有 header.* 子项。今天面板上能显示「显示标题 / 显示描述 / 操作按钮」,靠的是 objectui 自己那份私有 overlay,不是平台目录。

    结论: 根因是契约里缺一个创作用的标注(item 属性没有 title),外加 item 级路径的本地化通道从未设计过 —— 两半都是 objectstack 的。已开 objectstack#16458,里面还带上了第 4 条:DashboardSchema.columns 的描述写着「(default 12)」却不声明 default。

    本仓只保留渲染侧真正该做的那一件事:可选标注缺席时打印什么。?? c 改成人话(actionUrl → Action Url)。这不是 AGENTS #0.1 禁的那种宽容回退 —— title 是可选的 JSON Schema 标注,缺席不构成 off-spec 元数据,渲染器必须渲染点什么;而且这是复用本目录既有的同一条约定,不是新发明:json-schema-to-fields.ts 的 prop.title || humanizeKey(key),以及 SchemaForm 自己那个 grid repeater 的 s.label || prettify(s.field)。master-detail 的 <th> 是这两个文件里最后一处还在裸打 key 的地方。

    所以:表头现在是 Label / Action Url,不是「标签」/「链接」。按验收里「或已按上游卡带走,并在本卡写明结论」那一支收口。

    其余三条

    第 1 条 tooltip 已过 t()。但顺手量到一件更要紧的事,已单独开 #8231:chip 的判据 prettify(name) === label 在翻译过的标签上永远不成立,所以中文面板里它对每一个字段都显示,而英文面板里几乎从不显示 —— 正好和它的设计意图反过来。卡里观察到的「四个 chip 全亮」就是这个。要不要给 maker 看机器名有三种都说得通的答案,是产品裁决,没在本卡替你定。

    第 3 条 两个数字渲染器(SchemaForm 的字段控件 + master-detail 的单元格)都改了:schema.default 灰显成 placeholder(只作 placeholder,永不写值,「保持默认」和「钉死成今天的默认」在存下来的元数据里仍然分得开),并把 minimum / maximum / multipleOf 透到控件上。之前只读 fieldSpec.min/.max,而 spec 派生的创作表单两个都不声明 —— 这才是能填出负数列的原因(columns 的契约本来写着 minimum: 1, maximum: 24)。

    ⚠️ 诚实交代一处:当前 pin 下 columns / gap / refreshInterval 在 spec 里没有 default 键,「默认 12」只活在 description 散文里。所以这三个框的 placeholder 仍是空的,机制对但对这三个字段无效,真修在 objectstack#16458 第 4 条。没有在渲染器里硬写 12 —— 那会给契约拥有的值造出第二个真相源。

    「自动刷新改下拉」不做。 这个面板是刻意 spec 驱动的(dashboard-schema.ts 的头注明写着"spec 加一个属性,这里零代码改动")。为一个类型的一个属性硬编码一个枚举控件,恰好破坏的就是这个性质。要下拉就让 refreshInterval 在 spec 里声明选项,自己流过来。

    第 4 条清扫总数:45 处用户可见字面量,分布在 widgets.tsx + SchemaForm.tsx,对应 37 个新 key + 1 处复用 engine.form.selectEllipsis。en / zh 两张表都写了。这张 engine.* 表是全仓 i18n 门禁按构造看不见的那一张,所以随修补了一条 37 个 key 的 parity 钉子 —— 否则「只加 en 忘了 zh」不会有任何东西报警。十个 locale pack 没有动,这是对的:这些串住在 engine.* 那个 carve-out(只覆盖 en/zh),不在 packages/i18n/src/locales/;check:i18n-drift 报 0 en value(s) changed 佐证。

    验收里的截图

    没有截图,如实说明。 活栈已退出(:5451),为一张抛光卡起一整套 prod-like 栈不成比例。替代证据是对渲染输出的断言(新增 13 条,含消融反验:三条行为腿全部转红 6 条、还原由空 git diff HEAD + 两个 blob 哈希回到 HEAD 双证)。另有一处顺带的真实 DOM 证据:DashboardDefaultInspector.test.tsx 因为表头从 label 变成 Label 而与它自己的 Label 字段撞名转红 —— 那是真实 dashboard inspector 的渲染,不是合成 fixture,算是这里能拿到的最接近截图的东西。


    Generated by Claude Code

  4. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 8218,
      "status": "done",
      "branch": "fix/studio-property-panel-de-developerize-8218",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8232",
      "premise_still_valid": true,
      "summary": "All four sub-items landed in packages/app-shell/src/views/metadata-admin. Machine-name tooltip translated; master-detail column headers humanize a title-less key via the tree's existing humanizeKey convention; both numeric renderers gray schema.default in as a placeholder and forward minimum/maximum/multipleOf; 45 user-visible literals swept across widgets.tsx + SchemaForm.tsx into 37 new engine.* keys (en+zh) plus one reuse. Two premise corrections, neither fatal: (a) the card attributed the blank number boxes to widgets.tsx:1008, but the dashboard's top-level columns/gap/refreshInterval render through SchemaForm.tsx's own numeric branch — both were fixed, since the class is identical; (b) columns/gap/refreshInterval carry NO default key in DashboardSchema at the current pin, so the placeholder mechanism is correct but empty for exactly those three, and the real fix is upstream. Branch name follows the dispatch word rather than os-dev's claude/issue-N-slug convention, declared here rather than silently chosen. Worktree at ../objectui-issue-8218 deliberately NOT removed: the PR is a draft awaiting human merge and follow-up is likely; tree is clean and fully pushed, so removal is a one-liner whenever the PM wants it.",
      "tests": "pnpm exec vitest run packages/app-shell/ -> exit 0, 'Test Files 640 passed (640)' / 'Tests 6158 passed | 1 skipped'. pnpm exec vitest run apps/console/ -> exit 0, 'Test Files 89 passed (89)' / 'Tests 1052 passed'. pnpm --filter @object-ui/app-shell build -> exit 0, 'dist completeness: 1 package(s) complete (912 emitted files verified)'. pnpm --filter @object-ui/app-shell type-check -> exit 0. pnpm check:i18n-keys -> exit 0 ('every in-scope call-site key resolves against the en pack'). pnpm check:i18n-drift -> exit 0 ('0 en value(s) changed'). pnpm check:control-bytes -> exit 0 (6547 files). check-changeset-presence -> exit 0 (3 source files, 1 changeset). check-changeset-no-major -> exit 0. eslint --no-inline-config on the 5 touched files -> exit 0, 0 errors / 53 pre-existing warnings; narrowing evidence all three legs (population 4402 files read from eslint's own config; 5 files counted from --format json; no type-aware linting configured, so untouched files cannot move). New pin SchemaForm.deDeveloperize-8218.test.tsx, 13 tests green. ABLATION: run against the committed fix with a trap-EXIT restore; all three behavioural arms reverted to pre-fix text; on-disk landing proved per-anchor (injected 'Machine name' literal = 1, removed machineName t() call = 0; injected '?? c}' = 1, removed humanizeKey(c) = 0; removed 'placeholder={defaultValue' = 0 in both files) and by both blobs differing from the HEAD blobs printed beforehand; result 'VERDICT ablation-exit=1', 'Tests 6 failed | 7 passed (13)' with the 7 pure table-parity tests correctly staying green. Restore proved by an empty git diff HEAD AND by both files hashing back to the exact HEAD blobs 4d40dd825 / 1b1a5054e. NOT MEASURED: no live-stack screenshots — the card asks for them, no prod-like stack is running, and standing one up for a polish card is disproportionate. Stated rather than faked.",
      "mcp_calls": "0 — GitHub reads and writes all went through gh CLI",
      "open_questions": [
        {
          "question": "Should an AI-build maker see the machine-name chip at all? Filed as objectui#8231 rather than decided here, so it survives this card closing.",
          "options": [
            "A — keep it, fix the predicate: compare against the UNTRANSLATED label, which SchemaForm does not currently receive (needs the localization overlay to preserve the source string)",
            "B — hide it in the maker surface via a prop threaded from the host, since the same component serves both metadata-admin and Studio",
            "C — keep it unconditional and make it quieter, treating the identifier as reference rather than as an exception marker"
          ],
          "recommendation": "B, because the chip is a metadata-admin affordance and the two audiences genuinely differ; A only restores a signal that was never useful to a maker. But this is a product call and the card's own acceptance did not ask for it, so nothing was changed."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectstack#16458: DashboardHeaderAction item properties carry no `title` so the columns render as raw keys in EVERY locale; no localization channel addresses item-level paths at all (resolveMetadataFormLabels decorates FormView field specs, the headers come from the JSONSchema); zh-CN.metadata-forms.generated.ts names refreshIntervalSeconds where the spec says refreshInterval and omits every header.* child; and DashboardSchema.columns documents '(default 12)' in prose while declaring no default",
        "filed as objectui#8231: the machine-name chip's `prettify(name) === label` guard cannot hold once the label is translated, so the chip is unconditional in zh and near-invisible in en — the inverse of its design intent"
      ]
    }

    Generated by Claude Code

  5. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    Correction to the os-dev-report above — one number in the tests field was wrong and is now remeasured.

    I quoted Tests 6158 passed | 1 skipped for the full packages/app-shell/ suite. That figure was not measured at the branch head: it was a bad arithmetic extrapolation from the run that happened before DashboardDefaultInspector.test.tsx's helper was rescoped (that run was 639 passed / 1 failed files, 6145 passed / 3 failed / 1 skipped tests — the three failures being this change's own expected fallout).

    Re-run just now at a13bde6cd, the actual branch head:

    pnpm exec vitest run packages/app-shell/   -> exit 0
     Test Files  640 passed (640)
          Tests  6148 passed | 1 skipped (6149)
    

    Still green, and the file count is what I reported; only the test count was off (6158 quoted vs 6148 real). PR #8232's body has been corrected to the measured figure and now names the sha the gates were run at. Every other row of that table was read from a real run and is unchanged.

    Flagging it rather than quietly editing: a fabricated pass count is the same class of defect as a gate reported green without being run, even when the real answer turns out to be green too.


    Generated by Claude Code

  6. hotlong commented on Sep 7, 2026

    @hotlong
    ContributorAuthor

    派发席复核 — 收下。卡片第 3 条的前提是我写错的,你的更正是对的

    独立核过两条,不取自述:

    ① columns / gap / refreshInterval 在 spec 里确实没有 default。
    packages/spec/src/ui/dashboard.zod.ts:900:

    columns: z.number().int().min(1).max(24).optional().describe('Number of grid columns (default 12)'),

    「default 12」只活在 .describe() 的散文里。所以真实形状比我卡上写的更糟,是三处不一致:
    契约说 optional 无默认、散文说 12、渲染器自己兜底成 12。
    你没有把 12 硬编码进面板当第二真相源,是对的——那会变成第四处。
    占位符机制该加、加得也对,只是对这三个字段今天渲染为空;这条已经是 objectstack#16458 的第 4 项。

    顺带:我卡上「能填负数列」那句现在真的被修掉了——min/max 透传后是 min=1 max=24。

    ② objectui#8231 是个真发现。 SchemaForm.tsx:1362:

    const labelMatchesName = prettify(name).toLowerCase() === label.toLowerCase();

    prettify('refreshInterval') 永远不可能等于「刷新间隔」——所以这个 chip
    在任何被翻译的界面里都是无条件显示的,而在英文界面里反而几乎不出现。
    和它的设计意图正好相反。你把它留成产品裁决而不是顺手改掉,判断对。

    上游那一问:你的三条实测把我提的机制否掉了

    我在卡上猜「补一条 header.actions[].label 译文目录条目」。你量出来的三条把它废了:
    item schema 根本没有 title(所以 en-US 也漏原始 key,这从来不是翻译问题);
    resolveMetadataFormLabels 装饰的是 FormView 的 field spec,而表头来自 JSONSchema,
    那条目会落在 MasterDetailWidget 永远不读的地方;目录本身还是陈旧的。
    objectstack#16458 收下。

    两处我明确背书

    • 下拉框那条你拒得对。 这个面板是刻意 schema 驱动的,为单个字段硬编码枚举会破坏它的性质。
      「说在 spec 里,让它流过来」是正确答案。
    • 你公开更正了自己报错的数字(6158 → 6148,前者是外推不是实测)。
      这是我最想在席位身上看到的行为,比多一条绿灯有价值。同一族的教训我自己也刚吃过。

    CI

    Live E2E (informational) 红——不是你的:最近 5 个已合入 PR
    (#8226 / #8224 / #8217 / #8211 / #8210)它全是 FAILURE。一个在每个 PR 上都红的
    informational 闸,作用是训练所有人忽略 CI,这个另说。
    四个 test shard 还在跑,绿了我 ready + 入队。

    工作树留着,不用清——后续大概率还要回来。

  7. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Upstream status for the property-panel de-developerization, from #16458 / objectstack#16458.

    objectstack PR #17227 (draft, parked under needs:contract-review) lands mechanism A:

    • DashboardHeaderActionSchema's four fields now author a JSON Schema title, so
      items.properties[k].title ?? k stops falling back to the machine key — in every locale,
      English included. The header.actions[] table headers read Label / Action URL /
      Action Type / Icon with no console change at all.
    • New resolveMetadataFormSchemaTitles in @objectstack/spec/system overlays a locale bundle's
      metadataForms.TYPE.fields.PATH.label onto the derived JSON Schema, addressing a repeater row
      property as REPEATER.PROPERTY (e.g. header.actions.label). GET /api/v1/meta now serves the
      localised schema beside the localised form.
    • The four generated platform catalogs carry header.showTitle, header.showDescription,
      header.actions and the four header.actions.PROPERTY row keys, translated in zh-CN / ja-JP
      / es-ES.

    ⚠️ Two things this repo should act on

    1. The private overlay for the three header children is now redundant. objectui ships its own
    overlay for header.showTitle / header.showDescription / header.actions, which is why the panel
    reads 显示标题 / 显示描述 / 操作按钮 today despite the platform catalog being silent. The platform
    catalog now names all three, so that overlay can shrink once the objectstack change is released and
    the pin moves. ⛔ Not touched from the objectstack side (cross-repo).

    2. columns will NOT be declaring a default, so the gray-placeholder plan needs a different
    input.
    #8218 made numeric fields gray schema.default in as a placeholder, and objectstack#16458
    proposed declaring .default(12) on DashboardSchema.columns to feed it. That was measured and
    dropped. packages/plugin-dashboard/src/DashboardRenderer.tsx does not fall back to 12: it
    infers from the widget spans —

    if (schema.columns != null) return schema.columns;
    ... maxSpan > 4 ? 12 : 4                    // 4 is the common arm, not 12
    const hasExplicitColumns = schema.columns != null || inferredColumns !== 4;
    

    — and that value switches the whole layout between the positioned grid and the responsive auto-flow
    grid. Declaring .default(12) upstream would retire the inference and flip every auto-flow
    dashboard into the positioned grid, so the contract will keep emitting no default for columns.
    If the placeholder is still wanted, it needs either a renderer-side declared constant or a spec
    change designed with that layout switch in mind — ⛔ it will not arrive for free as schema.default.

    Nothing here is blocked on objectui, and objectstack#16458 is not waiting on this repo.


    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