From ae81c1b525bfe7f82e7bb86a9a6b9864f7a7d06c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:17:13 +0000 Subject: [PATCH 1/4] test(app-shell): pin the position page's read-only permission-set previews (#26187) Red first: the Setup position page names its permission sets but shows nothing of what they grant. Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- .../PositionPermissionSetsSection.test.tsx | 149 ++++++++++++++++++ ...Page.positionPermissionSets-26187.test.tsx | 103 ++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx create mode 100644 ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.positionPermissionSets-26187.test.tsx diff --git a/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx new file mode 100644 index 00000000000..02cbb47bbfb --- /dev/null +++ b/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx @@ -0,0 +1,149 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The Setup position page shows, read-only, what each permission set the + * position names grants (objectstack#26187, ruling 6416233738 Q1). + * + * One `PermissionPreview` per named set, in the order the position names them, + * each under the set's name and a link to the set's page. No union of the sets + * is computed here and nothing is editable. A named set the server does not + * serve says so on one line, for that set only; a set whose read is refused + * says that instead, since a refused read is not an absent set. A position that + * names no set says so. + * + * The sets are read through the metadata client the page already uses + * (`client.get('permission', name)`), stubbed here. + */ + +import '@testing-library/jest-dom/vitest'; +import * as React from 'react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup, within, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +/** The sets the stubbed server serves, by name. */ +const SERVED: Record> = { + sales_read: { + name: 'sales_read', + label: 'Sales read', + objects: { account: { allowRead: true, readScope: 'unit_and_below' } }, + }, + sales_write: { + name: 'sales_write', + label: 'Sales write', + objects: { opportunity: { allowRead: true, allowEdit: true, readScope: 'org', writeScope: 'own' } }, + }, +}; + +/** Names whose read the stubbed server refuses. */ +const REFUSED = new Set(['locked_set']); + +const mockClient = { + get: vi.fn(async (type: string, name: string) => { + if (type !== 'permission') return null; + if (REFUSED.has(name)) throw new Error('You do not have permission to perform this action.'); + return SERVED[name] ?? null; + }), +}; + +vi.mock('./useMetadata', async (importOriginal) => ({ + ...(await importOriginal()), + useMetadataClient: () => mockClient, +})); + +import { PositionPermissionSetsSection } from './PositionPermissionSetsSection'; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +function mount(permissionSets: unknown) { + render( + + + , + ); +} + +/** The rendered set blocks, in document order. */ +const setBlocks = () => screen.getAllByTestId('position-permission-set'); + +describe('the position page shows each named permission set read-only (#26187)', () => { + it('two named sets give two previews, in declared order, each with its own scopes', async () => { + mount(['sales_write', 'sales_read']); + + await waitFor(() => expect(screen.getAllByTestId('position-permission-set-preview')).toHaveLength(2)); + const blocks = setBlocks(); + expect(blocks.map((b) => b.getAttribute('data-set-name'))).toEqual(['sales_write', 'sales_read']); + + // Each block's heading is the set's name, linking to the set's page in the + // same (environment) scope. + const headings = screen.getAllByRole('heading', { level: 3 }); + expect(headings.map((h) => h.textContent)).toEqual(['sales_write', 'sales_read']); + expect(within(blocks[0]).getByRole('link', { name: 'sales_write' })).toHaveAttribute( + 'href', + '/apps/setup/metadata/permission/sales_write?scope=environment', + ); + expect(within(blocks[1]).getByRole('link', { name: 'sales_read' })).toHaveAttribute( + 'href', + '/apps/setup/metadata/permission/sales_read?scope=environment', + ); + + // Each preview shows its OWN set's object rows and scopes: no union. + expect(within(blocks[0]).getByText('opportunity')).toBeInTheDocument(); + expect(within(blocks[0]).queryByText('account')).toBeNull(); + expect(within(blocks[0]).getByText('org')).toBeInTheDocument(); + expect(within(blocks[0]).getByText('own')).toBeInTheDocument(); + expect(within(blocks[1]).getByText('account')).toBeInTheDocument(); + expect(within(blocks[1]).queryByText('opportunity')).toBeNull(); + expect(within(blocks[1]).getByText('unit+below')).toBeInTheDocument(); + + expect(mockClient.get).toHaveBeenCalledWith('permission', 'sales_write'); + expect(mockClient.get).toHaveBeenCalledWith('permission', 'sales_read'); + }); + + it('is read-only: no form control is offered', async () => { + mount(['sales_write']); + await screen.findByTestId('position-permission-set-preview'); + const block = setBlocks()[0]; + expect(within(block).queryAllByRole('checkbox')).toHaveLength(0); + expect(within(block).queryAllByRole('combobox')).toHaveLength(0); + expect(within(block).queryAllByRole('button')).toHaveLength(0); + }); + + it('a named set the server does not serve shows one "not found" line, and only for that set', async () => { + mount(['sales_read', 'ghost_set', 'sales_write']); + + const missing = await screen.findByTestId('position-permission-set-missing'); + expect(missing).toHaveTextContent('ghost_set'); + expect(missing).toHaveTextContent(/not found/i); + await waitFor(() => expect(screen.getAllByTestId('position-permission-set-preview')).toHaveLength(2)); + expect(setBlocks().map((b) => b.getAttribute('data-set-name'))).toEqual(['sales_read', 'ghost_set', 'sales_write']); + expect(within(setBlocks()[1]).queryByTestId('position-permission-set-preview')).toBeNull(); + expect(screen.getAllByTestId('position-permission-set-missing')).toHaveLength(1); + }); + + it('a refused read is said as a refusal, not as "not found"', async () => { + mount(['locked_set', 'sales_read']); + + const failed = await screen.findByTestId('position-permission-set-read-failed'); + expect(failed).toHaveTextContent('locked_set'); + expect(failed).toHaveTextContent('You do not have permission to perform this action.'); + expect(screen.queryByTestId('position-permission-set-missing')).toBeNull(); + await waitFor(() => expect(screen.getAllByTestId('position-permission-set-preview')).toHaveLength(1)); + }); + + it('a position that names no set shows the empty state and reads nothing', async () => { + mount([]); + expect(await screen.findByTestId('position-permission-sets-empty')).toBeInTheDocument(); + expect(screen.queryByTestId('position-permission-set')).toBeNull(); + expect(mockClient.get).not.toHaveBeenCalled(); + }); + + it('a position with no permissionSets key at all shows the same empty state', async () => { + mount(undefined); + expect(await screen.findByTestId('position-permission-sets-empty')).toBeInTheDocument(); + expect(mockClient.get).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.positionPermissionSets-26187.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.positionPermissionSets-26187.test.tsx new file mode 100644 index 00000000000..50000605e3a --- /dev/null +++ b/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.positionPermissionSets-26187.test.tsx @@ -0,0 +1,103 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectstack#26187 — the Setup position page mounts the read-only permission + * previews: one per permission set the position names, in declared order, under + * the definition and beside the holders. Studio (no scope) does not, and nor + * does another type in the Setup scope. + */ +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +const POSITION = { name: 'sales_rep', label: 'Sales Rep', permissionSets: ['sales_write', 'sales_read'] }; + +const SETS: Record> = { + sales_read: { name: 'sales_read', objects: { account: { allowRead: true, readScope: 'unit' } } }, + sales_write: { name: 'sales_write', objects: { opportunity: { allowRead: true, allowEdit: true, writeScope: 'own' } } }, +}; + +const mockClient = { + list: vi.fn(async () => []), + listDrafts: vi.fn(async () => []), + get: vi.fn(async (type: string, name: string) => (type === 'permission' ? SETS[name] ?? null : null)), + getDraft: vi.fn(async () => null), + references: vi.fn(async () => []), + layered: vi.fn(async () => ({ + code: null, + overlay: POSITION, + overlayScope: null, + effective: POSITION, + provenance: 'org', + })), +}; + +const SCHEMA = { + type: 'object', + properties: { + name: { type: 'string' }, + label: { type: 'string' }, + permissionSets: { type: 'array', items: { type: 'string' } }, + }, +}; + +vi.mock('./useMetadata', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + useMetadataClient: () => mockClient, + useMetadataTypes: () => ({ + loading: false, + error: null, + entries: [ + { type: 'position', name: 'position', label: 'Position', allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: SCHEMA }, + { type: 'dashboard', name: 'dashboard', label: 'Dashboard', allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: SCHEMA }, + ], + }), + }; +}); +vi.mock('./PositionHoldersSection', () => ({ + PositionHoldersSection: ({ name }: { name: string }) =>
{name}
, +})); + +import { MetadataResourceEditPage } from './ResourceEditPage'; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +async function mount(type: string, search: string) { + render( + + + , + ); + await waitFor(() => expect(mockClient.layered).toHaveBeenCalled()); +} + +describe("the Setup position page shows its named permission sets read-only (#26187)", () => { + it('renders one preview per named set, in declared order, beside the holders', async () => { + await mount('position', '?scope=environment'); + + await waitFor(() => expect(screen.getAllByTestId('position-permission-set-preview')).toHaveLength(2)); + expect( + screen.getAllByTestId('position-permission-set').map((b) => b.getAttribute('data-set-name')), + ).toEqual(['sales_write', 'sales_read']); + expect(screen.getByTestId('position-holders-stub')).toHaveTextContent('sales_rep'); + }); + + it('does not render in Studio (no scope)', async () => { + await mount('position', ''); + await screen.findByDisplayValue('Sales Rep').catch(() => undefined); + expect(screen.queryByTestId('position-permission-sets')).toBeNull(); + expect(mockClient.get).not.toHaveBeenCalledWith('permission', expect.anything()); + }); + + it('does not render for another type in the Setup scope', async () => { + await mount('dashboard', '?scope=environment'); + await screen.findByDisplayValue('Sales Rep').catch(() => undefined); + expect(screen.queryByTestId('position-permission-sets')).toBeNull(); + }); +}); From 2e4b27383505712f8e37ba99794011a3057559cc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:35:31 +0000 Subject: [PATCH 2/4] fix(app-shell): the Setup position page previews each permission set it names, read-only (#26187) One PermissionPreview per named set, in declared order, under the set's name and a link to its page. No client-side union, no editing. A set the server does not serve shows one not-found line; a refused read says so. Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- .../PositionPermissionSetsSection.tsx | 173 ++++++++++++++++++ .../views/metadata-admin/ResourceEditPage.tsx | 24 ++- .../src/views/metadata-admin/i18n.ts | 17 ++ 3 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.tsx diff --git a/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.tsx b/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.tsx new file mode 100644 index 00000000000..83ca631209d --- /dev/null +++ b/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.tsx @@ -0,0 +1,173 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * PositionPermissionSetsSection — what the permission sets a position names + * grant, read-only, on the Setup catalog's position page (objectstack#26187, + * ruling 6416233738 Q1; ADR-0131 D4). + * + * A position grants access only through the permission sets its definition + * names (`permissionSets`). The position page used to list those names and show + * nothing of what they grant, so an admin reviewing a position could not see an + * object, a capability or a record scope without opening every set in turn. + * This section renders, for each named set, that set's own `PermissionPreview`: + * + * - one preview per named set, in the order the position names them, each + * under the set's name and a link to the set's page (same catalog scope); + * - no union of the sets is computed here: what a holder ends up with is the + * server's answer (the authorization resolver), not a client-side merge; + * - nothing is editable: the preview gets no `onPatch`, and a set is changed + * on its own page; + * - a named set the server does not serve (`client.get` answers `null` on a + * 404) shows one "not found" line for that set only; a refused or failed + * read is said as such, in the server's words, because a refused read is not + * an absent set; + * - a position that names no set says so. + * + * The names are read from the page's draft, as the position preview above this + * section reads them, so the list follows the definition on screen. Each set is + * read through the console metadata client the page itself uses. + */ + +import * as React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { AlertCircle, Loader2, ShieldCheck } from 'lucide-react'; +import { useMetadataClient } from './useMetadata.js'; +import { useMetadataLocale, t, tFormat } from './i18n.js'; +import { environmentScopeSuffix } from './catalog-scope.js'; +import { PermissionPreview } from './previews/PermissionPreview.js'; + +export interface PositionPermissionSetsSectionProps { + /** The position's machine name. */ + name: string; + /** The position definition on screen; only `permissionSets` is read. */ + draft: Readonly>; +} + +/** One named set's read, as this section shows it. */ +type SetRead = + | { state: 'loading' } + | { state: 'loaded'; body: Record } + | { state: 'missing' } + | { state: 'failed'; sentence: string }; + +/** The names a position definition declares, in order; anything else declares none. */ +export function declaredPermissionSets(draft: Readonly>): string[] { + const value = draft.permissionSets; + return Array.isArray(value) ? value.filter((s): s is string => typeof s === 'string' && s !== '') : []; +} + +/** The server's sentence out of a failed read, or `''` when it gave none. */ +function failureSentence(err: unknown): string { + return err instanceof Error && err.message ? err.message : ''; +} + +export function PositionPermissionSetsSection({ draft }: PositionPermissionSetsSectionProps) { + const client = useMetadataClient(); + const locale = useMetadataLocale(); + const [searchParams] = useSearchParams(); + const scopeSuffix = environmentScopeSuffix(searchParams); + + const declared = declaredPermissionSets(draft); + // The read effect keys on the list's content, not on the draft object, so an + // edit to another field of the position does not re-read every set. + const declaredKey = JSON.stringify(declared); + const [reads, setReads] = React.useState>({}); + + React.useEffect(() => { + const names: string[] = JSON.parse(declaredKey); + const unique = Array.from(new Set(names)); + let cancelled = false; + setReads(Object.fromEntries(unique.map((n) => [n, { state: 'loading' } as SetRead]))); + for (const setName of unique) { + client + .get>('permission', setName) + .then( + (body): SetRead => (body ? { state: 'loaded', body } : { state: 'missing' }), + (err: unknown): SetRead => ({ state: 'failed', sentence: failureSentence(err) }), + ) + .then((read) => { + if (!cancelled) setReads((prev) => ({ ...prev, [setName]: read })); + }); + } + return () => { + cancelled = true; + }; + }, [client, declaredKey]); + + return ( +
+
+
+ + {t('engine.position.sets.title', locale)} + {declared.length > 0 && ( + {declared.length} + )} +
+ {declared.length > 0 && ( +

{t('engine.position.sets.hint', locale)}

+ )} +
+
+ {declared.length === 0 ? ( +

+ {t('engine.position.sets.empty', locale)} +

+ ) : ( +
    + {declared.map((setName, i) => { + const read = reads[setName] ?? { state: 'loading' }; + return ( +
  1. +

    + + {setName} + +

    + {read.state === 'loading' && ( +

    + + {t('engine.position.sets.loading', locale)} +

    + )} + {read.state === 'missing' && ( +

    + {tFormat('engine.position.sets.notFound', locale, { name: setName })} +

    + )} + {read.state === 'failed' && ( +

    + + + {tFormat('engine.position.sets.readFailed', locale, { name: setName })} + {read.sentence && <> {read.sentence}} + +

    + )} + {read.state === 'loaded' && ( +
    + +
    + )} +
  2. + ); + })} +
+ )} +
+
+ ); +} diff --git a/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx b/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx index f64c616c035..b37a3b2c213 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx @@ -152,18 +152,28 @@ import { errorCodeIs, errorCodeIsAnyOf } from '@object-ui/types'; import { useCanAuthorMetadata, useCanReadAuthoringTrail } from '../../hooks/useCanAuthorMetadata.js'; import { environmentScopeSuffix, isEnvironmentScope, useWalledCatalogReadOnly } from './catalog-scope.js'; import { PositionHoldersSection } from './PositionHoldersSection.js'; +import { PositionPermissionSetsSection } from './PositionPermissionSetsSection.js'; import { declarationOnlyAuthoredObject } from './sharing-rule-catalog.js'; import { useRegisterUnsavedEdits } from '../../chrome/unsavedEdits.js'; import { RefusalMessage } from '../../components/RefusalMessage.js'; +/** What an environment section is handed: the item's name and its definition on screen. */ +interface EnvironmentSectionProps { + name: string; + draft: Readonly>; +} + /** * objectui#7611 — the Setup half of an item, rendered under its definition in - * the ENVIRONMENT scope (`catalog-scope.ts`): who holds it. Keyed by type and + * the ENVIRONMENT scope (`catalog-scope.ts`), in this order. Keyed by type and * internal to this page; the permission set's half lives in its own editor, * `PermissionMatrixEditPage`, which renders `AssignedUsersSection`. + * + * A position has two: who holds it, and — objectstack#26187 — what each + * permission set it names grants, read-only, one preview per set. */ -const ENVIRONMENT_SECTIONS: Readonly>> = { - position: PositionHoldersSection, +const ENVIRONMENT_SECTIONS: Readonly>>> = { + position: [PositionHoldersSection, PositionPermissionSetsSection], }; /** @@ -2114,7 +2124,7 @@ function MetadataResourceEditPageImpl({ const readOnly = !canWrite && !createMode; const envScope = isEnvironmentScope(searchParams); const scopeSuffix = environmentScopeSuffix(searchParams); - const EnvironmentSection = envScope && !createMode && !embedded ? ENVIRONMENT_SECTIONS[type] : undefined; + const environmentSections = envScope && !createMode && !embedded ? ENVIRONMENT_SECTIONS[type] : undefined; // #2272 — designer deep-link: `?sel=nav:` selects the nav item with // that spec `id` (stable across reorders, unlike the positional selection @@ -3246,9 +3256,11 @@ function MetadataResourceEditPageImpl({ {/* objectui#7611 — under the designer, too: `position` renders through its preview, so the section takes a bounded strip below the canvas instead of squeezing it. */} - {EnvironmentSection && ( + {environmentSections && (
- + {environmentSections.map((EnvironmentSection, i) => ( + + ))}
)} diff --git a/ui/packages/app-shell/src/views/metadata-admin/i18n.ts b/ui/packages/app-shell/src/views/metadata-admin/i18n.ts index 0376c3d9943..a9436055458 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/ui/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -255,6 +255,16 @@ const ENGINE_STRINGS_EN: Record = { 'engine.position.grants.byName': '(by name)', 'engine.position.grants.byNameHint': 'Granted because this permission set has the same name as the position. The position does not list it, but holders get it.', + // objectstack#26187 — the Setup position page's read-only previews of the + // permission sets the position names (`PositionPermissionSetsSection.tsx`). + 'engine.position.sets.title': 'Permission sets', + 'engine.position.sets.hint': + 'What each permission set this position names grants, in the order it names them. Open a set to change it.', + 'engine.position.sets.empty': 'This position names no permission sets.', + 'engine.position.sets.loading': 'Loading…', + 'engine.position.sets.notFound': 'Permission set "{name}" was not found.', + 'engine.position.sets.readFailed': 'Permission set "{name}" could not be read.', + 'engine.position.sets.open': 'Open this permission set', 'engine.list.emptyType': 'No {type} items registered', 'engine.list.emptyQuery': 'No matches for "{query}"', 'engine.list.createHint': 'Click "New" above to create the first {type}.', @@ -4062,6 +4072,13 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.position.grants.none': '无', 'engine.position.grants.byName': '(同名)', 'engine.position.grants.byNameHint': '因与岗位同名而授予。岗位定义未列出它,但持有人会获得它。', + 'engine.position.sets.title': '权限集', + 'engine.position.sets.hint': '此岗位列出的每个权限集授予的权限,按列出顺序显示。打开权限集可修改它。', + 'engine.position.sets.empty': '此岗位未列出任何权限集。', + 'engine.position.sets.loading': '加载中…', + 'engine.position.sets.notFound': '未找到权限集"{name}"。', + 'engine.position.sets.readFailed': '无法读取权限集"{name}"。', + 'engine.position.sets.open': '打开此权限集', 'engine.list.emptyType': '暂无 {type} 条目', 'engine.list.emptyQuery': '没有匹配 "{query}" 的结果', 'engine.list.createHint': '点击上方"新建"创建第一个 {type}。', From bab09d7c818ae156225008fb85dc30de0cc0726f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 21:30:56 +0000 Subject: [PATCH 3/4] chore(changeset): app-shell patch for the position page's permission-set previews (#26187) Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- .../26187-position-permission-preview.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/26187-position-permission-preview.md diff --git a/.changeset/26187-position-permission-preview.md b/.changeset/26187-position-permission-preview.md new file mode 100644 index 00000000000..77601a70840 --- /dev/null +++ b/.changeset/26187-position-permission-preview.md @@ -0,0 +1,19 @@ +--- +'@object-ui/app-shell': patch +--- + +fix(app-shell): the Setup position page shows, read-only, what each permission set the position names grants + +Clause-②: no +Metadata-protocol: none + +A position grants access only through the permission sets its definition names in `permissionSets`. The position page in Setup → Positions listed those names but showed nothing of what they grant, so an admin had to open every set to see its objects, capabilities and record scopes. + +- **One preview per named set.** Under the position's definition, beside its holders, the page now shows each named set's permission preview: the object × capability grid with each object's read and write scope, field-level overrides, row-level policies, system permissions and tabs. The previews follow the order the position names the sets. Each is headed by the set's name, which links to the set's own page. +- **No merge, no editing.** Each preview shows one set as stored. The page does not compute what the sets add up to, and nothing in the previews is editable. A set is changed on its own page. +- **A set that does not load.** A named set the server does not serve shows one line, `Permission set "NAME" was not found.`, for that set only; the other previews still show. A read the server refuses says that, with the server's sentence, instead of "not found". +- **No sets.** A position that names no permission sets says so. + +The permission-set page itself is unchanged: its matrix already shows each object's read and write scope. + +What to do: nothing. No spec key, stored shape or API changes. From 1de02f3d4ba63d3981d56d242b016a572017bb3b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 22:04:58 +0000 Subject: [PATCH 4/4] test(app-shell): mount the section under the console's metadata route (#26187) A path-relative link resolves against the matched route's pathname, so the href pin needs the route the console mounts the page under. Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude --- .../PositionPermissionSetsSection.test.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx index 02cbb47bbfb..d1fc4ecb933 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PositionPermissionSetsSection.test.tsx @@ -19,7 +19,7 @@ import '@testing-library/jest-dom/vitest'; import * as React from 'react'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, within, waitFor } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; /** The sets the stubbed server serves, by name. */ const SERVED: Record> = { @@ -58,10 +58,20 @@ afterEach(() => { vi.clearAllMocks(); }); +/** + * Mounted as the console mounts the page: the element of the `metadata/:type/:name` + * route (`AppContent.tsx`), since a path-relative link resolves against the + * matched route's pathname. + */ function mount(permissionSets: unknown) { render( - + + } + /> + , ); }