Skip to content

fix(fields): a text field that declares options reads the option label in its record cell - #28817

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-00j4m6
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-00j4m6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28792

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:文本类字段(text、textarea 等走文本单元格的类型)如果声明了非空的 options,列表和详情里的只读单元格现在显示匹配选项的标签(如 Backlog),不再显示存储值 backlog;存储值不变,匹配不到的值照原样显示,没有 options 的字段完全不变。
  • 为什么改:同一个 options 声明,图表维度和分组表头早就按标签显示,只有单元格显示原值,一个声明读出两种样子。席位路线是对齐(enforce),不是禁止声明。
  • 风险与回滚:低。只改只读显示,不碰编辑输入、表单、存储和 spec。仓内 platform-objects、examples、平台 apps、plugins、services 以及 hotcrm 实测没有任何非选择类字段声明 options,现有界面零变化。回滚即撤销本 PR。
  • 先看:ui/packages/fields/src/index.tsx(TextCellRenderer 的选项分支,和从 SelectCellRenderer 提出来共用的 findDeclaredOption);ui/packages/fields/src/__tests__/TextCellRenderer.declaredOptions-28792.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/fields/src/__tests__/TextCellRenderer.declaredOptions-28792.test.tsx,预期 7/7 通过(main 上两条标签用例红:显示 backlog 而不是 Backlog)。
  • 需要你做的:无,席位落地。

What changes

