From 385fa922673dc8095752d86990f34de99960ccac Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 23:09:46 +0000 Subject: [PATCH 1/2] feat(app-shell): Studio home returns to the Studio landing; the two Setup exits say so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shell header's first button and a new first row in the package switcher both lead to `/studio` (the Studio landing), behind the same unsaved-edits gate. The installed card's Setup link reads "Open in Setup" with an outward arrow, and the empty Organization flows block links to Setup › Packaged automation (`/apps/setup/component/automation/packaged`, built from the `automation:packaged` component ref). Claude-Session: https://claude.ai/code/session_01Li9iYX6uAhVZJgyCfJ1Maz Co-authored-by: Claude --- .changeset/12513-studio-home-loop.md | 12 + .../src/views/metadata-admin/i18n.ts | 12 +- .../BuilderLanding.setupExit-12513.test.tsx | 63 +++++ .../views/studio-design/BuilderLanding.tsx | 19 +- ...tudioDesignSurface.homeLoop-12513.test.tsx | 264 ++++++++++++++++++ ...tudioDesignSurface.pillarNavGuard.test.tsx | 10 +- .../studio-design/StudioDesignSurface.tsx | 62 +++- 7 files changed, 420 insertions(+), 22 deletions(-) create mode 100644 .changeset/12513-studio-home-loop.md create mode 100644 packages/app-shell/src/views/studio-design/BuilderLanding.setupExit-12513.test.tsx create mode 100644 packages/app-shell/src/views/studio-design/StudioDesignSurface.homeLoop-12513.test.tsx diff --git a/.changeset/12513-studio-home-loop.md b/.changeset/12513-studio-home-loop.md new file mode 100644 index 0000000000..4af39efd29 --- /dev/null +++ b/.changeset/12513-studio-home-loop.md @@ -0,0 +1,12 @@ +--- +'@object-ui/app-shell': patch +--- + +Studio's way home leads back to the Studio landing, and the two links that leave for Setup say so (objectui#12513). + +- **The header's first button returns to the Studio landing** (`/studio`), where the author picks another package, a recent one or the organization's flows. It is titled "Studio home" and still asks before leaving unsaved pillar edits. The console home is one click further, through the landing's own top bar. +- **The package switcher's first row is "Studio home"**, above the package list. It asks before leaving unsaved edits, like the other rows. +- **The installed package card's link reads "Open in Setup"** with an outward arrow. It still opens the package's metadata directory in Setup, and its tooltip still explains environment overlays. +- **The empty Organization flows page links to Setup › Packaged automation**, the list its sentence names, through an "Open in Setup" link with an outward arrow. It offers no create button, since this scope creates nothing. + +The new copy lives in the metadata-admin designer's own string tables (en and zh). diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index bb195b6bed..6454afc5bf 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -2891,6 +2891,9 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.deselect': 'Clear selection', 'engine.studio.toggleRail': 'Toggle sidebar', 'engine.studio.home': 'Back to home', + // objectui#12513 — the header's first button and the package switcher's + // first row: both return to the Studio landing (`/studio`). + 'engine.studio.landingHome': 'Studio home', // objectui#11795 — the one line a phone shows under the Studio header. 'engine.studio.desktopHint': 'Studio is built for a desktop screen.', // Pillar tab labels @@ -3048,7 +3051,8 @@ const ENGINE_STRINGS_EN: Record = { // objectui#11808 — the routes a read-only package does have (ADR-0070 D2, // ADR-0005): an environment overlay, and the marketplace where the runtime has one. 'engine.studio.landing.readonlyHint': 'Code and installed packages are read-only, so they are not edited or duplicated here. Customize one with an environment overlay, for the metadata types that accept one.', - 'engine.studio.landing.overlay': 'Customize with an overlay', + // objectui#12513 — the link leaves Studio for Setup, and says so. + 'engine.studio.landing.overlay': 'Open in Setup', 'engine.studio.landing.overlayTitle': 'Open this package’s metadata: each type shows whether it accepts an environment overlay', 'engine.studio.landing.marketplace': 'Or install a template app from the marketplace', // objectui#11553 — the package-less scope: the organization's own flows. @@ -3057,6 +3061,8 @@ const ENGINE_STRINGS_EN: Record = { 'engine.studio.org.hint': 'Flows that belong to no package', 'engine.studio.org.description': 'The organization’s own flows that belong to no package, such as a clone of a packaged flow. They open editable.', 'engine.studio.org.none': 'No flows outside a package yet. Clone a packaged flow in Setup › Packaged automation to edit the copy here.', + // objectui#12513 — the empty block's way to that Setup list: a link, since this scope creates nothing. + 'engine.studio.org.openSetup': 'Open in Setup', 'engine.studio.publishedAllFlows': 'Published {count} package-less flow drafts', 'engine.studio.publishedAllFlowsOne': 'Published {count} package-less flow draft', 'engine.studio.auto.deepLinkMissing': 'The link names flow “{name}”, which is not here.', @@ -6187,6 +6193,7 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.deselect': '取消选择', 'engine.studio.toggleRail': '切换侧栏', 'engine.studio.home': '返回主页', + 'engine.studio.landingHome': 'Studio 首页', 'engine.studio.desktopHint': 'Studio 专为桌面屏幕设计。', // Pillar tab labels 'engine.fieldType.text': '单行文本', @@ -6339,7 +6346,7 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.landing.installedHeading': '已安装(只读 · 可浏览)', // objectui#11808 — the routes a read-only package does have. 'engine.studio.landing.readonlyHint': '源码与已安装的软件包为只读,不能在此编辑或复制。对于接受环境覆盖的元数据类型,可用环境覆盖(overlay)进行定制。', - 'engine.studio.landing.overlay': '用覆盖定制', + 'engine.studio.landing.overlay': '在设置中打开', 'engine.studio.landing.overlayTitle': '打开该软件包的元数据:每种类型都会标明是否接受环境覆盖', 'engine.studio.landing.marketplace': '或从应用市场安装一个模板应用', // objectui#11553 — the package-less scope: the organization's own flows. @@ -6348,6 +6355,7 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.studio.org.hint': '不属于任何软件包的流程', 'engine.studio.org.description': '本组织自有、不属于任何软件包的流程,例如软件包流程的克隆副本。可直接编辑。', 'engine.studio.org.none': '还没有不属于软件包的流程。在 设置 › 打包自动化 中克隆一个软件包流程,即可在这里编辑副本。', + 'engine.studio.org.openSetup': '在设置中打开', 'engine.studio.publishedAllFlows': '已发布 {count} 个不属于软件包的流程草稿', 'engine.studio.publishedAllFlowsOne': '已发布 {count} 个不属于软件包的流程草稿', 'engine.studio.auto.deepLinkMissing': '链接指向的流程「{name}」不在这里。', diff --git a/packages/app-shell/src/views/studio-design/BuilderLanding.setupExit-12513.test.tsx b/packages/app-shell/src/views/studio-design/BuilderLanding.setupExit-12513.test.tsx new file mode 100644 index 0000000000..207191a105 --- /dev/null +++ b/packages/app-shell/src/views/studio-design/BuilderLanding.setupExit-12513.test.tsx @@ -0,0 +1,63 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectui#12513 — the installed card's link leaves Studio for Setup, and says + * so: it reads "Open in Setup" and carries an outward arrow, while its route + * (the package's metadata directory, objectui#11808) and its testid stay. + */ + +import '@testing-library/jest-dom/vitest'; +import * as React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen, within } from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +const packages = vi.hoisted(() => ({ list: [] as Array> })); + +vi.mock('./packages-io', async (importOriginal) => { + const mod = await importOriginal(); + return { ...mod, fetchPackages: vi.fn(async () => packages.list) }; +}); + +// The create dialog is closed throughout; its form stack is not under test. +vi.mock('../metadata-admin/PackageFormDialog', () => ({ PackageFormDialog: () => null })); + +import { BuilderLanding, packagedAutomationPath } from './BuilderLanding'; + +const READ_ONLY = { id: 'com.example.showcase', name: 'Showcase', writable: false, namespace: 'showcase' }; + +afterEach(() => { + cleanup(); + packages.list = []; +}); + +function renderLanding() { + return render( + + + } /> + + , + ); +} + +describe('Studio landing: the installed card’s Setup exit says where it goes (objectui#12513)', () => { + it('reads "Open in Setup" with an outward arrow, on the same route', async () => { + packages.list = [READ_ONLY]; + renderLanding(); + + await screen.findByText('Showcase'); + const link = screen.getByTestId('studio-landing-overlay'); + expect(link).toHaveAccessibleName('Open in Setup'); + expect(link).toHaveAttribute('href', '/apps/setup/metadata?package=com.example.showcase'); + expect(link.querySelector('svg.lucide-arrow-up-right')).not.toBeNull(); + // The overlay explanation stays on the link's tooltip. + expect(link).toHaveAttribute('title', expect.stringContaining('environment overlay')); + expect(within(link).queryByText('Customize with an overlay')).toBeNull(); + }); + + it('builds the packaged-automation route from its Setup component ref', () => { + // The URL the Setup app's `nav_packaged_automation` entry resolves to. + expect(packagedAutomationPath()).toBe('/apps/setup/component/automation/packaged'); + }); +}); diff --git a/packages/app-shell/src/views/studio-design/BuilderLanding.tsx b/packages/app-shell/src/views/studio-design/BuilderLanding.tsx index ca7007e6ee..ff70f9c0a3 100644 --- a/packages/app-shell/src/views/studio-design/BuilderLanding.tsx +++ b/packages/app-shell/src/views/studio-design/BuilderLanding.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { Boxes, Building2, Clock, Hammer, Layers, Lock, Plus, Loader2, Copy, Store } from 'lucide-react'; +import { ArrowUpRight, Boxes, Building2, Clock, Hammer, Lock, Plus, Loader2, Copy, Store } from 'lucide-react'; import { toast } from 'sonner'; import { useRecentItems, type RecentNamedItem } from '../../context/RecentItemsProvider.js'; import { useRecentItemLabel } from '../../hooks/useRecentItemLabel.js'; @@ -30,6 +30,7 @@ import { isMarketplaceEnabled } from '../../runtime-config.js'; import { fetchPackages, duplicatePackage, isSpecPackageId, type PkgEntry } from './packages-io.js'; import { PackageIdInput } from './PackageIdInput.js'; import { studioOrgScopePath } from './studioScope.js'; +import { componentRefToUrlSegments } from '../../services/componentRegistry.js'; /** * objectui#11808 — the two places a read-only package's card sends the author. @@ -48,6 +49,17 @@ const MARKETPLACE_PATH = '/apps/setup/system/marketplace'; export const overlayPath = (packageId: string) => `/apps/setup/metadata?package=${encodeURIComponent(packageId)}`; +/** + * objectui#12513 — Setup › Packaged automation, where a packaged flow is + * cloned into the organization's own package-less flows. The Setup app's + * navigation reaches the page by its component ref (`automation:packaged`, + * registered in `builtinComponents`), so the URL is built from that ref the + * way the console's component route reads it, never spelled out by hand. + */ +const PACKAGED_AUTOMATION_REF = 'automation:packaged'; +export const packagedAutomationPath = () => + `/apps/setup/component/${componentRefToUrlSegments(PACKAGED_AUTOMATION_REF).join('/')}`; + /** objectui#11863 — how many recent packages the landing lists: one row of cards. */ const RECENT_PACKAGES_SHOWN = 3; @@ -360,7 +372,10 @@ export function BuilderLanding(): React.ReactElement { data-testid="studio-landing-overlay" className="inline-flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground hover:underline" > - {t('engine.studio.landing.overlay', locale)} + {/* objectui#12513 — the link leaves Studio for Setup: its + name says where it goes, the arrow that it goes out. */} + {t('engine.studio.landing.overlay', locale)} +