Repository navigation
Commit e9e344a
fix(app-shell): editing an action in Studio no longer stops the object from saving (#26827)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbQRVsC49uBbvJrjUXbWLaCA)_
Fixes #26728
Fixes #26736
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:Studio 的动作编辑器(数据 › 对象 › 操作)三处修正:「开放给 AI 智能体」改写规范的嵌套 `ai`
块;切换「类型」时清掉新类型拒收的键(如 API 动作上的脚本 `body`);「请求体形态」改成「平铺 /
包裹(键)」控件。数据支柱对「只是还没填完」的动作暂存不发送并说明缺什么,对服务端拒收的保存在提示条里点名到具体动作,「定位」直接打开它。
- **为什么改**:这三处以前都会写出规范拒收的动作,之后对象的每次自动保存都被 422
拒绝,直到刷新页面,界面上也看不出是哪个动作出了问题。
- **风险与回滚**:低。不改规范、不新增元数据键、不碰服务端;只改设计器的写法、暂存判断与两种语言的文案。回滚即 revert 本 PR。
-
**先看**:`ui/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx`
的 `patchAi` / `commitType` /
`TYPE_SCOPED_KEYS`;`ui/packages/app-shell/src/views/studio-design/metadataError.ts`
的 `actionHeldInput`(暂存判断,以规范的 `ActionSchema`
为准);`StudioDesignSurface.tsx` 的 `showObjTarget`。
- **两分钟验证**:`cd ui && pnpm exec vitest run
packages/app-shell/src/views/studio-design/DataPillar.actionRefusal-26736.test.tsx`,预期
2 个用例全绿;或在 Studio 里勾选「开放给 AI 智能体」再取消,对象照常保存,不再出现 422。
- **需要你做的**:无,席位落地。
## What changes
Three controls in Studio's action inspector wrote an action
`@objectstack/spec` refuses, and the Data pillar then sent it, so every
later save of the object answered 422 until a reload:
| control | wrote | now writes |
|:--|:--|:--|
| Expose to AI agents, and its description | flat `aiExposed` /
`aiDescription` (refused by name) | the spec's `ai: { exposed,
description }`, merged into any `ai` block the draft holds; the next
write clears the flat keys an older draft carries; unticking drops a
description the spec would refuse as too short (its box hides while
unexposed) |
| Type | only `type`, keeping `body`, `onSuccess`, `outcomeMessages`,
`undoable: true` the new type refuses | `type`, plus `undefined` for
each of those keys the draft holds and the new type refuses
(`TYPE_SCOPED_KEYS`); everything the new type accepts, `target`
included, is kept |
| Body Shape | free text under "More fields" (`{"wrap":"payload"}`
became a string; a stored `{ wrap }` displayed as `payload`) | a curated
Flat / Wrap control with a key input: `'flat'`, or `{ wrap: KEY }` once
a key is typed (an empty key is never written; the spec would accept it
and nest under `""`) |
The refusal surface the inspector reports through
(`studio-design/metadataError.ts`, the Data pillar in
`StudioDesignSurface.tsx`, and `ObjectActionsPanel.tsx`):
- **Held, not refused.** `objectHeldEdit` now also asks
`actionHeldInput` of each action: the spec's `ActionSchema` judges the
action, and when every issue is an input the inspector offers that has
no value yet (a script source, a script's body when it has neither body
nor target, the `target` a type requires, an exposed action's AI
description absent or still too short) the object is held unsent, with
the neutral line "Not saved yet: API endpoint on the action “Send” needs
a value". Anything else is sent and its refusal shown.
- **A refusal is placed on the action.** `objectSaveRefusal` gains
`objectActionLocator`: an issue at `actions.INDEX…` names the action by
its label, and the input when the key under it is one the inspector
edits. "Show me" switches to the Actions view and selects that action
(`ObjectActionsPanel`'s new `focus` prop), from any view.
- **Recovery needs no reload.** The autosave already resends on the next
edit; with the writes above, that edit leaves a draft the spec accepts,
and the strip clears when the save lands.
`ActionPreview` read the flat `aiExposed` for its AI pill; it now reads
`ai.exposed`, since the inspector no longer writes the flat key (bounded
in-place fix, on the claim's amended surface). New `studio` namespace
strings, en and zh: the Body Shape control,
`engine.studio.refusal.action` / `actionInput`,
`engine.studio.held.actionNeedsInput`. No `packages/spec/**`, server or
new metadata key.
## Why
One visible checkbox, or an ordinary Type switch, stopped the whole
object from saving, with nothing on screen saying which action or why; a
reload discarded the edits. The spec already defines every shape written
here, so the defect was the designer's.
## Tests
- `ActionDefaultInspector.aiBlock-26728.test.tsx` (ask 1): the real
inspector, drafts parsed with `ActionSchema` after a JSON round trip. A
tick writes `{ ai: { exposed: true } }` and the only refusal is
`ai.description`; a 40-character description parses and reads back;
unticking parses with and without a description; a draft carrying the
flat keys parses after the control's next write; other `ai` keys are
kept; `AI_DESCRIPTION_MIN` is the spec's minimum both sides.
- `ActionDefaultInspector.typeSwitch-26736.test.tsx` (asks 2, 3): script
with a body → API drops the body, and the draft parses once the endpoint
is typed (and after a Method edit); every type-scoped key is cleared,
`undoable: false` kept; a switch with nothing to clear writes the type
alone and keeps `target`. Body Shape: a stored `{ wrap: 'payload' }`
reads back as Wrap / `payload`; Wrap writes nothing until a key, then `{
wrap: KEY }`; clearing the key takes the shape off; Flat writes
`'flat'`; the old JSON text is refused `invalid_union`. Parity: each
`TYPE_SCOPED_KEYS` entry parsed on every `ActionType`, refused exactly
off its listed types.
- `metadataError.actionHeld-26736.test.ts` (asks 2, 4): seven held
cases, each paired with the spec's own refusal; four finished-but-wrong
cases not held; the object-level hold names the action and selects it;
field holds still come first; refusals at `actions.N.body` and
`actions.N` are placed on the action; field paths unchanged; an
unplaceable path stays unlocated.
- `DataPillar.actionRefusal-26736.test.tsx` (asks 1, 2, 4): the real
Data pillar, real inspector, real `MetadataClient`, a transport that
parses each action with `ActionSchema` and answers 422 with `actions.N…`
issues. A refused save names the action; "Show me" from the Form view
opens it on Actions; tick + untick sends an accepted save and the strip
clears, no reload. Script → API is held (no PUT, held line names the
endpoint on the action), and typing it sends an accepted save.
- Moved fixtures: `automationPreviews.i18n-10862.test.tsx` and
`inspectorStrings.hookAction.i18n-10586.test.tsx` now carry `ai: {
exposed }` instead of the flat keys.
- Ablations (one-off, not kept), each through `node
scripts/ablation-replace.mjs` on committed head `27b93089`, each
restored to the HEAD blob with `git diff HEAD` empty: A1 `patchAi`
writes `aiExposed` → aiBlock 4 failed; A2 Type switch clears nothing →
typeSwitch 2 failed + DataPillar held case failed; A3 no action hold →
metadataError 1 + DataPillar 1 failed; A4 no action locator →
metadataError 2 + DataPillar refusal case failed. Control after restore:
Tests 36 passed (36).
## Acceptance notes
- `ai` stays editable under "More fields" (not curated), so `category`,
`paramHints`, `outputSchema` and `requiresConfirmation` remain
reachable; `exposed` and `description` are therefore editable in both
places, writing one block.
- A Type switch keeps `target`. Moving between two target types (a URL
to a flow) keeps the old value in the new type's picker, where an
unknown name shows as `(not found)`; the spec judges it on the new type.
- The hold's notion of "unfinished" is the spec's own refusal at those
inputs, so a whitespace-only script source, which the spec accepts, is
sent.
- `StudioDesignSurface.tsx` edit is about 20 lines in the Data pillar;
#26737 edits the same file's flow fold, so whichever lands second merges
`main`.
## Verification
All readings at head `27b93089` (base `c8583f80`), under
`scripts/pm/os-verify-lock.sh` where heavy.
<details><summary>Touched packages: @object-ui/app-shell,
@object-ui/i18n</summary>
- Prerequisite builds: `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/sdui-parser
--filter=@objectstack/lint --filter=@objectstack/client
--concurrency=2`, 36 tasks, VERDICT command-exit 0.
- Type-check: `cd ui && pnpm exec turbo run type-check
--filter=@object-ui/app-shell --filter=@object-ui/i18n --concurrency=2`,
65 tasks; both packages run `tsc --noEmit && tsc -p tsconfig.test.json`
(the test config covers the new tests); VERDICT command-exit 0.
- The four new test files plus every `ActionDefaultInspector*`,
`metadataError*`, `DataPillar*`, `ObjectActionsPanel*` file and the two
moved fixtures: Test Files 41 passed (41), Tests 322 passed (322). The
rest of app-shell is left to CI's Test UI shards.
- `pnpm --dir ui exec vitest run scripts/` (reads the i18n catalogues):
Test Files 178 passed, 2 skipped (180); Tests 5065 passed, 2 skipped;
VERDICT command-exit 0.
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 34 commands, each run with its exit code
captured before any pipe: 34 exit 0. PR-body gates read this body
through `GITHUB_EVENT_PATH` / `PR_BODY`.
- `dispatch-gates --ran`: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
</details>
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4c29f38 commit e9e344a
14 files changed
Lines changed: 1073 additions & 25 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views
- metadata-admin
- inspectors
- previews
- studio-design
- 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 | + | |
| 18 | + | |
| 19 | + | |
| 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 | + | |
0 commit comments