Skip to content

Commit 693e100

Browse files
feat(studio): customize a read-only base's flow into its pair, with one ledger switch and Restore package original (#28824)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbMDxQQZtBknmCatggWjGfjm)_ Fixes #28570 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:在 Studio 的自动化栏位里,只读基础包(如 HotCRM)的流程头部多了「自定义」按钮。第一次保存编辑时,经 ensure 接口拿到配对包 `BASE.custom`,把这个流程以同名草稿写进去,并声明 `overrides: { package, basedOn }`。覆盖流程的头部显示「覆盖 HotCRM」徽标,仍然只用启用台账的那一个开关,不画定义开关。流程的 ⋯ 菜单多了「恢复软件包原版」,确认后删除配对包里的草稿和行,基础流程重新运行。 - 为什么改:ADR-0146 D9 第 1–3 项(#28236 的 N-18)。以前想改 HotCRM 的流程只能在 Setup 克隆,两份都会跑;现在用同名覆盖替换它,开关还是同一个。 - 风险与回滚:低。只改 `app-shell` 的 studio-design 目录和 studio 文案(en/zh 各加一条),不改协议、服务端或 spec。是否已覆盖,读的是声明覆盖那一行本身(配对包草稿,否则 layered 读取的 `overlay`),不读信封里随基础包封印走的 `deletable`(服务端席位 1 的发现)。可写包的流程照旧有删除和定义开关(对照测试)。回滚即还原本 PR。 - 先看:`StudioDesignSurface.tsx` 里 `AutomationsPillar` 的 `doSave`(写进配对包并声明覆盖)、`restoreFlow`(两次删除)和 `flowSwitchEl`(覆盖流程不画定义开关);然后 `FlowItemMoreMenu.tsx` 的 `restore`。 - 两分钟验证:`cd ui && pnpm exec vitest run packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx`,预期 10 条全过。 - 需要你做的:无,席位落地。 ## What changes On a read-only base, the Automations pillar now customizes a flow the way the Interfaces pillar customizes a view, a dashboard or a report (the card's Wanted 1 to 6). - **Customize this flow (Wanted 1).** The flow's header draws `StudioCustomizeButton`, N-17's control, with the same block reasons: `walled` from the surface's `customizeDoorWalled`, `noVersion` when the listed manifest states no version. Pressing it only unlocks the flow's editors (canvas `onPatch`, the node inspector, the autosave). Nothing is written until an edit is saved. - **The first saved edit (Wanted 2).** `doSave` asks the surface's ensure door once per flow (`StudioBaseCustomization.ensure`, the same path `InterfacesPillar` takes) and writes the flow's draft into the package that answer names, under the flow's own name. The body is `overrideDraftBody(draft, base, basedOn)`: the served flow without its `_` keys, declaring `overrides: { package, basedOn }`. The first save into the pair is pinned to "no draft there" (`ifNoneMatch`), the later ones to the version they were answered. The flow's load reads its draft at the pair once one is listed, so the designer then edits that draft. The rest of the package stays read-only: Delete flow, New similar's target, the item tools and the definition switch keep their read-only gates. - **One switch (Wanted 3).** The open override keeps `FlowLedgerSwitch` on the flow's name (`_status` row, unchanged). When no ledger row is served, a customized flow draws no switch at all, where the base's own flow keeps its locked definition switch. `toggleEnabled` stays refused on a read-only package's flow, so Studio never writes the override's `status` to mean on or off. - **The badge (Wanted 4).** The header draws `StudioOverrideChip` from `useServedOverride`: the pair's draft through the draft door, else the layered read's `overlay`, which N-11 serves as the pair's row. - **Restore package original (Wanted 5).** `FlowItemMoreMenu` gains a `restore` prop that draws N-17's `StudioRestoreOriginalItem`. The pillar passes it when the served declaration exists. Per server seat 1's finding on the card, Restore is decided from the declared override's own row, never from the served envelope's `deletable`, which follows the base's seal. The confirmation (`StudioRestoreOriginalDialog`, new `kind: 'flow'`) says the base's flow runs again under the same switch, and names the draft that goes first. Confirming deletes the pair's draft, then the pair's row, both at the pair (`client.reset`), reloads the flow, re-reads the declaration and the engine's rows, and the base's flow reads back. - **Refusals (Wanted 6).** A refused ensure door or a refused save into the pair is shown as the server sent it. A refused publish of the pair's flow goes through the surface's existing per-draft path, which carries the server's reason as sent (`publishRefusal`, `engine.studio.refusal.publishPlain`). - **One new string**, `engine.studio.customize.restoreFlowBody`, in the studio namespace's `en` and `zh`. ## Tests `AutomationsPillar.customizeFlow-28570.test.tsx`, 10 cases, through the mounted pillar, its real autosave, the real header controls and the real `ensureCustomizationPackage` over a fetch stub that also serves the `/automation` doors: - Customize is offered and writes nothing; until it is pressed the canvas edits nothing. - The first saved edit asks the door once (201) and writes the pair's draft under the flow's name with the declaration and no `_` key; the base is untouched; no `/toggle` request is sent; the badge appears; one switch, the ledger's; the second edit asks the door no more and is pinned to `v1`. - A published override: badge, no Customize, the ledger switch alone, Restore offered although the layered envelope says `deletable: false`, no Delete flow, and an edit saves into the pair keeping the declaration. - Restore: the confirmation's title, the flow sentence and the draft sentence; two deletes at the pair (draft, then row); the base's flow reads back with Customize offered again. - No ledger row served: the base's flow keeps its locked definition switch (control); once customized no switch is drawn, and the saved body keeps the base's `status`. - A walled tenant admin: Customize disabled with the reason; nothing asked or sent. - A refused save into the pair is shown verbatim. - A refused publish of the pair's flow reads with the server's sentence. - CONTROLS: the base's flow with no override offers no Restore; a writable package's flow keeps Delete flow and its definition switch, with no Customize. ## Acceptance notes - `FlowItemMoreMenu` lives in its own file, `studio-design/FlowItemMoreMenu.tsx`, not inside `StudioDesignSurface.tsx`, so the edit to it is that file. The claim names `FlowItemMoreMenu` in the file surface. - `setup/packagedFlowLedger.ts` was not touched: the ledger row is keyed by the flow's name, and N-11 keeps `packagedFlowOwner(name)` answering the base, so the existing hook already binds the override. - The flow's item tools stay read-only on a customized flow, as the Interfaces pillar keeps them on a customized item. ## Verification All local runs at head `146e0b60` unless stated. <details><summary>Touched-package tests</summary> - New file: `Test Files 1 passed (1)`, `Tests 10 passed (10)`. - 155 ui test files naming `AutomationsPillar`, `FlowItemMoreMenu`, `StudioCustomizeControls`, the automations route, or importing the studio namespace, in four batches at `47ed4fb8`: 40/40 files 336 tests, 40/40 files 851 tests, 40/40 files 283 tests, 35/35 files 587 tests, all passed. `packages/i18n/`: 94 files, 1438 passed, 13 skipped. The commits after `47ed4fb8` are a one-line render change to Restore's `disabled` (it no longer reads a ref while rendering), the new no-ledger-row case and two renamed test parameters. At `146e0b60` the new file and five neighbouring files (`AutomationsPillar.ledgerSwitch-28148`, `.deleteFlow-27356`, `.newSimilar-28567`, `StudioDesignSurface.oneSurface-28572`, `InterfacesPillar.customize-28562`) ran again: `Test Files 6 passed (6)`, `Tests 52 passed (52)`. - The rest of app-shell's suite is CI's Test UI (narrowing declared: the set above is a `git grep` of the touched components and strings, blind to tests that reach them indirectly). </details> <details><summary>Ablations (each one-shot through `scripts/ablation-replace.mjs`, restored to the HEAD blob, `git diff HEAD` empty)</summary> - Save names no pair: 2 red. Restore not read from the declaration: 2 red. Editors stay locked: 3 red. No badge: 2 red. Load ignores the pair: 1 red. Walled block dropped: 1 red. Definition switch drawn on a customized flow: 1 red (the no-ledger-row case). Dialog says the item sentence: 1 red. Row delete dropped: 1 red. </details> <details><summary>Type-check, lint, derived gates</summary> - `pnpm exec turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n --concurrency=2` at `146e0b60`: `Tasks: 66 successful, 66 total` (the first run, at `92ba27eb`, was red on two unused parameters in the new test's doubles, TS6133; fixed in `146e0b60`). - ESLint on the six touched source and test files: 0 errors. In `StudioDesignSurface.tsx` the React Compiler's "memoization could not be preserved" warnings move from 27 to 29, for `restoreFlow` (the same shape `deleteOpenFlow` already warns on); no other warning class is added. - `node scripts/pm/dispatch-gates.mjs --commands`: 34 derived families, each run once at `92ba27eb` and exit 0, including `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest run scripts/`. `--ran` reconciliation: `34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN`. The only commit after it renames two test parameters. - Every family CI runs is read from this PR's check-runs. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jRWZNXu1HAHuMowQXS)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jRWZNXu1HAHuMowQXS --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2587485 commit 693e100

