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/25098-setup-packages-to-studio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/platform-objects': patch
---

feat(platform-objects): Setup no longer carries the unfiltered Packages entry; the full package list lives in Studio

Clause-②: no
Metadata-protocol: none

The full `developer:packages` list is a development surface, so its one unfiltered navigation entry is now Studio's Overview → Packages, which is unchanged. Setup's `group_apps` drops its `nav_packages` entry. Setup keeps the marketplace-filtered "Installed Apps" door (`nav_marketplace_installed`, contributed by `@objectstack/cloud-connection` wherever the marketplace capability is present), so its Apps group reads Browse Marketplace, Installed Apps and Packaged Actions.

The Setup copies of the entry's label (`apps.setup.navigation.nav_packages` in `en`, `zh-CN`, `ja-JP` and `es-ES`) and their recorded source hashes are removed with it; Studio's copies stay. A bookmark to the Packages page keeps working, since the page itself did not move.
11 changes: 6 additions & 5 deletions packages/cli/test/connect-agent-both-halves-wire.pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,15 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
.toContain('nav_connect_agent');
// The Setup tree the platform admin is served BY THIS COMPOSITION: 9
// authored groups less `group_approvals`, which nothing contributes
// into and the filter collapses, plus the 25 entries
// `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one.
// into and the filter collapses, plus the 24 entries
// `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one. (25 until
// #25098 moved the unfiltered Packages entry to Studio.)
// `nav_settings_branding` is not among them, and not because of a
// tenancy wall (this harness boots none): since #24766 it is
// contributed by `@objectstack/service-settings` together with the
// `branding` namespace, offered only where no organization wall stands,
// and this harness composes platform-objects and the MCP bundle alone.
expect(allowedIds).toHaveLength(34);
expect(allowedIds).toHaveLength(33);
expect(allowedIds).not.toContain('nav_settings_branding');
// The two per-user stores are reached from the User record page, not
// from Setup's menu (#23803).
Expand All @@ -284,8 +285,8 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
expect(withAccountHalf.statusCode).toBe(200);
expect(withoutAccountHalf.statusCode).toBe(200);
expect(navIds(withAccountHalf.body?.item)).toEqual(navIds(withoutAccountHalf.body?.item));
// 34: the composition the count in the test above states.
expect(navIds(withAccountHalf.body?.item)).toHaveLength(34);
// 33: the composition the count in the test above states.
expect(navIds(withAccountHalf.body?.item)).toHaveLength(33);

// The mechanism behind the invariance, asserted rather than assumed:
// the bundle aims exactly one contribution at `setup` and one at
Expand Down
15 changes: 9 additions & 6 deletions packages/platform-objects/src/apps/setup-nav.contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,19 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [
],
},
{
// Package ADMINISTRATION is a platform/operator concern (ADR-0084:
// packages are Operate, out of the builder) — so its home is Setup, not
// the application builder. The console binds `developer:packages` to the
// existing package-management page. Building/creating apps is a separate
// journey (Home cover → /studio); this entry is for install/inspect/admin.
// No unfiltered Packages entry here (#25098, ruling 甲 amending #23892 B):
// the full `developer:packages` list is a development surface, so its one
// unfiltered door is Studio's Overview → Packages (`studio.app.ts`). Setup
// reaches installed packages through the marketplace-filtered "Installed
// Apps" door (`nav_marketplace_installed`, `developer:packages` with
// `params: { source: 'marketplace' }`), which
// `@objectstack/cloud-connection` contributes to this group wherever the
// marketplace capability is present. `setup-packages-door.test.ts` pins
// both halves.
app: 'setup',
group: 'group_apps',
priority: BASE_PRIORITY,
items: [
{ id: 'nav_packages', type: 'component', label: 'Packages', componentRef: 'developer:packages', icon: 'package' },
// Packaged actions are OPERATIONAL state — on/off per packaged action
// (ADR-0126 §7.4: "Studio keeps the editing; Setup gets the operational
// state"). ADR-0146 D4 keeps the action switch here while the flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// `sys_metadata_activation` ledger this package registers (#12419) and work
// on compositions with NO automation service; gating the entry on that
// service would hide a page that still works there.
// - no `requiredPermissions` — matches `nav_packages` beside it: the Setup
// - no `requiredPermissions` — like the rest of `group_apps`: the Setup
// app's own `setup.access` gates the app, and the activation WRITE doors
// enforce `manage_metadata` / the ADR-0126 §5 operator gate server-side.
import { describe, it, expect } from 'vitest';
Expand Down Expand Up @@ -72,7 +72,7 @@ describe('the Setup Packaged Actions entry targets the console component (ADR-01
});
});

