diff --git a/.changeset/authz-a4-managed-by-tristate.md b/.changeset/authz-a4-managed-by-tristate.md new file mode 100644 index 000000000..559152c13 --- /dev/null +++ b/.changeset/authz-a4-managed-by-tristate.md @@ -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. diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx new file mode 100644 index 000000000..8fde8e835 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx @@ -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) { + 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) => 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) { + return { name: 'sales_perms', label: 'Sales', isProfile: false, objects: {}, fields: {}, ...extra }; +} + +async function renderWith(extra: Record) { + clientImpl = makeClient(baseSet(extra)); + render( + + + , + ); + // 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(); + }); +}); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx index 225f3a81c..0ecae185f 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx @@ -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). */}
+ {/* [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. */} - {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')} {!!draft.isDefault && ( {t('perm.badge.default')} diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index f95058a58..2e2f0ed04 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -720,6 +720,7 @@ const ENGINE_STRINGS_EN: Record = { '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', @@ -1927,6 +1928,7 @@ const ENGINE_STRINGS_ZH: Record = { 'perm.filter.empty': '没有匹配的对象。', 'perm.field.name': '名称', 'perm.field.label': '标签', + 'perm.badge.platform': '平台内置', 'perm.badge.package': '包内置', 'perm.badge.custom': '本环境自定义', 'perm.badge.default': '新用户默认',