You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit aa874c8
Browse filesBrowse the repository at this point in the historyBrowse files
fix(studio): a Data rail switch sends the pending edit first; a broken CEL formula stays held after the inspector closes and keeps its returnType (#27511, #27485) (#27617)
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)_
## 维护者速读
- **改了什么**:Studio 数据栏切换对象前,先用 `useDraftAutoSave` 现有的 `flush` 把 1.5
秒防抖内的编辑发出去,存不了的先问;CEL 拦截从「检查器开着」改为跟草稿走,关检查器不再放行坏公式;坏公式不再清掉字段已存的
`returnType`。
- **为什么改**:#27511 切换对象会静默丢掉 1.5 秒内的编辑;#27485 关检查器会把解析不过的公式自动存成草稿(还丢了
`returnType`),切换对象会静默丢掉被拦的编辑。
- **风险与回滚**:只动 app-shell 的数据栏和字段检查器,加一条 i18n 文案;没有第二条自动保存路径。三个按卡的提交可单独
revert;关检查器那一臂是独立提交 73b0ad8(见 Acceptance notes 的 objectui#4306 子决定 A
冲突)。
- **先看**:`StudioDesignSurface.tsx` 的 `openObject` 与 CEL
判定保留(`celVerdicts` / `fieldCelStamp`);`ObjectFieldInspector.tsx` 的
`settleFormulaType`。
- **两分钟验证**:数据栏打开一个对象,加一个字段后 1 秒内点另一个对象,再点回来,字段还在;公式字段输入
`record.est_hours > (`,按 Esc 关检查器,出现「未保存」提示,点名该字段并带「Show me」,且没有发出保存。
- **需要你做的**:裁定 objectui#4306 子决定 A 与 #27485 关闭臂的冲突(Acceptance notes 第 2
条);维持原裁定就 revert 73b0ad8,其余照常合并。
Fixes#27511Fixes#27485
Clause-②: no
Metadata-protocol: none
## What changes
**#27511, the rail switch inside the autosave delay.** The Data pillar's
rail now opens an object through one `openObject` gate. With an edit
pending inside the 1.5 s debounce, it calls the autosave's existing
`flush` and opens the next object once that save has landed; a save
already in flight is waited out the same way. An edit that cannot be
sent (its save was refused, or Studio holds it as unfinished) goes
through the existing `confirmDropUnsavedWork` question; declining keeps
the open object and its edit. The `requestedSurface` deep-link path uses
the same gate. No second autosave path: the timer, the target guard and
the single-flight of `useDraftAutoSave` are untouched (only its comment
now names the caller that flushes).
**#27485, returnType.** The field inspector stamped `returnType` from
the formula's inferred type on every edit, and a source that does not
parse infers `unknown`, so each broken keystroke cleared the stored
type. `settleFormulaType` now settles the stamp only once the editor's
lint verdict and inference verdict are both in for the same source: a
source with a lint error keeps the stored type; a clean source stamps
its proven type, or clears it when none is proven, as before. The order
of the two async callbacks does not matter.
**#27485, switch arm.** A switch over a held edit (a CEL error) never
flushes; it asks through the same gate as above.
**#27485, close arm (own commit 73b0ad8).** The inspector's CEL count
used to be keyed to the open field selection, so closing the inspector
(Close, Escape, another field card, another tab) zeroed it and the
autosave sent the broken expression. The pillar now keeps each inspector
verdict with a stamp of the field's CEL-bearing properties and the field
list (`fieldCelStamp`). A kept verdict holds the save while the draft
still carries that exact text, and ends when that text is edited, the
field is removed, or the object reloads. While it holds, the pillar
shows the existing held line with a new clause
(`engine.studio.held.celInvalid`, en + zh) and **Show me**, which
reopens the field; Studio's Publish refusal names it through the
existing held-edit report.
## Why
Both cards are one defect form: a pending or held Studio draft edit was
tied to the current selection (open inspector, object in the rail)
rather than to the draft, so changing the selection dropped the edit or
released its hold. Triage asked for exactly this: "The rail switch calls
useDraftAutoSave's existing flush, or confirms first … with no second
autosave path" and "Tie the CEL hold to the draft rather than to the
inspector's selection, and preserve returnType."
## Tests
- `DataPillar.railSwitchFlush-27511.test.tsx` (new, 4): flush then
switch sends one save to the old object with the new field and none to
the new one; a save in flight is waited out; a refused save asks, a
declined ask stays, an agreed ask drops it unsent; a switch with nothing
pending opens at once and sends nothing.
- `ObjectFieldInspector.returnTypeHeld-27485.test.tsx` (new, 3): the
card's repro keeps `number`; a source that parses again stamps `text`; a
clean source that proves no type still clears it.
- `DataPillar.celGate.test.tsx`: new switch-arm case (asks; declined
stays; agreed drops unsent); close arm: the hold survives the Close
button, Escape, another field card and the Records tab, and the held
line names the field; **Show me** reopens it and a fix releases the
hold; removing the field ends the hold. The old case "drops the count
when the inspector closes, so the draft cannot wedge unsaved" is removed
(see Acceptance notes).
- `StudioDesignSurface.autosaveSwitch-11232.test.tsx`: the Data control
case now expects the pending edit to go to its own object and still
nothing to the newly opened one.
Ablations, each through `scripts/ablation-replace.mjs`, restored to HEAD
with an empty `git diff HEAD` afterwards:
| Ablation | Result |
|---|---|
| Rail onClick back to a bare `setCurrent(o)` | railSwitchFlush 3 of 4
red (nothing-pending case stays green, as it should) |
| `settleFormulaType` without its `errors > 0` guard | returnType repro
red; the other two green |
| `confirmDropUnsavedWork` line removed from `openObject` | refused-save
case and the celGate switch-arm case red |
| Kept-verdict condition forced false | celGate 5 red (four closers and
Show me) |
## Acceptance notes
1. **File surface widened, seat-approved.**
`ui/packages/i18n/src/namespaces/studio/en.ts` and `zh.ts` gain one key,
`engine.studio.held.celInvalid`, for the held clause. The ui seat 3 seat
approved this widening at 13:14:22Z on 2026-10-29.
2. **Conflict with objectui#4306 sub-decision A (ruled by the maintainer
2026-10-29T15:17Z: the close arm stays; sub-decision A's reset-on-close
mechanism is replaced, its two invariants stand).** The base test file
restated the ruling as: "the host RESETS its own count when the
selection changes or the inspector unmounts (ruling sub-decision A) —
writability must never depend on a child's teardown running, or closing
the panel on a faulty formula leaves Save wedged shut with no editor on
screen to fix it", and pinned it with the case "drops the count when the
inspector closes, so the draft cannot wedge unsaved". #27485's triage
asks for the opposite at the close: "Tie the CEL hold to the draft
rather than to the inspector's selection". This PR follows the card and
keeps the ruling's two invariants instead of its mechanism: writability
does not depend on a child's teardown (the verdict lives in host state,
stamped to the draft), and the draft never wedges with no editor on
screen (the held line names the field and **Show me** reopens it;
editing the text, removing the field or reloading ends the hold).
objectui#4306 itself was not readable from this session (403), so the
quote above is the in-repo restatement. The close arm is its own commit,
73b0ad8, separable: at its parent 19ceb5d the four touched suites pass
25/25, so if the ruling stands, reverting 73b0ad8 leaves #27511,
returnType and the switch arm intact (#27485 would then stay open for
its close arm).
Ruling-ref: 6500685972
3. **Commit shape deviation.** The dispatch asked for one commit per
card. A WIP commit holding both cards' code (5002d07) was pushed before
the split; with no force push allowed, it stays, and 59a0510 takes the
close arm back out so the following commits are one per card (e72223f#27511, 19ceb5d#27485 returnType and switch arm, 73b0ad8#27485 close
arm). A merge of `main` brings in #27340, which also touches
`ObjectFieldInspector.tsx` in another region; no conflict.
4. **Residual windows (not fixed here).** The inspector lints 250 ms
after the last keystroke. Escape or a switch inside that window can
still read the previous verdict (a stale 0 releases nothing new, but the
last keystroke's fault is unjudged). Reopening a held field: the
inspector's mount report of 0 releases the hold for about 250 ms until
its lint runs. Closing inside the window right after a fix can leave a
false hold; reopening the field clears it.
5. **Same defect class, not measured.** The panel family's CEL hold
(Validations and the other tabs, keyed by tab) has the same
selection-keyed shape. Out of scope for both cards; noted, not filed.
6. `CelPredicateField` was not touched; the returnType fix is wholly in
the inspector.
## Verification
On head efb181a (after merging `main` at 1ed3360):
- `pnpm --dir ui exec vitest run
packages/app-shell/src/views/studio-design
packages/app-shell/src/views/metadata-admin/inspectors`: 460 files, 4610
passed, 1 skipped, 0 failed (under the verification lock).
- The four touched suites at 19ceb5d (the close arm's parent): 25/25;
at head: 30/30.
- `pnpm --filter @object-ui/app-shell type-check`: exit 0 (after
rebuilding `@object-ui/core`, whose new `measureCellCurrency` export
#27340 brought in; the first run read the stale dist). `pnpm --filter
@object-ui/i18n type-check`: exit 0.
- `eslint` on the 9 changed TS/TSX files: 0 errors (warnings are
pre-existing `react-hooks/set-state-in-effect` lines elsewhere in the
surface).
- `node scripts/pm/dispatch-gates.mjs --ran`: 34 derived, 34 run, 0 not
measured. 33 exit 0. `pnpm --dir ui exec vitest run scripts/` exited 1
on five 15 s timeouts while the 460-file run held the box; alone under
the lock it was 1 timeout (`check-vi-mock-inherit`, a whole-tree sweep
at 16 s), and that file alone is 83/83 green. None of the five reads a
file this PR touches.
- `node scripts/check-sdui-manifest.mjs`: exit 0. `node
scripts/check-nul-bytes.mjs`: exit 0.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the changed files has 0 errors (the
full `pnpm lint` is left to CI).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request. (two folded cards, one PR, per the dispatch)
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (the maintainer brief
is in Chinese by the requester's standing rule)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbCrKUjGeTReNEmUJx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(studio): closing the field inspector keeps a formula that does not parse unsaved, with the field's stored result type, and a switch to another object asks before it drops it (#27485)
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
- The Data pillar holds a field's draft while its CEL formula or condition does not parse. That hold used to end the moment the field inspector closed (Escape, Close, another field card, another tab), and the autosave then sent the broken expression as the object's draft, which publish refuses. The hold now belongs to the draft: it lasts while the draft still carries the text the inspector judged, and ends when that text is edited, the field is removed or the object reloads. While it lasts, the pillar shows the held line ("Not saved yet: an expression on the field “F Total” has an error to fix…") with a **Show me** that reopens the field, and the Studio's Publish names it.
12
+
- Typing a formula that does not parse (`record.est_hours > (`) cleared the field's stored `returnType`, because a broken source proves no type. The stored type now stays while the formula has a lint error; a formula that parses stamps its proven type, or clears it when none is proven, as before.
13
+
- With such an edit held, switching to another object in the Data pillar's rail dropped it silently. The switch now asks first; declining keeps the object and the edit.
fix(studio): switching objects in the Data pillar's rail sends an edit made inside the autosave delay instead of dropping it (#27511)
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The Studio autosave sends a draft 1.5 s after the last edit, and sends nothing once another object has been opened. An edit made less than 1.5 s before a click on another object in the Data pillar's rail (a field removed, a label renamed) was therefore dropped, with nothing on screen, and was gone after a reload. The switch now sends that edit to its own object first and opens the next object once the save has landed; a save already in flight is waited out the same way. An edit that cannot be sent (its save was refused, or Studio holds it as unfinished) is not dropped silently: the switch asks first, and declining keeps the open object and its edit.
0 commit comments