it('lives in group_apps beside Packages — package administration is Operate (ADR-0084)', () => {
it('lives in group_apps — operational package state is Operate (ADR-0084)', () => {
expect(entry().group).toBe('group_apps');
});

Expand All @@ -93,8 +93,10 @@ describe('the Setup Packaged Actions entry targets the console component (ADR-01
describe('Setup › Packaged automation stays retired (ADR-0146 D11)', () => {
it('no entry names the retired id or the retired `automation:packaged` ref', () => {
const items = allItems().map(({ item }) => item);
// Control: the walk reads the real array.
expect(items.some((i) => i.id === 'nav_packages')).toBe(true);
// Control: the walk reads the real array. (It used to name Setup's
// `nav_packages`, which moved to Studio in #25098;
// `setup-packages-door.test.ts` pins that move.)
expect(items.some((i) => i.id === 'nav_packaged_actions')).toBe(true);
expect(items.filter((i) => i.id === 'nav_packaged_automation')).toEqual([]);
expect(items.filter((i) => i.componentRef === 'automation:packaged')).toEqual([]);
});
Expand Down
83 changes: 83 additions & 0 deletions packages/platform-objects/src/apps/setup-packages-door.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// The full package list lives in Studio; Setup carries no unfiltered door to it
// (#25098, ruling 甲 amending #23892 ruling B).
//
// The console's `developer:packages` component is the one list of what is
// installed. Opened with no `source` filter it is a development surface, so its
// unfiltered entry lives in Studio only (Overview → Packages). Setup reaches
// installed packages through the marketplace-filtered "Installed Apps" door —
// `developer:packages` with `params: { source: 'marketplace' }` — which
// `@objectstack/cloud-connection` contributes to Setup's `group_apps` wherever
// the marketplace capability is present, and pins on its side
// (`marketplace-installed-ui-entry.pin.test.ts`).
//
// This file is the platform-objects half: what THIS package ships for each app.
// Both directions are pinned, because either regression is silent at runtime —
// an unfiltered Setup entry simply renders, and a Studio entry that goes
// missing leaves the full list reachable only by a hand-typed URL.
import { describe, it, expect } from 'vitest';

import { SETUP_APP } from './setup.app.js';
import { SETUP_NAV_CONTRIBUTIONS } from './setup-nav.contributions.js';
import { STUDIO_APP } from './studio.app.js';

type NavItem = {
id?: string;
type?: string;
componentRef?: string;
params?: Record<string, unknown>;
children?: NavItem[];
};

const PACKAGES_REF = 'developer:packages';

/** Every nav item in a tree, depth-first. */
const walk = (items: unknown[] | undefined, out: NavItem[] = []): NavItem[] => {
for (const raw of items ?? []) {
const item = raw as NavItem | undefined;
if (!item) continue;
out.push(item);
if (Array.isArray(item.children)) walk(item.children, out);
}
return out;
};

/** A `developer:packages` entry with no `source` filter: the full list. */
const isUnfilteredPackagesDoor = (item: NavItem): boolean =>
item.componentRef === PACKAGES_REF && item.params?.source === undefined;

const setupItems = (): NavItem[] => [
...walk(SETUP_APP.navigation as unknown[]),
...SETUP_NAV_CONTRIBUTIONS.flatMap((c) => walk(c.items as unknown[])),
];

describe('the unfiltered package list is a Studio surface, not a Setup one (#25098)', () => {
it('the predicate tells the full list from the marketplace-filtered door', () => {
// Controls on the predicate itself, so the absence below cannot pass
// because the predicate matches nothing.
expect(isUnfilteredPackagesDoor({ type: 'component', componentRef: PACKAGES_REF })).toBe(true);
expect(
isUnfilteredPackagesDoor({ type: 'component', componentRef: PACKAGES_REF, params: { source: 'marketplace' } }),
).toBe(false);
});

it('Setup ships no unfiltered `developer:packages` entry', () => {
const items = setupItems();
// Control: the walk reads the real Setup shell and its contributions.
expect(items.some((i) => i.id === 'group_apps')).toBe(true);
expect(items.some((i) => i.id === 'nav_packaged_actions')).toBe(true);

expect(items.filter(isUnfilteredPackagesDoor)).toEqual([]);
expect(items.filter((i) => i.id === 'nav_packages')).toEqual([]);
});

it('Studio still ships the unfiltered entry, under Overview', () => {
const overview = walk(STUDIO_APP.navigation as unknown[]).find((i) => i.id === 'group_overview');
expect(overview, 'Studio lost its group_overview').toBeDefined();

const doors = walk(overview!.children).filter(isUnfilteredPackagesDoor);
expect(doors).toHaveLength(1);
expect(doors[0]).toMatchObject({ id: 'nav_packages', type: 'component', componentRef: PACKAGES_REF });
});
});
6 changes: 0 additions & 6 deletions packages/platform-objects/src/apps/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ export const en: TranslationData = {
nav_system_overview: { label: 'System Overview' },

// Apps / Marketplace
// `nav_packages` is a SETUP entry (package administration is an
// operator concern — ADR-0084), distinct from the same-id entry under
// `apps.studio.navigation`. Two apps carrying one nav id is normal;
// each app resolves its own subtree, so the studio copy never answers
// for this one. Its absence here was #5750's first cause.
nav_packages: { label: 'Packages' },
nav_packaged_actions: { label: 'Packaged Actions' },
nav_marketplace_browse: { label: 'Browse Marketplace' },
nav_marketplace_installed: { label: 'Installed Apps' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const esESSourceHashes: Readonly<Record<string, string>> = {
'apps.setup.description': 'deacdfc679232a4a',
'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf',
'apps.setup.navigation.group_apps.label': '699b367291abcbe6',
'apps.setup.navigation.nav_packages.label': '8cd8f29658025f73',
'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3',
'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd',
'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60',
Expand Down
3 changes: 0 additions & 3 deletions packages/platform-objects/src/apps/translations/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ export const esES: TranslationData = {
navigation: {
group_overview: { label: 'Resumen' },
group_apps: { label: 'Aplicaciones' },
// Setup's own `nav_packages` — distinct from the same id under
// `apps.studio.navigation`, which does not answer for this app (#5750).
nav_packages: { label: 'Paquetes' },
nav_packaged_actions: { label: 'Acciones empaquetadas' },
nav_marketplace_browse: { label: 'Explorar Marketplace' },
nav_marketplace_installed: { label: 'Aplicaciones instaladas' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const jaJPSourceHashes: Readonly<Record<string, string>> = {
'apps.setup.description': 'deacdfc679232a4a',
'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf',
'apps.setup.navigation.group_apps.label': '699b367291abcbe6',
'apps.setup.navigation.nav_packages.label': '8cd8f29658025f73',
'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3',
'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd',
'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60',
Expand Down
3 changes: 0 additions & 3 deletions packages/platform-objects/src/apps/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ export const jaJP: TranslationData = {
navigation: {
group_overview: { label: '概要' },
group_apps: { label: 'アプリ' },
// Setup's own `nav_packages` — distinct from the same id under
// `apps.studio.navigation`, which does not answer for this app (#5750).
nav_packages: { label: 'パッケージ' },
nav_packaged_actions: { label: 'パッケージアクション' },
nav_marketplace_browse: { label: 'マーケットプレイスを閲覧' },
nav_marketplace_installed: { label: 'インストール済みアプリ' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const zhCNSourceHashes: Readonly<Record<string, string>> = {
'apps.setup.description': 'deacdfc679232a4a',
'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf',
'apps.setup.navigation.group_apps.label': '699b367291abcbe6',
'apps.setup.navigation.nav_packages.label': '8cd8f29658025f73',
'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3',
'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd',
'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60',
Expand Down
6 changes: 0 additions & 6 deletions packages/platform-objects/src/apps/translations/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ export const zhCN: TranslationData = {
navigation: {
group_overview: { label: '总览' },
group_apps: { label: '应用' },
// The Setup app's own `nav_packages`. A translation for this id already
// existed under `apps.studio.navigation` and did NOT answer here — the
// lookup is per app — so Setup fell back to the author's 'Packages'
// literal in an otherwise Chinese menu (#5750). Both entries are
// legitimate; keep them both.
nav_packages: { label: '软件包' },
nav_packaged_actions: { label: '打包动作' },
nav_marketplace_browse: { label: '浏览应用市场' },
nav_marketplace_installed: { label: '已安装应用' },
Expand Down
Loading