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
20 changes: 20 additions & 0 deletions .changeset/authz-a4-managed-by-tristate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@object-ui/app-shell": patch
---

feat(app-shell): A4 — permission-provenance tri-state badge (framework#2920)

The Studio permission-matrix editor's provenance badge was two-state
(package / custom). It is now a **tri-state — platform / package / admin(custom)**,
mirroring the unified `sys_*.managed_by` vocabulary landed in framework#2920 so
the Studio matrix and the Setup record page read the same source-of-truth
labels.

- `PermissionMatrixEditor` — `managedBy === 'platform'` renders a **Platform**
badge; `'package'` (or an active `packageId`) renders **Package**; everything
else (including legacy `'user'`) falls through to **Custom**.
- New `perm.badge.platform` i18n key (en + zh-CN).

The Setup record page surfaces provenance via the framework object's now-`select`
`managed_by` field (rendered by the generic record renderer), so no record-page
change is required here.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* A4 (framework#2920) — the provenance badge in the Studio permission matrix is
* a TRI-STATE (platform / package / admin-custom), mirroring the unified sys_*
* `managed_by` vocabulary. This guards each branch of the ternary.
*/

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

function makeClient(set: Record<string, unknown>) {
return {
layered: async () => ({ effective: set, code: null, overlay: null, overlayScope: null }),
getDraft: async () => null,
list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []),
get: async (type: string) => (type === 'object' ? { fields: [] } : null),
save: async (_t: string, _n: string, payload: Record<string, unknown>) => payload,
} as any;
}

let clientImpl: any;

vi.mock('./useMetadata', () => ({
useMetadataClient: () => clientImpl,
useMetadataTypes: () => ({
loading: false,
error: null,
entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }],
}),
}));
vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null }));

import { PermissionMatrixEditPage } from './PermissionMatrixEditor';

afterEach(cleanup);

function baseSet(extra: Record<string, unknown>) {
return { name: 'sales_perms', label: 'Sales', isProfile: false, objects: {}, fields: {}, ...extra };
}

async function renderWith(extra: Record<string, unknown>) {
clientImpl = makeClient(baseSet(extra));
render(
<MemoryRouter>
<PermissionMatrixEditPage type="permission" name="sales_perms" />
</MemoryRouter>,
);
// Wait for the load to settle (the label input renders after the fetch).
await screen.findByDisplayValue('Sales');
}

describe('PermissionMatrixEditPage — provenance tri-state badge (A4 framework#2920)', () => {
it('managedBy "platform" renders the Platform badge', async () => {
await renderWith({ managedBy: 'platform' });
expect(screen.getByText('Platform')).toBeInTheDocument();
expect(screen.queryByText('Custom')).not.toBeInTheDocument();
});

it('managedBy "package" renders the Package badge', async () => {
await renderWith({ managedBy: 'package' });
expect(screen.getByText('Package')).toBeInTheDocument();
});

it('managedBy "admin" renders the Custom badge', async () => {
await renderWith({ managedBy: 'admin' });
expect(screen.getByText('Custom')).toBeInTheDocument();
expect(screen.queryByText('Platform')).not.toBeInTheDocument();
});

it('legacy managedBy "user" still falls through to Custom (read compat)', async () => {
await renderWith({ managedBy: 'user' });
expect(screen.getByText('Custom')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,15 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
WHO OWNS the set (provenance, ADR-0086 D3) and whether it is the
package's suggested default (ADR-0090 D5). */}
<div className="flex items-center gap-1.5 pb-1">
{/* [A4 framework#2920] Provenance tri-state — platform / package /
admin(custom) — mirrors the unified sys_* `managed_by` vocab so
the Studio matrix and the Setup record page read the same. */}
<Badge variant="outline" className="text-[10px]">
{draft.managedBy === 'package' || packageId
? t('perm.badge.package')
: t('perm.badge.custom')}
{draft.managedBy === 'platform'
? t('perm.badge.platform')
: draft.managedBy === 'package' || packageId
? t('perm.badge.package')
: t('perm.badge.custom')}
</Badge>
{!!draft.isDefault && (
<Badge variant="secondary" className="text-[10px]">{t('perm.badge.default')}</Badge>
Expand Down
2 changes: 2 additions & 0 deletions packages/app-shell/src/views/metadata-admin/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
'perm.filter.empty': 'No objects match the filter.',
'perm.field.name': 'Name',
'perm.field.label': 'Label',
'perm.badge.platform': 'Platform',
'perm.badge.package': 'Package',
'perm.badge.custom': 'Custom',
'perm.badge.default': 'Default for new users',
Expand Down Expand Up @@ -1927,6 +1928,7 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
'perm.filter.empty': '没有匹配的对象。',
'perm.field.name': '名称',
'perm.field.label': '标签',
'perm.badge.platform': '平台内置',
'perm.badge.package': '包内置',
'perm.badge.custom': '本环境自定义',
'perm.badge.default': '新用户默认',
Expand Down
Loading