The seam. options is a spec key on every field type (FieldSchema.options). Two faces already honour a declared non-empty list on any type, on purpose: analytics dimension labels (service-analytics dimension-labels.ts, "the condition is the declared list itself, never the field's type") and grid group headers (plugin-grid objectGridGrouping.ts). The read-only text cell ignored it, so text + options: [{ value: 'backlog', label: 'Backlog' }] read backlog in a list or detail cell and Backlog in a chart or group header.

  • TextCellRenderer (ui/packages/fields/src/index.tsx). When the field carries a non-empty options array (the same test analytics applies, Array.isArray(options) && options.length > 0) and the stored value is a string or number, the cell draws the matching option's label through core's optionDisplayLabel. The full-text hover title carries the same label. Every type this cell draws gets the same reading (text, textarea, code, qrcode, time, auto_number, the widget faces registered to it, and the fallback for undeclared types).
  • Unchanged: the stored value; a value that names no option (reads as itself); a field with no list or an empty list (byte-identical markup, pinned); arrays and expanded records (keep the coerced text, since neither is one option's code); edit inputs and forms (a text field with options stays a free text input); SelectCellRenderer's output.
  • One matcher. The select cell's value-to-option match (exact, then case-insensitive) moves out of SelectCellRenderer's body into a module function, findDeclaredOption, which both cells call. No second matcher was written. The grid grouping's matcher lives inside a plugin-grid hook and is not importable from @object-ui/fields.
  • Translation follows the select cell: the labels arrive already translated, because every host that hands a cell its field (generateColumns in plugin-grid, DetailSection in plugin-detail) runs translateOptions over a non-empty options list whatever the field's type.
  • How options reaches the cell. Through the existing field prop of CellRendererProps; no published prop or type was widened. The per-type metadata (TextFieldMetadata) does not name options, so the cell reads it structurally, (field as { options?: unknown }).options, the way LookupCellRenderer reads its static options, and guards it with Array.isArray.
  • Changeset .changeset/28792-text-cell-declared-option-label.md: patch on @object-ui/fields, with both declaration lines.

Clause-② measured: no. The diff touches no packages/spec path, no published export (findDeclaredOption is module-private; TextCellRenderer's signature is unchanged) and no wire shape. It changes what one console cell draws for metadata the spec already accepts. Metadata-protocol: none: no key, accepted value or meaning an author writes moved.

Tests

  • Measure first (shipped count). An AST scan (TypeScript compiler API, Field.X({...}) calls and { type: 'X', ... } literals with an options key that is not an empty array literal) over the tree at base c064d3a2:
    • packages/platform-objects: 88 files, 8 options declarations, 0 on a non-option type.
    • examples/**: 161 files, 53 declarations, 0 on a non-option type.
    • packages/apps (account, connector-github, connector-stripe, setup, studio): 16 files, 0 declarations, 0.
    • packages/plugins + packages/services: 773 files, 32 declarations, 0.
    • hotcrm (src/, apps/, at a598256): 302 files, 80 declarations, 0 fields (8 hits are dashboard widgets of type table/area, not fields).
    • Positive control: a fixture with Field.text({ options: [...] }) and { type: 'textarea', options: OPTS } is reported as 2 hits, and a Field.select beside them is not.
    • So the change moves no shipped screen; it lines up the cell for any author who declares the list.
  • Red first, at base (the test committed alone as bb5573c2, cell file at base): TextCellRenderer.declaredOptions-28792.test.tsx failed 2 of 7, expected 'backlog' to be 'Backlog' and expected 'in_progress' to be 'In progress' (the textarea case). The five controls were green on base.
  • Green at 32dc0196: the file passes 7/7. With it, cell-truncation.test.tsx, formulaFaces.textFace-12357.test.tsx, fieldFaceSeam-13216.test.tsx and select-label-association-e2e.test.tsx: 5 files, 39/39.
  • Controls (same file): unmatched other reads other; an empty options: [] reads the stored value; a field with no options renders exactly span.block.max-w-full.truncate with title="backlog" and text backlog; SelectCellRenderer with the same list still reads Backlog; TextField (the edit input) of a text field with options holds the raw backlog.
  • Full @object-ui/fields suite at 7e05456b: pnpm --dir ui exec vitest run --maxWorkers=2 packages/fields/: Test Files 292 passed, 1 skipped (293); Tests 4399 passed, 7 skipped (4406). Lock VERDICT command-exit 0.
  • Type-check: ui/packages/fields pnpm run type-check (tsc --noEmit && tsc -p tsconfig.test.json; the test config includes src/**/*.test.tsx, so the new test is compiled): exit 0, after building the @object-ui/fields^... closure.
  • eslint on the two changed source files: 0 errors (152 warnings, all react-refresh/only-export-components on the barrel's existing exports; the new function is not exported).
Ablation (one-off, from committed HEAD 32dc019)

node scripts/ablation-replace.mjs replaced the cell's final return (the TruncatedText element whose text is option ? optionDisplayLabel(option) : String(safe)) with one whose text is String(safe), plus a marker comment, in ui/packages/fields/src/index.tsx: anchor 1 → 0, marker 0 → 1 (also grep -c = 1 on disk before the run), blob 630a20cf9f49 → 5fee7b9df6c0. TextCellRenderer.declaredOptions-28792.test.tsx: 2 failed, 5 passed, exactly the two label cases; every control stayed green. Restored: blob after restore 630a20cf9f49 equals HEAD, git diff HEAD empty. The ui suite imports the cell from source (../index), so no build sat between the mutation and the run.

Acceptance notes

  • Read-only form face not aligned (out of scope by the route). TextField's readonly branch (a read-only form field, not the cell) still prints the stored value. The order put edit inputs and forms out of scope; DetailSection's read-only rows draw through the cell and do align. A record view whose read-only face is the form widget would still read backlog. Noted, not filed: no shipped field declares the list (count above).
  • Case-insensitive match. Reusing the select matcher means a stored BACKLOG names the backlog option in the cell, as it does in a select cell; analytics and grid grouping match exactly, so that value reads Backlog in the cell and BACKLOG in a group header. Same split the select cell already has; noted, not filed.
  • Not touched, per the order: authoring lint, analytics, grouping, and sys_notification.topic.

Verification

  • Overlap re-read before opening: the 26 open PRs on objectstack-ai/objectstack at 21:2x UTC; every code PR's head was fetched and diffed against its merge base. None touches ui/packages/fields/src/index.tsx; the only one touching ui/packages/fields/ is the Dependabot group bump (chore(deps)(deps-dev): Bump the development-dependencies group with 2 updates #28499, package.json only).
  • origin/main moved from c064d3a2 to 70bf4343 while this ran (31 files); none under ui/packages/fields/ or ui/packages/core/src/evaluator/, so the branch was not merged forward.
Local derived gate loop, head 7e05456 (34 of 34, all exit 0)

node scripts/pm/dispatch-gates.mjs --commands derived 34 families for this diff; each was run with its exit code captured before any pipe; --ran reconciliation: "34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED (a DERIVED zero — all 34 recorded an exit code and none of them is 3)". Record: pm-orders/server/28792-gates-ran.txt in the project folder. Families include check-adr-0087-registration (plus self-test), check-changeset-no-major, check-empty-changeset (plus self-test), check-sdui-manifest (plus self-test), check-comment-mask-corpus, ui vitest scripts/, check:nul-bytes, check:published-files, check:test-source-alias, check:type-check-coverage, check:type-check-debt, check:undeclared-dep-imports, check:cross-package-test-inputs. Every family CI runs is read from this PR's check-runs, not re-run here.


Generated by Claude Code

A field that declares a non-empty options list now reads the matching
option label in the read-only text cell, the test analytics dimension
labels and grid group headers already apply on any field type. The
stored value is untouched, an unmatched value reads as itself, and a
field without a list is unchanged. The select cell's matcher moves to a
module function both cells call.

Claude-Session: https://claude.ai/code/session_01FEkQ5kmCTHo1Zn4w3D8EBp
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-01T21:39Z · head 7e05456b · #28792

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main with Fixes #28792 and a patch changeset for @object-ui/fields, declaring Clause-②: no and Metadata-protocol: none.
    • check-governed-merges --pr 28817 reads NOT governed (0 of 3 paths), +160 / -14. Landing tier S.
    • The claim (6563734011) names this PR's branch, claude/project-thread-00j4m6.
  2. Against the order (route: enforce):
    • The read-only text cell now shows a declared option's label, using analytics' test (a non-empty options array). The stored value is untouched, an unmatched value reads as itself, and a field with no list is drawn as before.
    • The labels arrive translated through the hosts' existing translateOptions.
    • Measured first: 0 shipped non-select fields declare options (platform-objects, examples, apps, plugins and services, and hotcrm). The positive control found 2 of 2. The stop condition was not met: options reaches the cell through the existing field prop, and no published prop or type widens.
    • Red first 2 of 7, green 7 of 7. The ablation reds exactly the 2 label cases.
  3. Seat's reading of the dev's two notes:
    • The cell reuses the select cell's matcher (exact, then case-insensitive), as the order asked: one matcher for the two cells that draw a field's options. Analytics and grouping match exactly. The two can differ only for a stored value whose case differs from every declared option's, and there are 0 shipped declarations on non-select types. Accepted, no card.
    • The read-only branch of the text form input stays as it is, because forms are out of scope in the order. No card.
  4. Contract review: not owed. Clause-②: no holds by the diff: no spec path, no published export, and TextCellRenderer's signature is unchanged.
  5. Gates: the dev reports 34 of 34 derived families exit 0 and the fields suite at 4399 passed. CI on this head has 0 red so far (11 pending), and auto-merge waits for it.
  6. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 21:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 21:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 3e05ed1 Nov 1, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-00j4m6 branch November 1, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

1 participant