7 files changed

Lines changed: 737 additions & 23 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(studio): customize a read-only base's flow into its pair under the flow's own name, with one ledger switch, the override badge and "Restore package original"
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
On a read-only base, Studio's Automations pillar now customizes a flow the way the Interfaces pillar customizes a view, a dashboard or a report (ADR-0146 D9 items 1 to 3, D2, D4).
12+
13+
- **Customize this flow.** The flow's header offers Customize, with the same control, the same ensure door (`POST /api/v1/packages/:id/ensure-customization`) and the same disabled reasons as the other customizable types. A walled tenant admin sees it disabled. Pressing it only unlocks the flow's editors. Nothing is written until an edit is saved.
14+
- **The first saved edit** asks the ensure door once and writes the flow's draft into the package that answer names, under the flow's own name. The body is the served flow without its `_` keys, declaring `overrides: { package, basedOn }`. The designer then edits that draft. The rest of the package stays read-only.
15+
- **One switch.** The open override keeps the activation-ledger switch on the flow's name. No definition switch is drawn for it, and Studio never writes the override's `status` to mean on or off.
16+
- **The badge.** The header carries the override badge, read from the served declaration (the pair's draft, else the layered read's `overlay`).
17+
- **Restore package original.** The flow's menu offers it for an override. Whether the flow is overridden is read from the declared override's own row, never from the served envelope's `deletable`, which follows the base's seal. The confirmation says the base's flow runs again under the same switch. Confirming deletes the pair's draft and then the pair's row, and the base's flow reads back. Delete flow stays on a writable package's own flow.
18+
- **Refusals.** A refused save into the pair, and a refused publish of it, read as the server sent them.
19+
- **New string.** `@object-ui/i18n`'s studio namespace gains `engine.studio.customize.restoreFlowBody` in `en` and `zh`. The other languages fall back to `en`, as the whole namespace does.

