Repository navigation
Commit 74801d1
fix(studio): stop offering the dead field.externalId toggle, and keep a stored value through every save (#27315)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB3FHBKJzKkbDiW2okoG1HcZ)_
Fixes #26674
Claim: 6471969865
Clause-②: no
Metadata-protocol: none
Studio stops offering the key; the protocol keeps parsing it. This is
not a retirement: no tombstone, no narrowing, no schema, type or export
change. `FieldSchema` accepts `externalId` exactly as before, and a
field that already carries `externalId: true` keeps it through every
Studio save.
## 维护者速读
- **改了什么:** Studio 的字段元数据表单、字段设计器抽屉、字段检查器三处不再显示「External ID」开关和「用于
upsert」的帮助文字;已经存了 `externalId: true` 的字段,在 Studio 里编辑保存后原样保留。
- **为什么改:** `field.externalId` 已判为 dead,任何
upsert、导入、种子都不按它匹配。开关让人以为重新导入会按它更新而不是新增,结果静默产生重复记录,`os lint`
还会对表单自己给出的值报警告。
- **风险与回滚:** 低。协议照旧解析该键,只删 UI
控件和只被它用到的文案;设计器更新路径改为靠展开保留旧值,三个保存面各有往返测试钉住。回滚 = revert 本 PR。
- **先看:** `ui/packages/plugin-designer/src/FieldDesigner.tsx`(更新路径不再写
`externalId`)、`ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx`(删复选框)、`packages/spec/src/data/field.form.ts`(删表单行)。
- **两分钟验证:** `cd ui && pnpm exec vitest run
packages/plugin-designer/src/MetadataFieldsPage.externalIdRoundTrip-26674.test.tsx`,预期
2 个用例全绿:编辑保存后 PUT 里两个字段的 `externalId` 仍是 `true`。
- **需要你做的:** 无,席位落地。
## What changes
Measured first on `origin/main` `2302b92b`: three Studio surfaces
offered the toggle.
| Surface | Before | After |
|---|---|---|
| Spec field metadata form (`packages/spec/src/data/field.form.ts`,
Advanced section) | row `externalId`, help text "Mark as external ID for
upsert operations", repeated in all four `*.metadata-forms.generated.ts`
bundles (en "External Id" / zh-CN "外部 ID" / ja-JP / es-ES) | row
removed; bundles regenerated with `node scripts/check-i18n-bundles.mjs
--write --filter=platform-objects` (16 lines removed, nothing else
moved) |
| Field Designer drawer
(`ui/packages/plugin-designer/src/FieldDesigner.tsx`, Advanced section;
used by `MetadataFieldsPage.tsx`) | `externalId` boolean control; update
path wrote `externalId: data.externalId === true` | control and its
initial value removed; the update path no longer names the key, so
`...editingField` carries a stored value through; a created field no
longer carries `externalId: false` |
| Field inspector
(`ui/packages/app-shell/.../inspectors/ObjectFieldInspector.tsx`,
Advanced section) | `External ID` checkbox | removed; `patchDef` spreads
the whole stored definition, so the key survives every edit |
- **i18n strings only the toggle used:**
`appDesigner.fieldDesigner.externalId` removed from all ten
`ui/packages/i18n/src/locales/*.ts` packs and from the plugin-designer
fallback table (`useDesignerTranslation.ts`);
`designer.field.externalId` removed from the Studio namespace (`en`,
`zh`).
- **`MetadataFieldsPage.tsx` is unchanged.** Its `toDesignerField` /
`fromDesignerField` pair already carries `externalId` both ways; with
the drawer now preserving it, the round-trip holds, and a test pins it
end to end.
- **Form ↔ Zod reconciliation:** `externalId` is recorded as a
deliberate root omission of the `field` form, with its reason (graded
`dead`, the schema still parses it, the seed dataset's `externalId` and
an import mapping's `upsertKey` are the real match keys).
- **Changesets (patch, one per touched published package):**
`@objectstack/spec`, `@objectstack/platform-objects`,
`@object-ui/plugin-designer`, `@object-ui/i18n`. `@object-ui/app-shell`
is private, so it gets none.
- **Liveness evidence** (`packages/spec/liveness/field.json`, row
`externalId`, claim amendment 6474597348): the evidence said Studio's
inspector and designer edit the key, which this PR makes false. It now
says the protocol still parses the key, no Studio surface offers it
(form row, designer drawer, inspector), and the `ui/` mentions only
carry a stored value through a save. Prose only: the grade stays `dead`,
`verifiedAt` and `authorHint` are unchanged.
- **Not governed:** no `skills/**`, `content/docs/**`, `.claude/**` or
other register path is touched.
## Tests
- `packages/spec/src/data/field-form-external-id-not-offered.test.ts`
(new, 4 cases): the field form declares no `externalId` row at any depth
(with a positive control that the walk reaches `storage`); no help text
still calls a field flag an upsert key; `FieldSchema` still parses an
authored `externalId: true`; the liveness row `field.externalId` still
reads `dead`.
-
`ui/packages/plugin-designer/src/__tests__/FieldDesigner.externalIdNotOffered-26674.test.tsx`
(new, 4 cases): no `externalId` control in the create or edit drawer
(each paired with the `trackHistory` sibling as positive control); a
field with `externalId: true` keeps it through the drawer's
edit-and-save; a created field carries no `externalId` key.
-
`ui/packages/plugin-designer/src/MetadataFieldsPage.externalIdRoundTrip-26674.test.tsx`
(new, 2 cases): the REAL `FieldDesigner` over a real `MetadataClient`
and a fetch double; after an edit-and-save the PUT keeps `externalId:
true` on the edited field and on an untouched one, a field without the
flag gains none, and every PUT field parses through `FieldSchema`.
-
`ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.externalIdNotOffered-26674.test.tsx`
(new, 3 cases): no `External ID` control (with its Advanced siblings
rendering), a stored value does not resurrect it, and a label edit keeps
`externalId: true` in the saved fields map.
- Moved pins: `metadata-form-zod-reconciliation.test.ts` gains the
`field` root omission row;
`object-lifecycle-panel-echo-decisions.test.ts` translated-leaf positive
control 695 → 694 (one row label fewer, per locale);
`ObjectFieldInspector.retiredIndexed.test.tsx` drops `External ID` from
its list of Advanced siblings.
## Acceptance notes
- `DesignerFieldDefinition.externalId` (`@object-ui/types`), its Zod
mirror and `MetadataService`'s field summary are unchanged on purpose:
they carry the stored value, which is what keeps it from being stripped.
- `content/docs/**` and `skills/**` already describe the field flag as
"Not read" and teach the seed dataset's `externalId`, so nothing there
became false.
## Verification
All readings at head `b357ce35` unless stated; local runs under
`scripts/pm/os-verify-lock.sh`.
Round r1 (head `0b08d306`, the liveness evidence prose only): `pnpm
--filter @objectstack/spec check:liveness` exit 0 (80 dead, 1219
classified); `pnpm --filter @objectstack/spec test` 749 passed + 1
skipped of 750 files, 22379 passed, 0 failed.
<details>
<summary>Package tests and type-checks</summary>
- `pnpm --filter @objectstack/spec test` at `5570dc7a`: 749/750 files,
22380 passed, 1 failed (`metadata-form-zod-reconciliation`, the expected
root-omission ripple). Fixed in `b357ce35`; that file, the new spec test
and `metadata-form-declared-rows.pin.test.ts` then 3/3 files, 91/91
tests.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- `pnpm --filter @objectstack/platform-objects test` at `5570dc7a`:
95/96 files, 1360 passed, 1 failed (the 695 leaf-count pin). Fixed in
`b357ce35`; that file 35/35.
- `pnpm --filter @objectstack/platform-objects typecheck`: exit 0.
- `cd ui && pnpm exec vitest run packages/plugin-designer/
packages/i18n/`: 139/139 files, 1833 passed, 13 skipped.
- app-shell, narrowed to the 40 test files that import
`ObjectFieldInspector` (the full app-shell suite is CI's Test UI): 40/40
files, 459/459 tests.
- `pnpm exec turbo run type-check --filter=@object-ui/plugin-designer
--filter=@object-ui/i18n --filter=@object-ui/app-shell`: 66/66 tasks,
each of the three type-checks executed (cache miss).
- `pnpm --dir ui run check:designer-field-key-parity`,
`check:i18n-keys`, `check:i18n-drift`: exit 0 each (drift: 1 key
removed, 0 values changed).
- `node scripts/check-i18n-bundles.mjs` (`pnpm check:i18n`): OK, 10
packages in sync.
</details>
<details>
<summary>Ablation (the update-path change)</summary>
`node scripts/ablation-replace.mjs` put `externalId: data.externalId ===
true` back into `FieldDesigner.handleFormSuccess`'s update path (anchor
1 → 0, blob `a0985e17` → `6a7c1f3e`). The two round-trip cases went red,
exactly those two: `FieldDesigner … keeps externalId: true through the
update path` and `MetadataFieldsPage … keeps externalId: true on the
edited field and on an untouched one` (`expected false to be true`); 4
other cases stayed green. Restored: blob equals HEAD, `git diff HEAD`
empty.
</details>
<details>
<summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 85 families. Reconciliation with `--ran`:
85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit is 0.
On the first pass nine families ran while a turbo build was rewriting
`packages/spec/dist` (exit 3 / timeout) and were re-run on a settled
tree, exit 0 each.
Then all 85 re-run once more, serially, on the final head `b357ce35`
with nothing else running: 85/85 exit 0; `--ran` again 85 derived, 85
run, 0 NOT-MEASURED, 0 UNRUN (`dispatch-gates --ran` exit 0).
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzg4R646jZ5Ky7hqq9L)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 10f27b6 commit 74801d1
32 files changed
Lines changed: 461 additions & 44 deletions
File tree
- .changeset
- packages
- platform-objects/src/apps/translations
- spec
- liveness
- src
- data
- system
- ui/packages
- app-shell/src/views/metadata-admin/inspectors
- i18n/src
- locales
- namespaces/studio
- plugin-designer/src
- __tests__
- hooks
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
813 | 813 | | |
814 | 814 | | |
815 | 815 | | |
816 | | - | |
817 | | - | |
818 | | - | |
819 | | - | |
820 | 816 | | |
821 | 817 | | |
822 | 818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
813 | 813 | | |
814 | 814 | | |
815 | 815 | | |
816 | | - | |
817 | | - | |
818 | | - | |
819 | | - | |
820 | 816 | | |
821 | 817 | | |
822 | 818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
813 | 813 | | |
814 | 814 | | |
815 | 815 | | |
816 | | - | |
817 | | - | |
818 | | - | |
819 | | - | |
820 | 816 | | |
821 | 817 | | |
822 | 818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1238 | 1238 | | |
1239 | 1239 | | |
1240 | 1240 | | |
1241 | | - | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
1242 | 1244 | | |
1243 | 1245 | | |
1244 | 1246 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
813 | 813 | | |
814 | 814 | | |
815 | 815 | | |
816 | | - | |
817 | | - | |
818 | | - | |
819 | | - | |
820 | 816 | | |
821 | 817 | | |
822 | 818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
382 | 382 | | |
383 | 383 | | |
384 | 384 | | |
385 | | - | |
| 385 | + | |
386 | 386 | | |
387 | 387 | | |
388 | 388 | | |
| |||
0 commit comments