Skip to content

Commit d8be368

Browse files
feat(app-shell): Studio's action New menu opens on common actions, with the blank action under Advanced (part of objectui#11861) (#11977)
Part of #11861 — the actions entry only. The automations entry stays for a later claim, so the card stays open. Clause-②: no ## What changes In an object's **Actions** view, **New** used to add one action straight away: "New action", an update of this record with no field values yet (objectui#11820's skeleton). It now opens a menu, as the Validations view's does since objectui#11931: | Preset | What it writes | When it is written | |---|---|---| | **Change a picklist field** | `operation: 'update'`, `params: [{ field }]` on the object's first author picklist, labelled "Change Status" for a Status field. The click asks for the new value and the platform writes it, as the caller. | At once. Disabled, saying "Needs a picklist field on this object.", when there is none the author may change. | | **Run a flow** | `type: 'flow'` | Held until the editor's **Flow name** is given | | **Open a web page** | `type: 'url'` | Held until the editor's **URL** is given | | **Open a page in a dialog** | `type: 'modal'` | Held until the editor's **Modal / page name** is given | The blank action is folded under **Advanced**, still named "New action", and it writes exactly what New wrote before (the same object literal, untouched). objectui#11820's `newActionPin` now clicks Advanced, then New action; it has no assertion changes. Every preset starts from the blank action's seed: a fresh name, the "New action" label, this object, and `record_header` placement. ## A preset that needs a target waits for it The spec refuses a `url`, `flow`, `modal`, `form` or `api` action without a `target`, and only the author knows which flow, address or page they mean. Such a preset is therefore **held in the panel**, the way a new validation rule waits for its condition (objectui#11820): - It is listed with "Not saved — needs “Flow name”". - The detail pane shows the Data pillar's held line (`engine.studio.held.line`, objectui#11786): "Not saved yet: the action “New action” needs “Flow name”. Your changes are kept here and saved once it is filled in." - The target input is the **action editor's own** (`ActionDefaultInspector`, not edited). For a flow or a page it is a picker of the app's names, and free text when there are none. - The first patch that gives the action a target writes it to the object draft. From then on it is an ordinary action. - **Delete** drops it unsent. - It stays on the object it was started on, and it never shows on another object's list. - Known cost, the same as the validation hold: leaving the view or the object first drops it, and Publish does not wait for it. **Which presets wait is decided by the action as it stands, not by a second list.** `heldInputKey` reads `TARGET_INPUT_KEYS`, the target-requiring types mapped to the editor's catalogue key for each target input. The keys are reused rows, so no new row was added for them. If the author switches a held action's Type to one without a target, the next patch writes it. ⛔ No preset writes a script (objectui#11921's ruling A). ## Why these four, and what is not offered - **The objectui#11820 skeleton moved under Advanced** rather than becoming the first preset (the claim allows either). With an empty `patch` and no `params`, the platform answers a click with a refusal. "Change a picklist field" is the update a click can run as soon as it is written. - **"Open a form" is not offered.** The spec's form target is the form view's name, written as the object name, a dot, and the view key. The editor's form picker lists `view` metadata names, and I did not verify that those are such keys. "Open a page in a dialog" targets a page, which the picker lists by name. - **"Call an API endpoint" is not offered.** It needs both an endpoint and a request contract. It stays reachable from the blank action's Type control. ## The spec, measured rather than claimed The runtime does not import `ActionSchema`. That keeps the schema out of the console's first load, for the same reason objectui#11921 writes out its body-key table. The two facts the module writes out are each re-derived from the spec in the new pin, `ObjectActionsPanel.presets-11861.test.tsx`: - **Types that need a target.** For every `ActionType`, the action is held exactly when `ActionSchema` refuses it at `target`, and with a target nothing is held and nothing is refused there. - **Field-name grammar for a param.** For eight names (`status`, `st`, `s`, `Status`, `_status`, `status_2`, `2status`, `sta-tus`), the picklist is offered exactly when the spec accepts it as `params.0.field`. It also pins the class. Every preset in `ACTION_PRESETS` ends as an action that `ActionSchema` and `ObjectSchema` accept, the parse the object-draft save applies. For held presets that means after the author types the target into the **real** registered `ActionDefaultInspector`. The rest of the pin covers: - the menu shape: 3 to 5 presets, and Advanced folded on every opening; - the exact written shapes; - picking a flow through the editor's picker; - Delete; - the object scoping; - the disabled preset, which never takes a `system`, hidden or read-only picklist. ## Ablations Run on head `67e28442c` through objectstack's `scripts/ablation-replace.mjs` in WRAP mode. Each anchor hit exactly once and moved 1 to 0. The blob changed, and each restore was proven: blob equal to HEAD, and `git diff HEAD` empty. The control run before them was 26 of 26 passing in the new pin. | Guard removed | Pin result | What went red | |---|---|---| | The hold: `heldInputKey(fresh) === null` replaced by `true`, so every preset is written at once | 6 failed, 20 passed | the three held presets' spec parse; "Run a flow" is held; Delete drops it unsent; it stays on its object | | `!f.system` in the picklist choice | 2 failed, 24 passed | "Change" asks for Status, not the platform's Sync State; the disabled row | | `!f.readonly` | 1 failed, 25 passed | the disabled row: the read-only Stage was taken | | the param-field grammar check | 5 failed, 21 passed | `s`, `Status`, `_status`, `2status`, `sta-tus` offered though the spec refuses them | | the held list's object filter | 1 failed, 25 passed | a held action shows on another object | | the `api` row of `TARGET_INPUT_KEYS` | 1 failed, 25 passed | the `api` parity case against `ActionSchema` | The first round of the same six, on `ace58cbf1`, read identically. ## Gates Every gate below ran on head `67e28442c` (`git rev-parse --short HEAD` after the last commit). The branch is cut from `main` at `d7e9e9ab6`. **Tests**, under the container's verify lock, with repo-root `pnpm exec vitest run`: - **The named set: 23 files, 348 tests passed.** It is the new pin, the objectui#11820 pin, every test naming `ObjectActionsPanel`, `ActionDefaultInspector` or `actionPresets`, the column-identity ratchet, and the three tests that read the designer table's source. - **The whole `studio-design` directory: 147 files, 961 tests passed.** - Lock verdict: `VERDICT command-exit 0`. **Type-check:** `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`) exited 0, after the dependency closure was built under the lock (`Tasks: 28 successful, 28 total`). `--listFiles` on the test project lists the new pin, the objectui#11820 pin and `actionPresets.ts`. **Lint:** `pnpm exec eslint` on the 5 touched TS files: 0 errors, 0 warnings. The first commit drew two `react-hooks/preserve-manual-memoization` warnings; the second commit removed them. **Other gates**, each with its own verdict line: - `check:i18n-keys`: 0. "Every in-scope call-site key resolves against the en pack …" - `check:i18n-designer-parity`: 0. "Every en row has a zh row, and every shared row carries the same placeholders." - `check:i18n-drift`: 0. "No en value changed in this range." - `check:i18n-dead-keys`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:unreferenced-sources`, `check:spec-symbols`, `check:esm-specifiers`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:test-path-roots`, `check:handler-key-reads`, `check:metadata-write-doors`: all 0. - `check-changeset-presence`: 0. "5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11861-action-presets.md." - `check-changeset-no-major`: 0. - `check:new-line-citations`: 0. "0 new citation(s)". - `check:control-bytes`: 0. **Not measured:** - `check:eager-locale-catalogues` and the eager-closure budget. Prerequisite not met: no console build here. CI's Bundle Analysis owns both. - A real-browser pass. There is no objectstack backend serving Studio in this container; the behaviour is pinned in happy-dom with the real action editor. ## Docs-driven rule The Actions row of `content/docs/guide/console.md` does not describe **New** today, and that file is outside this claim's file list, so it is not edited here. Suggested sentence to append to that row: "The **New** menu opens on common actions in plain words (*Change a picklist field*, *Run a flow*, *Open a web page*, *Open a page in a dialog*); a flow, web-page or page action is saved once it has its target. The blank action is under **Advanced**." The `packages/app-shell/README.md` does not describe this menu. ## First-load bytes The new designer-table rows are eager, as objectui#11937 measured, and the PM's note put the headroom at about 1.0 KB. So the copy is minimal: **10 new keys** (en and zh), down from 15 in the first commit. - *Advanced* reuses the Validations menu's own row (`engine.studio.rules.advanced`). - The blank action keeps its existing name row (`engine.studio.actions.newLabel`). - The three presets that wait for a target carry no description line. Their labels say what they do, and the held line names the input once picked. - The held line itself reuses `engine.studio.held.line`. The input names reuse the editor's own target-label rows. As a local proxy, `gzip -9` of `metadata-admin/i18n.ts` grows by 322 B against the merge-base. That figure includes one source comment, which the build strips. The console build was not run locally, so **the budget bot's reading on this PR is the measurement**. If it reads over the ceiling, this PR stays a draft. ## Acceptance notes - These are read-only observations, not filed (carrier: none for both): - The action editor's **Type** control writes `{ type }` alone. Switching an existing action to `url`, `flow`, `modal`, `form` or `api` composes a draft that `ActionSchema` refuses at `target`, and the Data pillar's hold (objectui#11786) does not cover actions. Measured only through the spec; not through a public entry. - The editor's `form` target picker lists `view` metadata names. Whether those are form view names in the spec's dotted form is unverified. - No focus move into the editor's target input after a held preset. The Validations hold focuses its own condition group, but here the input belongs to the registered editor, and the held line names it instead. Session: `https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU` --- _Generated by [Claude Code](https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0ab2b98 commit d8be368

