Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/28570-studio-customize-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
---

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"

Clause-②: no
Metadata-protocol: none

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).

- **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.
- **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.
- **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.
- **The badge.** The header carries the override badge, read from the served declaration (the pair's draft, else the layered read's `overlay`).
- **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.
- **Refusals.** A refused save into the pair, and a refused publish of it, read as the server sent them.
- **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.

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions ui/packages/app-shell/src/views/studio-design/FlowItemMoreMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
* `type/name` (objectui#13333). The pillar sends the delete itself. Reset
* overlay is not offered: it arrives with the overlay editing of the
* Studio / Setup boundary record's phase 2 (#28045).
* - **Restore package original** (objectstack#28570, ADR-0146 D2), a
* read-only base's flow that its pair overrides. The pillar decides it from
* the served declaration and holds the confirmation, outside this menu.
*/

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

/** What the clone door saved: the copy's name, the package it names, and its sentence. */
export interface FlowCopied {
Expand Down Expand Up @@ -124,15 +128,17 @@ interface NewSimilarDraft {
/**
* `label` is the flow's display label; `disabled` holds the menu's actions
* while a save is in flight. `onDelete` draws Delete flow (a writable
* package's flow); `newSimilar` draws New similar.
* package's flow); `newSimilar` draws New similar; `restore` draws Restore
* package original (a flow the base's pair overrides).
*/
export function FlowItemMoreMenu({ name, label, locale, disabled, onDelete, newSimilar }: {
export function FlowItemMoreMenu({ name, label, locale, disabled, onDelete, newSimilar, restore }: {
name: string;
label: string;
locale: string;
disabled?: boolean;
onDelete?: () => void;
newSimilar?: FlowNewSimilarProps;
restore?: { disabled?: boolean; onSelect: () => void };
}): React.ReactElement {
const [confirming, setConfirming] = React.useState(false);
const [copy, setCopy] = React.useState<NewSimilarDraft | null>(null);
Expand Down Expand Up @@ -209,6 +215,9 @@ export function FlowItemMoreMenu({ name, label, locale, disabled, onDelete, newS
{t('engine.studio.auto.deleteFlow', locale)}
</DropdownMenuItem>
)}
{restore && (
<StudioRestoreOriginalItem disabled={disabled || restore.disabled} locale={locale} onSelect={restore.onSelect} />
)}
</DropdownMenuContent>
</DropdownMenu>
{onDelete && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ export function StudioRestoreOriginalItem({
/**
* The confirmation of "Restore package original": it says what goes, the
* customization, and its unpublished draft when one is pending. `onConfirm`
* sends both deletes; nothing is sent on Cancel.
* sends both deletes; nothing is sent on Cancel. objectstack#28570 — a flow's
* (`kind: 'flow'`) says the base's flow runs again, under the same switch.
*/
export function StudioRestoreOriginalDialog({
open,
Expand All @@ -319,6 +320,7 @@ export function StudioRestoreOriginalDialog({
hasDraft,
locale,
onConfirm,
kind = 'item',
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
Expand All @@ -328,9 +330,15 @@ export function StudioRestoreOriginalDialog({
hasDraft: boolean;
locale: string;
onConfirm: () => void;
/** What is restored: an item served in the base's place, or a flow that runs in its place. */
kind?: 'item' | 'flow';
}): React.ReactElement {
const vars = { item: label, package: baseLabel };
const body = tFormat('engine.studio.customize.restoreBody', locale, vars);
const body = tFormat(
kind === 'flow' ? 'engine.studio.customize.restoreFlowBody' : 'engine.studio.customize.restoreBody',
locale,
vars,
);
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent data-testid="studio-restore-original-dialog">
Expand Down
Loading
Loading