Repository navigation
fix(fields): 注册路径的全屏长文本对话框接上 i18n(#3404) - #3405
Merged
Merged
Conversation
…log (#3404) `FullscreenFieldEditor` — the expand button and dialog `TextAreaField` and `RichTextField` render under `mobile.fullscreenLongText` — carried four English literals: the toggle's accessible name, the `Edit text` title fallback, `Cancel` and `Done`. No translation was missing: `form.fullscreen.*` and `common.cancel` have shipped in all ten locale packs since #3272 translated the built-in branch; this path never consumed them. Inside one form a zh session saw the built-in branch's translated footer next to this path's English one. All four now consume those EXISTING keys — no new keys, no locale-pack change — plus the sr-only `form.fullscreen.description` the built-in branch already carries, so the dialog has an `aria-describedby` instead of none. Copy resolves through `useFieldTranslation()` (`createSafeTranslation`), as the built-in branch does, not bare `useObjectTranslation`. Measured with no provider mounted, the bare hook returns raw keys (`common.cancel` -> "common.cancel") and drops the `{{label}}` interpolation — strictly worse than the literals. The safe hook's English defaults are byte-identical to those literals, so provider-less hosts render what they did before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 5, 2026 20:19
This was referenced Aug 22, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Aug 23, 2026
…s master_detail gets the #3405 hints `ActionParamDialog` decided whether to show the "paste a record id" placeholder and help text with its own literal over RAW param spellings (`param.type === 'lookup' || param.type === 'reference'`), while the degradation it was reacting to is performed by `paramToField` over RESOLVED widget keys (`LOOKUP_WIDGET_TYPES`). The two sets overlapped without either containing the other: `master_detail` degraded to a plain text box and got no hints at all (an unexplained empty field asking for a bare UUID), and `reference` was a hand-copy of a `PARAM_TYPE_ALIASES` row the adapter folds away before it tests membership. `paramToField` now exports `paramDegradesWithoutTarget(param)` — the one answer to "did this param fall back for want of a declared target?" — and both the adapter's own fallback branch and the dialog's two hint readers ask it. One membership table remains in this surface. Deliberate behaviour change: a targetless `master_detail` param now receives `actionDialog.lookupPlaceholder` and `actionDialog.lookupHelpText`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
yinlianghui
pushed a commit
that referenced
this pull request
Aug 24, 2026
…ies of the reference-bearing field family Four faces restated `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` inline, none deriving from it and none pinned against it, and every one diverged from it in BOTH directions: all four lacked `user` and `tree`, and all four carried a fifth spelling `reference` that no producer can emit. Each now reads the published family and carries an identity pin (a spy on its `has`) so a member-identical private copy fails rather than quietly re-forking the table. `resolveActionParams` also lacked `master_detail`, so a field-backed `master_detail` action param inherited no picker config and degraded to the "paste a record id" text input #3405 exists to prevent. It keeps answering for `reference` through the one alias table in `paramToField`, which the dialog still accepts from params already authored with it — the same expression `paramToField` evaluates one step later, so the half that populates a param's picker config and the half that forwards it can no longer disagree. Also corrects the two now-false "fourth and last private copy" claims in `paramToField.ts` / `paramToField.test.ts`, pointing at the census in the family's canonical home rather than restating a count locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
This was referenced Aug 24, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 1, 2026
…s master_detail gets the objectstack-ai#3405 hints (objectstack-ai#5882) * fix(app-shell): ask the adapter which params degraded, so a targetless master_detail gets the objectstack-ai#3405 hints `ActionParamDialog` decided whether to show the "paste a record id" placeholder and help text with its own literal over RAW param spellings (`param.type === 'lookup' || param.type === 'reference'`), while the degradation it was reacting to is performed by `paramToField` over RESOLVED widget keys (`LOOKUP_WIDGET_TYPES`). The two sets overlapped without either containing the other: `master_detail` degraded to a plain text box and got no hints at all (an unexplained empty field asking for a bare UUID), and `reference` was a hand-copy of a `PARAM_TYPE_ALIASES` row the adapter folds away before it tests membership. `paramToField` now exports `paramDegradesWithoutTarget(param)` — the one answer to "did this param fall back for want of a declared target?" — and both the adapter's own fallback branch and the dialog's two hint readers ask it. One membership table remains in this surface. Deliberate behaviour change: a targetless `master_detail` param now receives `actionDialog.lookupPlaceholder` and `actionDialog.lookupHelpText`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m * chore(changeset): declare the app-shell degraded-picker hint change (objectstack-ai#5654) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 1, 2026
…ies of the reference-bearing field family, and correct the "fourth and last" claim (objectstack-ai#6064) * fix(plugin-kanban,plugin-detail,app-shell): converge four private copies of the reference-bearing field family Four faces restated `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES` inline, none deriving from it and none pinned against it, and every one diverged from it in BOTH directions: all four lacked `user` and `tree`, and all four carried a fifth spelling `reference` that no producer can emit. Each now reads the published family and carries an identity pin (a spy on its `has`) so a member-identical private copy fails rather than quietly re-forking the table. `resolveActionParams` also lacked `master_detail`, so a field-backed `master_detail` action param inherited no picker config and degraded to the "paste a record id" text input objectstack-ai#3405 exists to prevent. It keeps answering for `reference` through the one alias table in `paramToField`, which the dialog still accepts from params already authored with it — the same expression `paramToField` evaluates one step later, so the half that populates a param's picker config and the half that forwards it can no longer disagree. Also corrects the two now-false "fourth and last private copy" claims in `paramToField.ts` / `paramToField.test.ts`, pointing at the census in the family's canonical home rather than restating a count locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L * fix(app-shell): give the new paramToField import an explicit .js extension `check:esm-specifiers` (inside the Type Check CI job) caught the relative specifier this PR added: Node's ESM resolver does not extension-search relative specifiers and app-shell's build emits them unchanged, so the published entry could not be imported under plain Node. Part of objectstack-ai#5874 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 28, 2026
…, and an unreadable one is refused (objectstack-ai#10158) Fixes objectstack-ai#10129 Clause-②: no — `node scripts/check-governed-queue-guard.mjs --test` over this branch's 16 paths answers `NOT GOVERNED — none matched`. The diff is `packages/app-shell`, `packages/core`, `packages/i18n` and one changeset; it touches no `AGENTS.md`, no `CLAUDE.md`, nothing under `.claude/**`, `skills/**` or `docs/adr/**`. ## Step 1 first: which side dropped the binding The card refused to name a side and asked for one to be established. **Outcome: the binding reaches the client, and the renderer path ignores it.** The fix is here, in this repo. The decision site is `paramToField()` in `packages/app-shell/src/utils/paramToField.ts` — the `paramDegradesWithoutTarget(param)` branch that rewrites `type` to `'text'`. But that is not where this defect lives, and finding that out is most of the work: for the reported param that branch **never fires**. One step earlier, `resolveActionParam()` in `packages/app-shell/src/utils/resolveActionParams.ts` resolves a field-backed param against the `objects` list its **caller** hands it: ```ts const owner = ctx.objects.find((o) => o?.name === ownerName); const field: RuntimeField | undefined = owner?.fields?.[param.field]; if (!field) { /* … type: param.type ?? 'text' … */ } ``` A field-backed param (`{ field: 'contract_type' }`) declares no inline `type`, so when the owner object is absent from that list the param becomes a `text` param — before any picker logic runs. Measured on this tree, both before and after the change: | resolver input | resolved `type` | `referenceTo` | `paramDegradesWithoutTarget` | widget | |---|---|---|---|---| | `{ field }`, owner object present | `lookup` | `clm_contract_type` | false | `lookup` | | `{ field }`, owner object **absent** | `text` | undefined | **false** | `text` | | `{ name, type: 'lookup', reference }` inline | `lookup` | `clm_contract_type` | false | `lookup` | Row 2 is the card, in every detail. `paramDegradesWithoutTarget` answering **false** is the load-bearing half: by the time anything downstream looks, the param *is* a `text` param, and text params do not degrade. So no "no reference target" warning fired, the "paste a record id" placeholder and help text (objectstack-ai#3405) did not apply, and the user got an unannotated empty box — no dropdown, no typeahead, and **no request for the referenced object on the wire, because no picker was ever built**. That silence is why this survived a version. ### Which callers cannot hold the right list - `packages/app-shell/src/console/ConsoleShell.tsx` — `GlobalActionRuntimeProvider` calls `useConsoleActionRuntime({ dataSource })` with **no** `objects` and **no** `objectName`. Its own docblock says why it exists: every `action:button` *outside* ObjectView / RecordDetailView / PageView / DeclaredActionsBar binds to it, and it was added so a `type: 'flow'` action there stops failing with "Flow handler not registered". The card's action is a flow trigger (`POST /api/v1/automation/contract_intake/trigger`). - `packages/app-shell/src/views/DeclaredActionsBar.tsx` — `objects: objectDef ? [objectDef] : []`. One object, so any `objectOverride` param misses; and when the bar is driven by its `actions` prop, `useMetadataItem('object', undefined)` leaves `objectDef` undefined and the list is empty. - `ObjectView` and `RecordDetailView` pass the full list and were never affected. ### What the server serves, versus what the record form gets Read in `/home/user/objectstack` (read-only; nothing was committed, pushed or opened there): - `packages/spec/src/ui/action.zod.ts` — `ActionParamSchema` is `strictObject`, and its own docblock names the two declaration modes: field-backed `{ field }`, and inline, where "a `lookup` / `master_detail` param declared this way MUST name its target object via `reference`". So for a **field-backed** param the server serves no object binding *on the param at all*, and is not supposed to: the binding lives on the object's field def, and the client already holds it. - `packages/spec/src/ui/action-params.zod.ts` — "Field-backed params are resolved to their effective value-shape inputs by the CALLER (the runtime, which holds the object metadata registry)". The client side of that same contract is `resolveActionParams`, and its registry is `useMetadata().objects`. - `packages/spec/src/conversions/registry.ts`, `field-reference-to-alias` — the serve path canonicalises a stored `reference_to` to `reference` so objectui may delete its fallback arms. That interlock is intact here: `normalizeSchemaReferenceKeys` still stamps both spellings at ingestion in `MetadataProvider`, and a control leg confirms a def carrying only `reference_to` resolves its target once it has been through that door. ⇒ the object binding is on the client, in the same store the record form reads. The action-param seam simply never asked the store for it. ⛔ Nothing in the server's contract had to change, and nothing upstream is filed. ## The lit control `legD` in the new pin file drives two paths in one run, against the same field def: - **record form** — the object's `contract_type` def handed to the shared field widget the form renders. Clicking the trigger queries `clm_contract_type` and offers its records. This is the card's "it works there in this same build", measured here rather than quoted. - **action param, through the console runtime** — `useConsoleActionRuntime({ dataSource })`, `ConsoleShell`'s supply verbatim, under a metadata store that has the object. Same outcome now: a picker, a query naming `clm_contract_type`, and options offered. With the supply half reverted, the second leg alone goes red and the record-form control stays green — which is the difference the card asked to be located. ## The change 1. **`useConsoleActionRuntime`** resolves field-backed params against the caller's objects unioned with the metadata store's, **caller first** (`withKnownObjects`, exported beside the resolver with its own docblock). A draft or preview overlay the caller carries still wins on any name it defines. The handler awaits `ensureType('object')` first — it is already promise-returning by contract — so "this field does not exist" is an answer and not a cold-boot race, which is what makes the refusal below sound. 2. **`resolveActionParam`** stamps `unresolvedField` (the `OBJECT.FIELD` pair, as a string) when it cannot read a field-backed param, and says so in dev. The last-resort `text` shape stays — a partially cached console must not crash — but it no longer travels anonymously. 3. **`ActionParamDialog`** refuses such a param: an alert naming the pair in place of the control, a `console.error` per dialog opening **in every build**, and a disabled Confirm so the action cannot be launched with a value the dialog had no contract to collect. The refusal is deliberately not gated on `required` — a param that cannot be read is a broken declaration either way, and a silent optional one is the same invisible failure a submit later. `actionDialog.unresolvedParam` is added to all ten locale bundles. The `OBJECT.FIELD` locator renders as its own node rather than an interpolation, so an identifier is never re-ordered by a translation. ## Evidence Pins: `packages/app-shell/src/views/ActionParamDialog.fieldBackedPickerReach-10129.test.tsx` — 13 passing, four legs, every leg with a control beside it. Reverse verification. Each leg mutated one file on disk, proved the mutation by blob hash (and for the surgical one, by an occurrence count that had to move `1 -> 0`), ran the pins, then restored with `git checkout HEAD -- path` and proved the restore by an empty `git diff HEAD` — never by an exit code. A `trap … EXIT INT TERM` carried the restore. Run from the committed fix, so the red and green numbers are about committed bytes. | ablation | result | |---|---| | revert `useConsoleActionRuntime.tsx` to the branch base | **1 failed**, 12 passed — only "the picker renders and QUERIES the referenced object, from the supply ConsoleShell really uses" | | revert `ActionParamDialog.tsx` to the branch base | **2 failed**, 11 passed — both refusal legs, `Unable to find an element by: [data-testid="param-unresolved-contract_type"]` | | delete only the `unresolvedField` emit from the resolver | **4 failed**, 9 passed — `expected undefined to be 'clm_contract.contract_type'`, plus both refusal legs | | restored | 13 passed, `git diff HEAD` empty | Local checks, all run from the repository root after the merge of `origin/main` at `6cae9a0b3`, against `6d4f4da96`: - `pnpm exec vitest run packages/app-shell/ packages/core/src/actions/ packages/i18n/` — `Test Files 837 passed (837)`, `Tests 8925 passed | 1 skipped (8926)`. - `pnpm --filter @object-ui/core --filter @object-ui/app-shell --filter @object-ui/i18n type-check` — exit 0, after `pnpm --filter '@object-ui/app-shell^...' build`. - `check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`, `check:i18n-designer-parity`, `check:control-bytes`, `check:new-line-citations`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit` — all exit 0. - `node scripts/check-changeset-presence.mjs` and `node scripts/check-changeset-no-major.mjs` — exit 0. - `check:eager-locale-catalogues` and `check:eager-closure` — exit 0 after `pnpm --filter '@object-ui/console...' build`. The tenth locale key adds nothing eager: the nine non-`en` catalogues stay on demand and `en` stays the only resident one. ⛔ No ceiling in `scripts/check-eager-closure-budget.mjs` was touched. Lint, as a declared narrowing rather than a repo-wide run. `pnpm exec eslint --no-inline-config` over the five changed `.ts`/`.tsx` sources: exit 0, `0 errors`, 76 warnings, **0 of them new** — the two `react-hooks/refs` warnings in the report are the pre-existing `serverActionEnvRef` pair, not this diff's. The population is what `eslint.config.js` itself configures, the file count is read off `--format json` (5), and the narrowing is sound because this repo does not enable type-aware linting: no `parserOptions.project` and no `projectService` appear in `eslint.config.js`, so nothing in this diff can move a verdict on a file it did not touch. The full repo-wide `eslint . --no-inline-config` is CI's run, not this branch's. ## Acceptance notes Out of scope, observed, ⛔ not filed and ⛔ not changed here: - `RecordDetailView` builds a **second**, near-identical `paramCollectionHandler` that does not go through the store union. It is not a defect today — that view receives the full `objects` list as a prop, which is why it was never in the reported path — but the two handlers are documented as a pair that must not drift (`RecordDetailView.paramRuntimeParity-6431.test.tsx`), and they now differ by this union. Successor: whoever next edits either handler; the pin file names both. - `RuntimeField` in `resolveActionParams.ts` still declares `reference_to`, which no read in that file consumes any more (objectui#6837 narrowed the read to `reference`). Dead declaration, no behavioural effect — ingestion canonicalises both spellings. ⛔ The card's two further dialog defects — no max-height / internal scroll, and the file widget posting a browser blob — are each their own card and neither is touched here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq --- _Generated by [Claude Code](https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #3404
问题
packages/fields/src/widgets/FullscreenFieldEditor.tsx四处英文字面量:展开按钮的可访问名(Edit ${label ?? 'text'} fullscreen)、标题兜底Edit text、Cancel、Done。键一个都不缺 ——
form.fullscreen.*与common.cancel自 #3272 给内置分支做国际化起就在十个语言包里齐全,只是这条路径从没消费。可达性也不是理论上的:mobile.fullscreenLongText打开后ObjectForm给每个长文本字段盖mobile_fullscreen,于是同一张表单内,内置分支渲染的字段显示「取消 / 完成」,注册 widget 渲染的显示 Cancel / Done。改动
四处全部改为消费既有键,零新增键、十个语言包一行未动。另补上内置分支早已有的 sr-only
form.fullscreen.description,详见下。关键实现选择:走
useFieldTranslation()而不是 issue 正文提到的裸useObjectTranslation。 这一点是实测决定的,不是偏好。本包 widget 会被独立/嵌入宿主以及本包绝大多数测试不带I18nProvider裸渲染,而在改动前的树上实测裸 hook 在无 provider 时返回的是原始键:{{label}}插值也整个塌掉 —— 比它要替换的英文字面量更差。useFieldTranslation(createSafeTranslation)正是内置分支自 #3272 起用的形状,其英文兜底与原字面量逐字节相同,因此无 provider 的渲染结果完全不变。相应地,英文兜底项加在了本包既有的FIELD_DEFAULTS(useFieldTranslation.ts)里 —— 那是代码内的英文兜底表,不是语言包,仍是零新增 i18n 键;common.cancel本就在表内,直接复用。「顺带观察」的 DialogDescription:补了,但理由与 issue 猜测的不同
issue 猜测可能触发 Radix 的 missing-Description 警告。实测该警告不存在:锁定的
@radix-ui/react-dialog@1.1.23整个文件里一个console.*调用都没有,上游已移除该警告,baseline 跑也确实没有任何告警输出。补它的真实理由是另外两条:Radix 仅在存在 Description 时才设置
aria-describedby(descriptionPresent ? descriptionId : undefined),所以改动前这条路径的对话框根本没有可访问描述,而逐像素相同的内置对话框有;且form.fullscreen.description在此路径上是个无消费者的键 —— 与本 issue 主体是同一处漂移。同文件、零新键。测试
新增两个文件,合计 14 例,textarea 与 richtext 两个 widget 各覆盖:
__tests__/FullscreenFieldEditor.i18n.test.tsx—— en 正向(与旧字面量逐字节相同,证明 en 是 no-op)、zh/ja 正向 + 英文字面量反向断言(只做正向断言的话,重新内联的字面量挨着翻译过的兄弟节点照样能过);ja 是因为 toggle 名是插值的、两个包把通用名词放在句子两端,只测 zh 会蒙混过关。另含 label 插值、aria-describedby、以及提交/取消仍然接线正常。__tests__/FullscreenFieldEditor.no-provider.test.tsx—— 无 provider 的那条腿,含 Commandment #-1 的无 CJK 断言。为什么无 provider 那几例必须单独成文件(这是本 PR 返工过的一处): 它们最初写在 i18n 文件里。反向验证(把
useFieldTranslation换回裸useObjectTranslation,预期变红)时它们依然全绿,而同一次实验正确地把RichTextField.mobileFullscreen.test.tsx弄红了。原因是I18nProvider会调用initReactI18next,把实例装成 react-i18next 的全局默认;同文件内只要有一例挂过 provider,就再也没有「无 provider」状态可观测 —— 那几例是靠读泄漏的全局实例而绿的,属于「因为什么都没发生而通过」的假绿。拆成独立文件后,同样的替换实验让它们如实变红(aria-label="form.fullscreen.toggle"),这才算真的钉住。同样的拆分理由见TagsField.placeholder.no-provider.test.tsx。反向验证方向
Tests 6 failed | 2 passed,失败的正是 zh/ja 各例(Received: aria-label="Edit text fullscreen");通过的 2 例是两个en用例 —— 它们改动前后都绿,因为字面量与 en 包逐字节相同。这一点如实记录:en 用例是 no-op 保护,不是缺陷复现器。aria-label="form.fullscreen.toggle"原始键,其中一例是既有的RichTextField.mobileFullscreen.test.tsx(它按'Edit Release notes fullscreen'查按钮)。命令与结果
从仓根跑(包内跑会假绿 / 路径过滤被吞,见 #3288/#3378),并以文件名核对确实执行:
按消费半径而非编辑包扫了 fixture:
packages/plugin-form/src/__tests__/ObjectForm.mobileFullscreen.test.tsx在packages/fields之外也消费这个组件,已纳入上面的运行范围;packages/i18n的语言包 parity 套件一并跑过,确认没有包漂移。范围
严格限定在
FullscreenFieldEditor.tsx+ 测试 + changeset(patch),外加useFieldTranslation.ts里的英文兜底项 —— 那是让本次改动不产生回归所必需的最小外扩(理由见上),且不属于裁定禁止的 form.tsx / 语言包 / TextAreaField / RichTextField 任何一项。未碰 form.tsx,未碰任何 locale 文件,未改两个宿主 widget。Generated by Claude Code