Repository navigation
Commit e41642d
feat(studio): the formula editor inserts a field by label and reads a CEL syntax fault in plain words (#27901)
domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg4i9JiKjEjwtuVzd4gKy9hB)_
Fixes #27837
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:Studio 公式编辑器加了「插入字段」(按字段标签选,插入 `record.字段名`);CEL
语法错误先给一句白话,再用等宽字体原样显示引擎的错误和 ^ 位置标记;打字时新出现的报错会自动滚到可见处。
- 为什么改:RC 走查里公式框只有裸文本框、报错是 "Unexpected token: EOF … ^" 这类技术话,且报错行落在
Remove 按钮那一栏下面看不见(浏览器实测复现)。
- 风险与回滚:只动 Studio 的 CEL 编辑器与 studio 文案(en/zh);生产者 `@objectstack/formula`
的 message 不变,存盘门禁与客户端校验读到的文字不变;回滚即 revert 本 PR。
- 先看:`CelPredicateField.tsx`(插入字段与滚动)、`celAuthoring.ts`(按 refusal code
加白话首句)、`ObjectFieldInspector.tsx`(传入带标签的字段清单)。
- 两分钟验证:Studio 打开任一对象的公式字段,点「插入字段」选一个字段,框里出现 `record.字段名`;再输入
`record.amount *`,下方先出白话一句,再出带 ^ 的引擎原文,且这一行在 Remove 按钮上方可见。
- 需要你做的:无,席位落地。
## What changes
The formula field's **Formula (CEL)** editor is the shared
`CelPredicateField`. It gains:
1. **Insert field.** With the new optional `fieldOptions` prop (`{ name,
label }[]`), the editor shows an **Insert field** picker: a searchable
list of fields by label (machine name beside it), matching on either.
Choosing one inserts the CEL reference at the caret, replacing any
selection: `record.` plus the name in `record` scope, the bare name
otherwise. The caret lands after the insert. `ObjectFieldInspector`
passes the object's other fields (not the formula field itself, which
may not reference itself), labelled through the same `useSafeFieldLabel`
resolver the canvas and grid use; a field with no label shows its name.
No other host passes `fieldOptions`, so no other editor shows a picker.
2. **A syntax fault in plain words.** `lintCelPredicate` now reads the
refusal `code` and `params.detail` that `@objectstack/formula`'s
`validateExpression` already returns (its `expression-refusal.ts`
vocabulary exists so a localized surface keys its own sentence to the
code). For the catch-all CEL code `invalid-cel` the lint issue carries
`plain: { key, detail }`, and the editor leads with
`engine.celLint.invalidCel` (a plain sentence) followed by the engine's
`detail` (its fault plus the source excerpt with the `^` mark) in a
monospace block, so the mark lines up under the spot. `message` is
unchanged, so the Save gate, `clientValidation` and the server-verdict
dedupe read the producer's words as before. Every other refusal (an
`unknown-field` with its did-you-mean, `cel-unknown-function`,
`cel-method-call`, `cel-template-brace`, …) keeps its message: those
already carry a prescription for their fault.
3. **The finding stays in view.** When the editor has focus and its
findings change, the findings list is scrolled into view (`block:
'nearest'`), so in a scrolling inspector a new error line no longer
lands below the visible area where the **Remove** footer sits. A finding
present on mount does not scroll the panel.
Studio i18n (en, zh) gains `perm.cel.insertField`,
`perm.cel.searchFields`, `perm.cel.noMatchingFields` and
`engine.celLint.invalidCel`. Root changeset: `@object-ui/app-shell`
patch, `@object-ui/i18n` patch.
## Tests
- `CelPredicateField.formulaAuthoring-27837.test.tsx` (11 tests): the
lint against the REAL `@objectstack/formula` (`record.estimated_hours *`
→ `invalid-cel` with `plain`; `record.estimated_hour * 2` →
`unknown-field` keeps its message, no `plain`; an inherited property
name as a code gets no lead); the editor renders the plain key then the
excerpt in a `pre` with the `^`; Insert field lists by label, inserts
`record.estimated_hours` at the caret with the caret after it, replaces
a selection, searches by label, inserts the bare name in flattened
scope, and is absent without `fieldOptions` or when read-only; findings
scroll into view under a focused editor and not on mount.
- `ObjectFieldInspector.formulaFieldPicker-27837.test.tsx` (1 test): the
real inspector offers the siblings `name`, `estimated_hours`,
`hourly_rate` (not `est_cost` itself) and choosing **Estimated Hours**
writes `expression: 'record.estimated_hours'`.
<details><summary>Ablations (one per behaviour, each restored to HEAD by
the tool)</summary>
All through `node scripts/ablation-replace.mjs … -- bash
scripts/pm/os-verify-lock.sh -c "pnpm --dir ui exec vitest run (the two
new files)"`; app-shell tests import `src`, so no dist build is
involved.
- **plain**: delete the `'invalid-cel'` row of `PLAIN_LEAD_CODES` →
anchor x1→x0, blob 98f36c1→61e831add2c1 → `Tests 4 failed | 7
passed (11)` (the two plain tests, and the two scroll tests, which wait
on the plain line) → restored, blob == HEAD.
- **picker**: insert the bare name in `record` scope → anchor x1→x0 →
`Tests 3 failed | 8 passed (11)` (both caret-insert tests and the
inspector test) → restored.
- **scroll**: delete the `scrollIntoView` call → anchor x1→x0 → `Tests 1
failed | 10 passed (11)` → restored.
</details>
## Acceptance notes
- **The footer overlap, measured once in a browser before any layout
change** (as the card asked): a scratch Vite bundle of the real
`StudioInspector` + hosted `InspectorShell` (footer
`InspectorRemoveButton`) + `CelPredicateField`, real Tailwind theme,
Chromium at 1280×720, a filler above the editor so it sits at the bottom
of the scroll area, then typing `record.estimated_hours *`. Without the
scroll (control leg, `scrollIntoView` stubbed out): scroll viewport
68–622, error line 627–720, `elementFromPoint` at the error line = the
Remove footer (`div.border-t.px-4.py-2.5`), so the line is hidden under
the footer. With this PR: error line 529–622, inside the viewport,
`elementFromPoint` = the error line. In both legs the footer does NOT
overlay the content box (footer top 622 = viewport bottom 622), which
agrees with #27830's code read: the shell is not wrong; the line simply
lands below the visible area. The probe was not committed.
- **The producer seam, named.** The plain sentence is one per refusal
code. `@objectstack/formula` collapses every CEL parse fault into
`invalid-cel` with the engine's text as `detail`; cel-js itself knows
the fault's code (`unexpected_token`, `unterminated_string`, …) and its
range, and `EvalError` in `packages/formula/src/types.ts` declares
`pos?: { start, end }` ("Source position when known") that
`classifyError` never fills. A per-fault plain sentence ("the formula
ends too early", "a quote is not closed") and a "at character N" would
need the formula package to carry cel-js's parse code and range into the
refusal params. That is a change to a published package outside this
card's file surface: filed by the seat as #27904 (`domain:engine`), not
made here.
- `invalid-cel` also covers type faults (`1 + 'a'` → "no such overload:
int + string"), so the plain sentence is worded for both: something is
missing, extra, out of place, or doesn't fit the values beside it.
- `ui/AGENTS.md` #2 (docs): no hand-written page under `content/docs/`
describes the Studio formula editor (only release notes mention it,
which are release-owned), so no doc page changed.
- The picker is offered only on the formula editor. The field-rule
editors (`visibleWhen` / `readonlyWhen` / `requiredWhen`) share the
component and could pass `fieldOptions` too; not in this card.
## Verification
All local readings below are on head `261cf0a4` (the branch with
`origin/main` merged in, no conflict).
- `pnpm --filter @object-ui/app-shell run type-check` and `pnpm --filter
@object-ui/i18n run type-check`, after `pnpm --filter
'@object-ui/app-shell^...' build`: lock VERDICT command-exit 0.
- Affected tests: every `ui/` test file that names `CelPredicateField`,
`celAuthoring`, `ObjectFieldInspector` or `lintCelPredicate` (100 files,
the two new ones included) plus `packages/i18n/`: `Test Files 191 passed
(191)`, `Tests 2422 passed | 13 skipped (2435)`. The whole `ui/` suite
is left to CI's Test UI.
- `pnpm --dir ui check:new-line-citations`: `0 new citation(s)`.
<details><summary>Derived gates: 34 families, each exit captured before
any pipe; dispatch-gates --ran reconciles 34 run, 0
NOT-MEASURED</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
Every other family CI runs (lint, type-check lanes, Test UI, Build and
E2E UI) is read from this PR's check-runs, not re-run here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbrXDpQYUAJ8MKZpNL)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 813fd9b commit e41642d
8 files changed
Lines changed: 484 additions & 9 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views/metadata-admin
- inspectors
- i18n/src/namespaces/studio
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
0 commit comments