‎ui/packages/app-shell/src/views/studio-design/AutomationsPillar.customizeFlow-28570.test.tsx‎

Lines changed: 469 additions & 0 deletions
Large diffs are not rendered by default.

‎ui/packages/app-shell/src/views/studio-design/FlowItemMoreMenu.tsx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
* `type/name` (objectui#13333). The pillar sends the delete itself. Reset
2121
* overlay is not offered: it arrives with the overlay editing of the
2222
* Studio / Setup boundary record's phase 2 (#28045).
23+
* - **Restore package original** (objectstack#28570, ADR-0146 D2), a
24+
* read-only base's flow that its pair overrides. The pillar decides it from
25+
* the served declaration and holds the confirmation, outside this menu.
2326
*/
2427

2528
import * as React from 'react';
@@ -52,6 +55,7 @@ import { t, tFormat, translateMetadataType } from '../metadata-admin/i18n.js';
5255
import { envelopeData, envelopeRefused } from '../setup/packagedFlows.js';
5356
import { apiBase } from '../../utils/apiBase.js';
5457
import { STUDIO_HEADER_ICON_BUTTON } from './studio-tokens.js';
58+
import { StudioRestoreOriginalItem } from './StudioCustomizeControls.js';
5559

5660
/** What the clone door saved: the copy's name, the package it names, and its sentence. */
5761
export interface FlowCopied {
@@ -124,15 +128,17 @@ interface NewSimilarDraft {
124128
/**
125129
* `label` is the flow's display label; `disabled` holds the menu's actions
126130
* while a save is in flight. `onDelete` draws Delete flow (a writable
127-
* package's flow); `newSimilar` draws New similar.
131+
* package's flow); `newSimilar` draws New similar; `restore` draws Restore
132+
* package original (a flow the base's pair overrides).
128133
*/
129-
export function FlowItemMoreMenu({ name, label, locale, disabled, onDelete, newSimilar }: {
134+
export function FlowItemMoreMenu({ name, label, locale, disabled, onDelete, newSimilar, restore }: {
130135
name: string;
131136
label: string;
132137
locale: string;
133138
disabled?: boolean;
134139
onDelete?: () => void;
135140
newSimilar?: FlowNewSimilarProps;
141+
restore?: { disabled?: boolean; onSelect: () => void };
136142
}): React.ReactElement {
137143
const [confirming, setConfirming] = React.useState(false);
138144
const [copy, setCopy] = React.useState<NewSimilarDraft | null>(null);
@@ -209,6 +215,9 @@ export function FlowItemMoreMenu({ name, label, locale, disabled, onDelete, newS
209215
{t('engine.studio.auto.deleteFlow', locale)}
210216
</DropdownMenuItem>
211217
)}
218+
{restore && (
219+
<StudioRestoreOriginalItem disabled={disabled || restore.disabled} locale={locale} onSelect={restore.onSelect} />
220+
)}
212221
</DropdownMenuContent>
213222
</DropdownMenu>
214223
{onDelete && (

‎ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ export function StudioRestoreOriginalItem({
309309
/**
310310
* The confirmation of "Restore package original": it says what goes, the
311311
* customization, and its unpublished draft when one is pending. `onConfirm`
312-
* sends both deletes; nothing is sent on Cancel.
312+
* sends both deletes; nothing is sent on Cancel. objectstack#28570 — a flow's
313+
* (`kind: 'flow'`) says the base's flow runs again, under the same switch.
313314
*/
314315
export function StudioRestoreOriginalDialog({
315316
open,
@@ -319,6 +320,7 @@ export function StudioRestoreOriginalDialog({
319320
hasDraft,
320321
locale,
321322
onConfirm,
323+
kind = 'item',
322324
}: {
323325
open: boolean;
324326
onOpenChange: (open: boolean) => void;
@@ -328,9 +330,15 @@ export function StudioRestoreOriginalDialog({
328330
hasDraft: boolean;
329331
locale: string;
330332
onConfirm: () => void;
333+
/** What is restored: an item served in the base's place, or a flow that runs in its place. */
334+
kind?: 'item' | 'flow';
331335
}): React.ReactElement {
332336
const vars = { item: label, package: baseLabel };
333-
const body = tFormat('engine.studio.customize.restoreBody', locale, vars);
337+
const body = tFormat(
338+
kind === 'flow' ? 'engine.studio.customize.restoreFlowBody' : 'engine.studio.customize.restoreBody',
339+
locale,
340+
vars,
341+
);
334342
return (
335343
<AlertDialog open={open} onOpenChange={onOpenChange}>
336344
<AlertDialogContent data-testid="studio-restore-original-dialog">

0 commit comments

Comments
 (0)