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
12 changes: 12 additions & 0 deletions .changeset/12513-studio-home-loop.md
Original file line number Diff line number Diff line change
@@ -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).
14 changes: 10 additions & 4 deletions packages/app-shell/src/views/metadata-admin/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2890,7 +2890,9 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
'engine.studio.close': 'Close',
'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
Expand Down Expand Up @@ -3048,7 +3050,8 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
// 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.
Expand All @@ -3057,6 +3060,8 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
'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.',
Expand Down Expand Up @@ -6187,7 +6192,7 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
'engine.studio.close': '关闭',
'engine.studio.deselect': '取消选择',
'engine.studio.toggleRail': '切换侧栏',
'engine.studio.home': '返回主页',
'engine.studio.landingHome': 'Studio 首页',
'engine.studio.desktopHint': 'Studio 专为桌面屏幕设计。',
// Pillar tab labels
'engine.fieldType.text': '单行文本',
Expand Down Expand Up @@ -6340,7 +6345,7 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
'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.
Expand All @@ -6349,6 +6354,7 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
'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}」不在这里。',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Record<string, unknown>> }));

vi.mock('./packages-io', async (importOriginal) => {
const mod = await importOriginal<typeof import('./packages-io')>();
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(
<MemoryRouter initialEntries={['/studio']}>
<Routes>
<Route path="/studio" element={<BuilderLanding />} />
</Routes>
</MemoryRouter>,
);
}

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');
});
});
19 changes: 17 additions & 2 deletions packages/app-shell/src/views/studio-design/BuilderLanding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -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"
>
<Layers className="h-3 w-3" /> {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)}
<ArrowUpRight aria-hidden="true" className="h-3 w-3" />
</Link>
</div>
</div>
Expand Down
Loading
Loading