fix(app-shell): de-developerize the Studio interface property panel (#8218) - #8232
Merged
Merged
Conversation
Studio's interface panel grafts the metadata-admin form in front of an AI-build maker, so four habits written for an administrator landed in an otherwise fully Chinese surface. One cause, one pass. - The machine-name chip's `title="Machine name"` goes through the engine string table. - A master-detail column whose item schema carries no `title` humanises the key instead of printing `actionUrl` raw. `title` is an OPTIONAL annotation, so its absence is not off-spec metadata: this reuses the convention already applied wherever a title is missing (`json-schema-to-fields`'s `prop.title || humanizeKey`, and this file's own grid repeater `s.label || prettify(s.field)`), not a lenient contract fallback. - Both numeric renderers gray the schema `default` in as a placeholder and forward `minimum` / `maximum` / `multipleOf`. Only `fieldSpec.min` and `.max` were read before, which a spec-derived authoring form never declares, so the panel accepted a negative column count its own contract had ruled out. The default stays a placeholder, never a written value. - 45 user-visible literals across the two files now resolve through the engine table in en and zh. `DashboardDefaultInspector.test.tsx`'s helper is scoped to `<label>`: the humanised `Label` column header now collides with the curated Label field in a bare `getByText`, which is itself in-DOM evidence the change reaches the real dashboard panel.
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #8218
Clause-②: no
The diff is
packages/app-shell/src/views/metadata-admin/**plus one changeset. It touches no contract or spec surface: no@objectstack/spectype, no authorable-surface declaration, no governed path (AGENTS.md/CLAUDE.md/.claude/**/docs/adr/**). The contract-shaped half of the card was measured to belong upstream and was filed there instead — see below.What changed
Four symptoms, one cause:
SchemaForm+widgets.tsxwere written for an administrator editing metadata, and Studio's 「界面」 panel now grafts that same form in front of an AI-build maker.1. Machine-name tooltip.
title="Machine name"on the identifier chip now resolves through the engine string table (en+zh).2. Master-detail column headers.
{itemProps[c]?.title ?? c}printed the raw JSON Schema key (actionUrl,actionType) as if it were a column name. It now humanises the key.This is not a lenient contract fallback in the sense AGENTS §5 #0.1 forbids.
titleis an OPTIONAL JSON Schema annotation; its absence is not off-spec metadata, and the renderer must render something. The change reuses the convention this tree already applies wherever a title is missing, rather than inventing one:inspectors/json-schema-to-fields.ts—prop.title || humanizeKey(key), three sites;SchemaForm's own grid repeater<th>—s.label || prettify(s.field).The master-detail
<th>was the last place in these two files still printing a bare key.3. Numeric fields. Both numeric renderers (
SchemaForm's field control and the master-detail cell) now:defaultin as a placeholder, so an empty box reads "using the default" instead of "unknown". Placeholder, never a written value — "left on the default" stays distinguishable from "pinned to today's default" in saved metadata;minimum/maximum/multipleOfonto the control. OnlyfieldSpec.min/.maxwere read before, and a spec-derived authoring form (dashboardForm, …) declares neither — the bounds live on the JSONSchema the same spec produced. That is why the panel accepted a negative column count its own contract (columns: minimum 1, maximum 24) had already ruled out. The form spec still wins where it speaks; a test pins that.4. Sweep.
widgets.tsx+SchemaForm.tsxswept end to end: 45 user-visible literals at 45 call sites, over 37 new keys plus one reuse of the existingengine.form.selectEllipsis. aria-labels (move / remove / reveal / configure), placeholders, empty-state prose ("Bind a source object to …"), the segmented filter-element control, secret-field copy, the code editor'sread-onlyandLoading editor…,Not configured,Section {n}.Both
enandzhtables were written. This console'sengine.*table is the one string table the repo's i18n gates cannot see by construction (seemetadata-admin/i18n.ts's header andpackages/i18n/README.md, "Scope — theengine.*carve-out"), so a parity pin over all 37 keys ships with the change — nothing else would notice a key added toenand forgotten inzh.Deliberately out of scope, and why
refreshIntervaldropdown the card floated as optional. Declined. This panel is spec-driven on purpose —dashboard-schema.tssays in as many words that a new dashboard prop flows through with zero code changes here. Hardcoding a per-field enum widget for one property of one metadata type would break exactly that property, and it is the shape #0.1 warns about. If a dropdown is the right affordance,refreshIntervalshould declare its options in the spec and flow through.prettify(name) === label) can never hold once the label is translated, so the chip is unconditional inzhand near-invisible inen. Three defensible answers, all product calls.engine.*carve-out (en+zhonly, by that carve-out's documented posture), not inpackages/i18n/src/locales/.check:i18n-driftconfirms zero pack values moved.Where the localized column name landed, and why
The card asked me to decide this rather than follow its guess. The upstream guess was right about the direction and wrong about the mechanism, so the fix went upstream but not to the place the card named. Filed as objectstack#16458.
Three measured facts:
The bare keys are not only a translation gap — they leak in
en-UStoo.DashboardHeaderAction's Zod fields carry.describe(...)and no title, soz.toJSONSchemaemitsdescriptionand notitlefor all four.itemProps[c].titleisundefinedin every locale. An English maker readslabel / actionUrl / actionType / iconjust the same. Adding zh entries to a catalog would not have fixed even the English surface.No localization channel reaches this
<th>at all. The only path for these forms isresolveMetadataFormLabels, which decorates the FormView field specs. The array headers come from the JSONSchema — a different object no overlay touches. Adashboard.fields['header.actions[].label']catalog entry, which is what the card proposed, would land on aFormFieldSpecthatMasterDetailWidgetnever reads; its props do not even includefieldSpec. Necessary-but-not-sufficient at best, and today not reachable in that direction.The generated catalog is stale for this very form.
zh-CN.metadata-forms.generated.ts'sdashboard.fieldsnamesrefreshIntervalSecondswhere the spec's field isrefreshInterval, and carries noheader.*children at all. objectui ships a private overlay for those three, which is why the panel reads 显示标题 / 显示描述 / 操作按钮 today despite the catalog being silent.So: the root cause is a missing authoring annotation in the contract, plus an undesigned localization channel for item-level property paths — both objectstack's. objectstack#16458 carries all of that, plus a fourth item this PR ran into:
DashboardSchema.columnsdocuments "(default 12)" in prose and declares nodefault, so the new placeholder is empty for exactlycolumns/gap/refreshInterval. A number invented on the renderer side would be a second source of truth for a value the contract owns, so it was not hard-coded here. What stayed in this repo is only what is genuinely the renderer's decision: what to print when the optional annotation is absent.Verification
No screenshots. The card's acceptance asks for them from a live prod-like stack; there is none running (the local one on :5451 has exited) and standing one up for a polish card is not proportionate. Stating that plainly rather than claiming a frame I did not take. What replaces it is rendered-output assertions plus one piece of incidental in-DOM evidence, below.
Gates run at
a13bde6cd(the branch head), exit codes captured before any pipe:pnpm exec vitest run packages/app-shell/Test Files 640 passed (640)·Tests 6148 passed | 1 skipped (6149)pnpm exec vitest run apps/console/Test Files 89 passed (89)·Tests 1052 passedpnpm --filter @object-ui/app-shell builddist completeness: 1 package(s) complete (912 emitted files verified)pnpm --filter @object-ui/app-shell type-checktsc --noEmit && tsc -p tsconfig.test.json, cleanpnpm check:i18n-keyspnpm check:i18n-drift0 en value(s) changedin the ten packspnpm check:control-bytesnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsmajordeclaredTests are run from the repo root with path filters, per AGENTS' "怎么跑测试" (the
pnpm --filter <pkg> testform named in the dispatch is safe for this package specifically, since its script pins--root ../.., but the root form is used so the invocation guard is the one doing the deciding).Lint.
eslint --no-inline-configover the 5 touched files: 0 errors, 53 warnings, every warning pre-existingno-explicit-any/react-refresh/ hook-deps at lines the diff does not touch. Narrowing evidence, all three legs: the total population is 4402 files read from eslint's own config (eslint . --format json, not an estimate); the narrowed run linted 5 files, counted from--format json; and the config enables no type-aware linting (noproject/projectServiceineslint.config.js), so this diff cannot move the verdict on any file it does not contain. The repo-wide run reports 94 errors across 78 files under--no-inline-config, none of them in this diff and all in files byte-identical toorigin/mainhere.New pin.
SchemaForm.deDeveloperize-8218.test.tsx— 13 tests, green: tooltip in both locales and the literal's absence from the zh DOM; humanised headers plus a negative (actionUrlgone) plus thetitle-wins case; placeholder / min / max / step, form-spec-bound precedence, an all-absent case, and the master-detail cell; the 37-key parity pin with the notation-only exemption named rather than silently weakened.Reverse verification (ablation). Run against the committed fix, with a
trap ... EXIT INT TERMrestore. All three arms were mutated back to their pre-fix text; on-disk landing was proved by grepping the injected and removed texts separately (injectedtitle="Machine name"= 1, removedengine.form.machineNamecall = 0; injected?? c}= 1, removedhumanizeKey(c)= 0; removedplaceholder={defaultValue= 0 in both files) and by both blobs hashing differently from the HEAD blobs printed before the mutation. Result:VERDICT ablation-exit=1,Tests 6 failed | 7 passed (13)— every behavioural arm turned red, the 7 pure-table-parity tests correctly stayed green. Restore proved by an emptygit diff HEADand by both files hashing back to the exact HEAD blobs (4d40dd825…,1b1a5054e…).Incidental in-DOM evidence, and a test the change legitimately broke.
DashboardDefaultInspector.test.tsxwent red on three cases withFound multiple elements with the text: Label— its helper's baregetByText('Label')started matching both the curated Label field and the header-actions column, because that<th>now readsLabelwhere it used to readlabel. That is a rendering of the real dashboard inspector, not a synthetic fixture, so it is the closest thing to a screenshot available here: the change demonstrably reaches the panel the card is about. The helper is now scoped toselector: 'label', which is what it always meant.Filed alongside: objectstack#16458 (upstream contract half) · #8231 (the machine-name chip's degenerate predicate).
Not merged, not enqueued, left as draft per the dispatch.
Generated by Claude Code