From 98c92a69fc37194e505139c4528c1a7b5ca9d9d6 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Thu, 25 Jun 2026 00:07:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(studio):=20package=20lifecycle=20UI=20?= =?UTF-8?q?=E2=80=94=20Duplicate=20/=20Adopt=20loose=20items=20/=20structu?= =?UTF-8?q?re-only=20delete=20(ADR-0070=20D4/D5/D6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PackageDetailSheet: Duplicate (POST /duplicate), Adopt loose items (POST /adopt-orphans, D5 migration), keep-data choice on delete (?keepData). D6 guardrail test: scope selector never defaults to the Local sentinel. +i18n EN/ZH. Co-Authored-By: Claude Opus 4.8 --- .changeset/adr-0070-package-lifecycle-ui.md | 18 ++++++ .../src/views/metadata-admin/PackagesPage.tsx | 55 ++++++++++++++++++- .../src/views/metadata-admin/i18n.ts | 18 ++++++ .../metadata-admin/package-scope.test.ts | 15 +++++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 .changeset/adr-0070-package-lifecycle-ui.md diff --git a/.changeset/adr-0070-package-lifecycle-ui.md b/.changeset/adr-0070-package-lifecycle-ui.md new file mode 100644 index 000000000..034d5b994 --- /dev/null +++ b/.changeset/adr-0070-package-lifecycle-ui.md @@ -0,0 +1,18 @@ +--- +"@object-ui/app-shell": patch +--- + +feat(studio): package lifecycle UI — Duplicate base, Adopt loose items, structure-only delete (ADR-0070 D4/D5/D6) + +`PackageDetailSheet` gains the user-facing affordances for the package-as- +lifecycle-unit work: +- **Duplicate** → `POST /packages/:id/duplicate` (clone a base into a new + writable package; D4). +- **Adopt loose items** → `POST /packages/:id/adopt-orphans` (migrate every + package-less orphan into this base; D5). +- **Delete** now asks whether to drop records too (`?keepData`) — structure-only + vs everything (D4 Q3). + +D6 guardrail test: the scope selector never defaults to the package-less +`Local / Custom` sentinel (`writableBaseOptions` excludes it; real bases sort +first). diff --git a/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx b/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx index 99c2cc274..aa3062f29 100644 --- a/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx +++ b/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx @@ -32,6 +32,8 @@ import { ExternalLink, AlertTriangle, Trash2, + Copy, + Inbox, } from 'lucide-react'; import { Button, @@ -455,10 +457,13 @@ function PackageDetailSheet({ tFormat('engine.packages.detail.deleteConfirm', locale, { name: pkg?.manifest.name || id }), ); if (!ok) return; + // ADR-0070 D4 (Q3) — let the user keep records (delete structure only). + const alsoData = window.confirm(t('engine.packages.detail.deleteKeepData', locale)); + const qs = alsoData ? '' : '?keepData=true'; setBusy('delete'); setMsg(null); try { - await apiJson(`${API}/${encodeURIComponent(id)}`, { method: 'DELETE' }); + await apiJson(`${API}/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' }); onChanged(); onOpenChange(false); } catch (e: any) { @@ -468,6 +473,46 @@ function PackageDetailSheet({ } }; + // ADR-0070 D4 — duplicate this base into a NEW writable package (re-namespaced). + const duplicateApp = async () => { + const target = window.prompt(t('engine.packages.detail.duplicatePrompt', locale), `${id}-copy`); + if (!target || !target.trim()) return; + setBusy('duplicate'); + setMsg(null); + try { + await apiJson(`${API}/${encodeURIComponent(id)}/duplicate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ targetPackageId: target.trim(), targetName: `${pkg?.manifest.name ?? id} (copy)` }), + }); + setMsg({ kind: 'ok', text: t('engine.packages.detail.duplicated', locale) }); + onChanged(); + } catch (e: any) { + setMsg({ kind: 'err', text: e?.message ?? 'Duplicate failed' }); + } finally { + setBusy(null); + } + }; + + // ADR-0070 D5 — adopt every package-less (loose) item in this env INTO this base. + const adoptOrphans = async () => { + const ok = window.confirm( + tFormat('engine.packages.detail.adoptConfirm', locale, { name: pkg?.manifest.name || id }), + ); + if (!ok) return; + setBusy('adopt'); + setMsg(null); + try { + await apiJson(`${API}/${encodeURIComponent(id)}/adopt-orphans`, { method: 'POST' }); + setMsg({ kind: 'ok', text: t('engine.packages.detail.adopted', locale) }); + onChanged(); + } catch (e: any) { + setMsg({ kind: 'err', text: e?.message ?? 'Adopt failed' }); + } finally { + setBusy(null); + } + }; + const toggleEnable = () => run( 'toggle', @@ -613,6 +658,14 @@ function PackageDetailSheet({ {busy === 'export' ? t('engine.packages.detail.exporting', locale) : t('engine.packages.detail.export', locale)} + +