6 files changed

Lines changed: 787 additions & 23 deletions

File tree

‎.changeset/11861-action-presets.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
Studio's action "New" menu opens on common actions in plain words, with the blank action under "Advanced" (part of objectui#11861: the actions entry).
6+
7+
"New" in an object's Actions view used to add one action, "New action": an update of this record with no field values yet (objectui#11820). It now opens on four starting points, each an action of a shape the spec already has:
8+
9+
- **Change a picklist field** — asks for a new value of the object's first picklist (its own: never a system, hidden or read-only one), then saves it. It is the declarative record update with that field as its one input (`operation: 'update'`, `params: [{ field }]`), labelled "Change Status" for a Status field, and written at once.
10+
- **Run a flow**, **Open a web page**, **Open a page in a dialog** — a `flow`, `url` or `modal` action. The spec refuses each of these without a `target`, and only the author knows which one they mean. So, like a new validation rule waiting for its condition (objectui#11820), the action is listed as not saved and kept in the panel until the action editor's own Flow name, URL or page input is filled in. The first value given writes it. Delete drops it unsent, and leaving the view before then drops it too.
11+
12+
The menu row for "Change a picklist field" names the field it will ask about. When the object has no picklist the author can change, the row is listed disabled, saying what it needs.
13+
14+
The blank action, still named "New action", is unchanged, one click away under "Advanced". No starting point writes a script.
15+
16+
Nothing is added to the package entry: no export, prop, type member or language-pack key. The new copy lives in the metadata-admin designer's own string tables (en and zh).

‎packages/app-shell/src/views/metadata-admin/i18n.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3337,6 +3337,17 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
33373337
'No action editor is registered in this session, so this action’s properties cannot be edited here.',
33383338
'engine.studio.actions.newLabel': 'New action',
33393339
'engine.studio.actions.delete': 'Delete',
3340+
// objectui#11861 — the New menu's starting points (`actionPresets.ts`).
3341+
'engine.studio.actions.presets': 'Common actions',
3342+
'engine.studio.actions.notSaved': 'Not saved — needs “{input}”',
3343+
'engine.studio.actions.heldNeeds': 'the action “{action}” needs “{input}”',
3344+
'engine.studio.actions.preset.change': 'Change a picklist field',
3345+
'engine.studio.actions.preset.changeHint': 'Asks for a new “{field}”, then saves it.',
3346+
'engine.studio.actions.preset.changeNeeds': 'Needs a picklist field on this object.',
3347+
'engine.studio.actions.preset.changeLabel': 'Change {field}',
3348+
'engine.studio.actions.preset.runFlow': 'Run a flow',
3349+
'engine.studio.actions.preset.openUrl': 'Open a web page',
3350+
'engine.studio.actions.preset.openPage': 'Open a page in a dialog',
33403351
// API view
33413352
'engine.studio.api.subtitle':
33423353
'Auto-generated REST endpoints for this object, derived from its current schema. Copy a request as cURL and call it with your API token.',
@@ -6463,6 +6474,16 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
64636474
'engine.studio.actions.editorMissing': '本次会话没有注册操作编辑器,因此无法在这里编辑该操作的属性。',
64646475
'engine.studio.actions.newLabel': '新操作',
64656476
'engine.studio.actions.delete': '删除',
6477+
'engine.studio.actions.presets': '常用操作',
6478+
'engine.studio.actions.notSaved': '未保存 —— 需要「{input}」',
6479+
'engine.studio.actions.heldNeeds': '操作「{action}」需要「{input}」',
6480+
'engine.studio.actions.preset.change': '修改下拉选择字段',
6481+
'engine.studio.actions.preset.changeHint': '询问新的「{field}」,然后保存。',
6482+
'engine.studio.actions.preset.changeNeeds': '需要该对象有一个下拉选择字段。',
6483+
'engine.studio.actions.preset.changeLabel': '修改{field}',
6484+
'engine.studio.actions.preset.runFlow': '运行流程',
6485+
'engine.studio.actions.preset.openUrl': '打开网页',
6486+
'engine.studio.actions.preset.openPage': '在对话框中打开页面',
64666487
// API view
64676488
'engine.studio.api.subtitle':
64686489
'该对象自动生成的 REST 端点,依据当前 schema 派生。可复制为 cURL,带上你的 API 令牌调用。',

‎packages/app-shell/src/views/studio-design/ObjectActionsPanel.newActionDefault-11820.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ const draft = {
4545
function addAction(): Record<string, unknown> {
4646
const onPatch = vi.fn();
4747
render(<ObjectActionsPanel draft={draft} onPatch={onPatch} />);
48+
// objectui#11861 — New opens on presets; this skeleton is the blank action,
49+
// still named "New action", folded under Advanced.
4850
fireEvent.click(screen.getByRole('button', { name: /New/ }));
51+
fireEvent.click(screen.getByRole('button', { name: 'Advanced' }));
52+
fireEvent.click(screen.getByRole('button', { name: 'New action' }));
4953
expect(onPatch).toHaveBeenCalledTimes(1);
5054
const actions = onPatch.mock.calls[0][0].actions as Array<Record<string, unknown>>;
5155
expect(actions).toHaveLength(2);

0 commit comments

Comments
 (0)