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
21 changes: 21 additions & 0 deletions .changeset/12141-allow-environment-overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@object-ui/app-shell': minor
---

Studio and the metadata admin read a metadata type's overlay flag under its new name, `allowEnvironmentOverlay`, and the read-only banners name `OS_METADATA_WRITABLE` (objectui#12141, the objectui half of objectstack-ai/objectstack#22340 S2).

BREAKING (`@object-ui/app-shell`): the `RichMetadataTypeEntry` member `allowOrgOverride` is renamed to `allowEnvironmentOverlay`, and so is the member `resolveResourceConfig` accepts on its server entry and returns. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)

- FROM `allowOrgOverride` → TO `allowEnvironmentOverlay`

Why: the server renames the `GET /meta/types` field `entries[].allowOrgOverride` to `entries[].allowEnvironmentOverlay` and writes only the new name (objectstack-ai/objectstack#22340, ruled Q1 A / Q2 A). Its meaning is unchanged: may an environment overlay an item of this type that a package ships (ADR-0131 D6). There is no alias window and no dual read. Every reader in the metadata admin and Studio reads the new name only: the editor's write gates, the permission matrix, the type list's create button, the embedded item editor, the directory's "Writable only" filter and its tiles, and the page header's badge.

**Migration.** A host that reads `RichMetadataTypeEntry` or `resolveResourceConfig`'s result renames the member; its value stays the same. Run this release against a server that carries objectstack-ai/objectstack#22340 S2. Against an older server, the field this release reads is absent: every packaged item of the five overlay types (`view`, `dashboard`, `report`, `translation`, `email_template`) opens read-only, and their directory tiles drop from "writable" to "create-only".

The read-only messages now name only remedies that exist:

- **The read-only type banner** used to tell an admin to set `OBJECTSTACK_METADATA_WRITABLE` and to "flip `allowOrgOverride` in the registry". The server reads only `OS_METADATA_WRITABLE`, and the registry is a TypeScript literal in `@objectstack/spec` that no deployment edits. The banner now names `OS_METADATA_WRITABLE` and says what it opens: the type's items that no package ships, never an item a package ships. It offers no registry flip. English and Chinese both change.
- **The "writable" badge** on a type the env var elevates (directory tile and page header) names `OS_METADATA_WRITABLE` in its tooltip, with the same reach.
- **The permission matrix's "provided by a code package" hint** no longer lists the env var among its routes, since the env var does not open a set a package ships. It says so instead. "Clone to customize", redeploying the source and creating a new runtime set are unchanged. The hint for a type with no runtime write channel keeps the env var as a remedy and states its reach. English and Chinese both change.

**Clause-②: yes.** `RichMetadataTypeEntry`, exported from the package entry, loses the member `allowOrgOverride` and gains `allowEnvironmentOverlay`, with the same optional boolean type. `resolveResourceConfig`, also exported, does the same on its parameter and its return type. No other export, prop or i18n key is added or removed: the banner and hint strings live in the metadata admin's own table, and the banner drops its `{override}` placeholder.
2 changes: 1 addition & 1 deletion content/docs/guide/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The console opens at **http://localhost:5180** (the port is fixed in `apps/conso
| **Package-less Flows in Studio** | Flows that belong to no package — such as a clone of a packaged flow, made from Setup › Packaged automation — are listed and edited at `/studio/~org/automations`, reached from the Studio home ("Not in a package") or the package switcher. They open editable, edits save as drafts, and Publish promotes those drafts. |
| **Studio Automations Rail** | The Automations pillar's flow rail has a search box that matches a flow's label or its machine name, case-insensitively. A long flow name wraps instead of being cut off. |
| **Create App in Studio** | In a package that has no app yet, the Studio header's **Create app** saves the new app as a draft and then opens the **Interfaces** pillar, where that app is designed. Leaving a pillar that holds an unsent edit asks first. |
| **Read-only Packages in Studio** | The Studio home lists code and installed packages under "Installed (read-only · browsable)". Clicking a card opens the package for browsing (`/studio/:packageId/data`). The card's **Customize with an overlay** link opens the package's metadata directory (`/apps/setup/metadata?package=:packageId`). Whether an item can take an org overlay is decided per metadata type (`allowOrgOverride`), not per package, and the directory marks each type. Unless the runtime reports `features.marketplace: false`, the section also links to the marketplace (`/apps/setup/system/marketplace`), the same route as Home's "Start with a template". |
| **Read-only Packages in Studio** | The Studio home lists code and installed packages under "Installed (read-only · browsable)". Clicking a card opens the package for browsing (`/studio/:packageId/data`). The card's **Customize with an overlay** link opens the package's metadata directory (`/apps/setup/metadata?package=:packageId`). Whether an item can take an org overlay is decided per metadata type (`allowEnvironmentOverlay`), not per package, and the directory marks each type. Unless the runtime reports `features.marketplace: false`, the section also links to the marketplace (`/apps/setup/system/marketplace`), the same route as Home's "Start with a template". |
| **Design in Studio** | Workspace admins get a top-bar entry inside a running app that opens its owning package on the Studio design surface. On an interface route — a dashboard, page, or report — it deep-links straight to that surface's design page in the Interfaces pillar (`/studio/:packageId/interfaces?surface=<type>:<name>`, e.g. `surface=page:showcase_crm_workbench`); elsewhere (objects, the app root) it opens the package's Data tab (`/studio/:packageId/data`). These interfaces are authored in Studio — there is no in-page edit panel. |
| **Dashboard Refresh** | A dashboard page shows a **Refresh All** button above its widgets, and a dashboard that sets `refreshIntervalSeconds` (Studio's auto-refresh field) re-reads its widgets' data every that many seconds. Widgets re-read in place, so they are not remounted. `0` or no value means no automatic refresh. Known gap: a dataset-bound single-value (KPI) tile does not refresh yet. |
| **Refused Dashboards and List Views** | The server lists only the dashboards and list views a user may open, so one gated by `requiredPermissions` is not in their lists. When such a dashboard or list view is opened by its URL, the console asks the server about it by name. If the server refuses it with `403 PERMISSION_DENIED`, the page says "You don't have access" and shows the server's sentence, which names the item. The page does not say "not found", and the list page does not switch to the default view. For a name that does not exist, the dashboard page still says it was not found and the list page still opens the default view. An object's own list view that the server sends whole to a user who may edit the object is listed and opens. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const HOOK_ENTRY = {
name: 'hook',
label: 'Hook',
// What makes the item writable, and therefore what makes an Edit button exist.
allowOrgOverride: true,
allowEnvironmentOverlay: true,
schema: {
type: 'object',
properties: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* objectui#12141 — the metadata directory's type tiles and the page header's
* writability badge read `/meta/types` `allowEnvironmentOverlay`, the name the
* server writes after objectstack-ai/objectstack#22340 S2, and the badge for a
* hatch-elevated type names `OS_METADATA_WRITABLE`.
*
* Two surfaces draw the same badge from the same entry: the directory's tile
* (`DirectoryPage`) and the header of every list and edit page (`PageShell`).
* Both are pinned, because each holds its own copy of the badge.
*
* What is pinned, each against a control:
*
* - the five overlay types (`view`, `dashboard`, `report`, `translation`,
* `email_template`), served as the S2 server writes them, are badged
* writable and counted by "Writable only";
* - a type whose ONLY open door is the overlay flag — `agent` elevated by the
* `OS_METADATA_WRITABLE` hatch, which grants no runtime create — is badged
* writable and counted, so the disjunction reads the new field and not just
* `allowRuntimeCreate`; its tooltip names `OS_METADATA_WRITABLE` (never the
* legacy `OBJECTSTACK_`-prefixed name) and what the hatch opens;
* - the negative control: `api`, without the field, is badged read-only and
* not counted; and the same `agent` entry served without the field drops to
* read-only, so nothing else opens it.
*/

import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, cleanup, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import type { RichMetadataTypeEntry } from './useMetadata';

const PKG = 'com.example.showcase';
const OVERLAY_TYPES = ['view', 'dashboard', 'report', 'translation', 'email_template'] as const;
const HATCH_TITLE_FRAGMENT = 'OS_METADATA_WRITABLE';

/** The entry the S2 server writes for one of the five: both doors open, from the registry. */
function overlayEntry(type: string): RichMetadataTypeEntry {
return {
type,
label: type,
domain: 'ui',
allowEnvironmentOverlay: true,
allowRuntimeCreate: true,
overrideSource: 'registry',
};
}

/** `agent` with `OS_METADATA_WRITABLE=agent`: the overlay flag is its only open door. */
const HATCHED_AGENT: RichMetadataTypeEntry = {
type: 'agent',
label: 'Agent',
domain: 'ai',
allowEnvironmentOverlay: true,
allowRuntimeCreate: false,
overrideSource: 'env',
};

const API: RichMetadataTypeEntry = {
type: 'api',
label: 'API Endpoint',
domain: 'system',
allowEnvironmentOverlay: false,
allowRuntimeCreate: false,
overrideSource: 'registry',
};

let served: RichMetadataTypeEntry[] = [];

/** Stable singleton: the page's package load effect keys on the client identity. */
const CLIENT = {
list: async (type: string) =>
type === 'package' ? [{ manifest: { id: PKG, scope: 'project', name: 'Showcase' } }] : [],
};

vi.mock('./useMetadata', async (importOriginal) => {
const mod = await importOriginal<typeof import('./useMetadata')>();
return {
...mod,
useMetadataClient: () => CLIENT,
useMetadataTypes: () => ({ loading: false, error: null, entries: served }),
// Every served type is contributed by the one project package, so the
// directory's package scope keeps all of them.
useGlobalDiagnostics: () => ({
loading: false,
error: null,
summary: { entries: [], total: 0, scannedTypes: 0, scannedItems: 0 },
byType: {},
warnByType: {},
countsByType: {},
lockedByType: {},
packagesByType: Object.fromEntries(served.map((e) => [e.type, [PKG]])),
allPackages: [PKG],
reload: () => {},
}),
};
});

import { MetadataDirectoryPage } from './DirectoryPage';
import { PageShell } from './PageShell';

afterEach(() => {
cleanup();
});

function renderDirectory() {
return render(
<MemoryRouter initialEntries={[`/metadata?package=${PKG}`]}>
<MetadataDirectoryPage />
</MemoryRouter>,
);
}

/** The directory tile for one type, addressed by the machine name it prints. */
async function tile(type: string): Promise<HTMLElement> {
const code = await screen.findByText(type, { selector: 'code' });
return code.closest('a') as HTMLElement;
}

describe('DirectoryPage — the type tiles read `allowEnvironmentOverlay` (objectui#12141)', () => {
it('the five overlay types and a hatch-elevated type are writable; `api` is not', async () => {
served = [...OVERLAY_TYPES.map(overlayEntry), HATCHED_AGENT, API];
renderDirectory();

for (const type of OVERLAY_TYPES) {
const badge = within(await tile(type)).getByText('writable');
expect(badge).toHaveAttribute('title', 'Writable per ADR-0005 overlay opt-in');
}

const hatched = within(await tile('agent')).getByText('writable');
const title = hatched.getAttribute('title') ?? '';
expect(title).toContain(HATCH_TITLE_FRAGMENT);
expect(title).toContain('never an item a package ships');
expect(title).not.toContain('OBJECTSTACK_METADATA_WRITABLE');

expect(within(await tile('api')).getByText('read-only')).toBeInTheDocument();
expect(within(await tile('api')).queryByText('writable')).toBeNull();

// "Writable only" counts the five and the hatched type: six of seven.
expect(screen.getByRole('button', { name: /Writable only \(6\)/ })).toBeInTheDocument();
});

it('the hatch-elevated type served WITHOUT the field is read-only and not counted', async () => {
const { allowEnvironmentOverlay: _omitted, ...withoutField } = HATCHED_AGENT;
served = [withoutField, API];
renderDirectory();

expect(within(await tile('agent')).getByText('read-only')).toBeInTheDocument();
expect(within(await tile('agent')).queryByText('writable')).toBeNull();
expect(screen.getByRole('button', { name: /Writable only \(0\)/ })).toBeInTheDocument();
});
});

describe('PageShell — the header badge reads `allowEnvironmentOverlay` (objectui#12141)', () => {
function renderShell(entry: RichMetadataTypeEntry) {
return render(
<MemoryRouter initialEntries={[`/metadata/${entry.type}`]}>
<PageShell entry={entry}>
<div />
</PageShell>
</MemoryRouter>,
);
}

it.each(OVERLAY_TYPES)('%s: badged writable from the registry opt-in', (type) => {
renderShell(overlayEntry(type));
expect(screen.getByText('writable')).toHaveAttribute('title', 'Writable per ADR-0005 overlay opt-in');
});

it('a hatch-elevated type: the tooltip names OS_METADATA_WRITABLE and what it opens', () => {
renderShell(HATCHED_AGENT);
const title = screen.getByText('writable').getAttribute('title') ?? '';
expect(title).toContain(HATCH_TITLE_FRAGMENT);
expect(title).toContain('never an item a package ships');
expect(title).not.toContain('OBJECTSTACK_METADATA_WRITABLE');
});

it('negative control: `api` without the field is badged read-only', () => {
renderShell(API);
expect(screen.getByText('read-only')).toBeInTheDocument();
expect(screen.queryByText('writable')).toBeNull();
});

it('the hatch-elevated entry served WITHOUT the field is not badged writable', () => {
const { allowEnvironmentOverlay: _omitted, ...withoutField } = HATCHED_AGENT;
renderShell(withoutField);
expect(screen.queryByText('writable')).toBeNull();
expect(screen.getByText('read-only')).toBeInTheDocument();
});
});
8 changes: 4 additions & 4 deletions packages/app-shell/src/views/metadata-admin/DirectoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ export function MetadataDirectoryPage() {
}, [scopedEntries]);

const writableCount = scopedEntries.filter(
(e) => e.allowOrgOverride || e.allowRuntimeCreate,
(e) => e.allowEnvironmentOverlay || e.allowRuntimeCreate,
).length;

const filtered = scopedEntries.filter((e) => {
if (writableOnly && !(e.allowOrgOverride || e.allowRuntimeCreate)) return false;
if (writableOnly && !(e.allowEnvironmentOverlay || e.allowRuntimeCreate)) return false;
if (domainFilter !== 'all' && (e.domain ?? 'other') !== domainFilter) return false;
if (query) {
const q = query.toLowerCase();
Expand Down Expand Up @@ -455,7 +455,7 @@ function TypeTile({
{lockedCount}
</Badge>
)}
{entry.allowOrgOverride ? (
{entry.allowEnvironmentOverlay ? (
<Badge
className={
'text-[10px] ' +
Expand All @@ -465,7 +465,7 @@ function TypeTile({
}
title={
entry.overrideSource === 'env'
? 'Writable via OBJECTSTACK_METADATA_WRITABLE env var'
? 'Writable via the OS_METADATA_WRITABLE env var: it opens the items no package ships, never an item a package ships'
: 'Writable per ADR-0005 overlay opt-in'
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ vi.mock('./useMetadata', () => ({
useMetadataTypes: () => ({
loading: false,
error: null,
entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }],
entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }],
}),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ vi.mock('./useMetadata', () => ({
useMetadataTypes: () => ({
loading: false,
error: null,
entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }],
entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }],
}),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ vi.mock('./useMetadata', () => ({
useMetadataTypes: () => ({
loading: false,
error: null,
entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }],
entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }],
}),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ vi.mock('./useMetadata', () => ({
{
type: 'validation',
label: 'Validation Rule',
allowOrgOverride: true,
allowEnvironmentOverlay: true,
schema: VALIDATION_SCHEMA,
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ vi.mock('./useMetadata', () => ({
useMetadataTypes: () => ({
loading: false,
error: null,
entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }],
entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }],
}),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function EmbeddedItemEditor({
}, [initialRaw, itemName, parentType, parentName]);

const readOnly =
subEntry != null && !subEntry.allowOrgOverride && !subEntry.allowRuntimeCreate;
subEntry != null && !subEntry.allowEnvironmentOverlay && !subEntry.allowRuntimeCreate;

async function doSave() {
if (!embeddedPath) {
Expand Down
4 changes: 2 additions & 2 deletions packages/app-shell/src/views/metadata-admin/PageShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function WritabilityBadge({
);
}

if (entry?.allowOrgOverride) {
if (entry?.allowEnvironmentOverlay) {
return (
<Badge
className={
Expand All @@ -108,7 +108,7 @@ function WritabilityBadge({
}
title={
entry.overrideSource === 'env'
? 'Writable via OBJECTSTACK_METADATA_WRITABLE env var'
? 'Writable via the OS_METADATA_WRITABLE env var: it opens the items no package ships, never an item a package ships'
: 'Writable per ADR-0005 overlay opt-in'
}
>
Expand Down
Loading
Loading