From 46ee8c2326232ba94d37fd454c7ec957b1b3e6bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 02:25:01 +0000 Subject: [PATCH 1/3] fix(app-shell): list a package's draft objects in the Access matrix, badged as drafts The package door read the published object list only, so a package whose objects were all drafts showed "0 objects" and could not be secured before its first publish. Under a packageId the matrix now reads the draft-overlaid list and each object's definition through MetadataClient.withPreviewDrafts, as the object picker does, and marks a draft row with the picker's draft mark. The environment door keeps the published reads. Co-Authored-By: Claude --- ...rmissionMatrixEditor.artifactTier.test.tsx | 2 + ...issionMatrixEditor.autosave-11787.test.tsx | 2 + .../PermissionMatrixEditor.badge.test.tsx | 2 + ...missionMatrixEditor.bulkMergeKeys.test.tsx | 2 + ...sionMatrixEditor.cloneToCustomize.test.tsx | 2 + ...rixEditor.companionPackageBinding.test.tsx | 2 + .../PermissionMatrixEditor.dirty.test.tsx | 2 + ...onMatrixEditor.draftObjects-26840.test.tsx | 162 ++++++++++++++++++ .../PermissionMatrixEditor.embedded.test.tsx | 2 + .../PermissionMatrixEditor.fieldBulk.test.tsx | 2 + ...ssionMatrixEditor.fieldWhen-25866.test.tsx | 2 + ...atrixEditor.firstSavePinned-11773.test.tsx | 2 + ...ionMatrixEditor.packageDoorFacets.test.tsx | 2 + ...ixEditor.packageDoorRereadRefusal.test.tsx | 2 + ...nMatrixEditor.readDecorationStrip.test.tsx | 2 + .../PermissionMatrixEditor.readonly.test.tsx | 2 + ...nMatrixEditor.readonlyHeaderBadge.test.tsx | 2 + ...ionMatrixEditor.recordScope-26187.test.tsx | 2 + ...trixEditor.rlsAddPolicyHold-13113.test.tsx | 2 + ...ionMatrixEditor.saveSurface-12725.test.tsx | 2 + .../PermissionMatrixEditor.scope.test.tsx | 2 + .../metadata-admin/PermissionMatrixEditor.tsx | 53 +++++- .../trailDoorsGate-12475.test.tsx | 2 + .../walledCatalogReadOnly-12183.test.tsx | 2 + ...gnSurface.accessCreateTaken-26836.test.tsx | 2 + ...ce.createAppOpensInterfaces-11794.test.tsx | 2 + ...tudioDesignSurface.pillarNavGuard.test.tsx | 2 + 27 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.draftObjects-26840.test.tsx diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx index e7dbe26ce55..717add7f431 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx @@ -87,6 +87,8 @@ function makeClient() { ...(provenance !== undefined ? { provenance } : {}), }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : [], get: async (type: string) => diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx index 96dddb2282c..ed2e33c20c7 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx @@ -56,6 +56,8 @@ function makeClient(server: Server, opts: { holdFirstSave?: Promise } = {} return { layered: async () => LAYERED, getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), get: async () => null, save: async ( diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx index e726a3875f4..98de4589744 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx @@ -33,6 +33,8 @@ function makeClient(set: Record) { ...(codeLayer ? { provenance: 'package' } : {}), }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : []), get: async (type: string) => (type === 'object' ? { fields: [] } : null), save: async (_t: string, _n: string, payload: Record) => payload, diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx index 8310ef86d16..61bcce22fa0 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx @@ -77,6 +77,8 @@ function makeClient(server: FakeServer) { return { layered: async () => ({ effective: server.set, code: null, overlay: null, overlayScope: null }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? server.objectNames.map((name) => ({ item: { name } })) : [], get: async (type: string) => (type === 'object' ? { fields: [] } : null), diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx index ffac81f83b6..d451e4fb838 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx @@ -89,6 +89,8 @@ function makeClient() { overlayScope: null, })), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : [], get: async (type: string) => diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.companionPackageBinding.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.companionPackageBinding.test.tsx index 6d6b99f7e9b..6e19b6eee95 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.companionPackageBinding.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.companionPackageBinding.test.tsx @@ -33,6 +33,8 @@ const SET = { const client = { layered: async () => ({ effective: SET, code: null, overlay: null, overlayScope: null }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), get: async () => null, save: async (_type: string, _name: string, payload: unknown) => payload, diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx index d6fcd4611b6..0672d442100 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx @@ -61,6 +61,8 @@ function makeClient(server: Server) { overlayScope: null, }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => { if (type === 'object') return [{ item: { name: 'a_account' } }]; return []; diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.draftObjects-26840.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.draftObjects-26840.test.tsx new file mode 100644 index 00000000000..24592b836c4 --- /dev/null +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.draftObjects-26840.test.tsx @@ -0,0 +1,162 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectstack#26840 — the package door's Access matrix lists the package's + * DRAFT objects too, so a new package can be secured before its first + * publish. + * + * The fake client behaves like the server's two reads of one package: + * + * • the published door (`list` / `get`) knows only the published object; + * • the draft-overlaid door (`withPreviewDrafts(true)`, `?preview=draft`) + * also serves each pending draft, with its `_draft` mark. + * + * The draft-only row carries the object picker's draft mark, its fields come + * from its draft, and its grants write into the set's draft. The environment + * door keeps the published list and its rows carry no mark. + */ + +import '@testing-library/jest-dom/vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +interface Server { + set: Record; + saved: Array>; + savedOpts: Array | undefined>; + listCalls: Array<{ preview: boolean; opts?: Record }>; +} + +const PUBLISHED = { name: 'a_account', label: 'Account', fields: { name: { label: 'Name' } } }; +const DRAFT_ONLY = { + name: 'a_invoice', + label: 'Invoice', + fields: { amount: { label: 'Amount' } }, + _draft: true, + _packageId: 'app.a', +}; + +function makeClient(server: Server, preview = false): any { + return { + withPreviewDrafts: (on: boolean) => makeClient(server, on), + layered: async () => ({ effective: server.set, code: null, overlay: null, overlayScope: null }), + getDraft: async () => null, + list: async (type: string, opts?: Record) => { + if (type !== 'object') return []; + server.listCalls.push({ preview, opts }); + return preview ? [PUBLISHED, DRAFT_ONLY] : [PUBLISHED]; + }, + get: async (type: string, name: string) => { + if (type !== 'object') return null; + if (name === PUBLISHED.name) return PUBLISHED; + // A draft-only object has no published definition. + return preview && name === DRAFT_ONLY.name ? DRAFT_ONLY : null; + }, + save: async (_type: string, _name: string, payload: Record, opts?: Record) => { + server.saved.push(payload); + server.savedOpts.push(opts); + server.set = payload; + return payload; + }, + }; +} + +let clientImpl: any; + +vi.mock('./useMetadata', () => ({ + useMetadataClient: () => clientImpl, + useMetadataTypes: () => ({ + loading: false, + error: null, + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], + }), +})); + +vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); + +import { PermissionMatrixEditPage } from './PermissionMatrixEditor'; + +afterEach(cleanup); + +function freshServer(): Server { + return { + set: { name: 'sales_perms', label: 'Sales', objects: {}, fields: {} }, + saved: [], + savedOpts: [], + listCalls: [], + }; +} + +function renderMatrix(opts: { packageId?: string; studio?: boolean } = {}) { + return render( + + {} } : undefined} + /> + , + ); +} + +const rowOf = (text: string) => screen.getByText(text).closest('tr')!; +const draftMarks = (row: HTMLElement) => row.querySelectorAll('[data-draft-marker]'); + +describe('PermissionMatrixEditPage — draft objects at the package door (objectstack#26840)', () => { + it('lists a draft-only object badged as a draft, and a published one without the badge', async () => { + clientImpl = makeClient(freshServer()); + renderMatrix({ packageId: 'app.a' }); + + await screen.findByText('Invoice'); + const invoiceMarks = draftMarks(rowOf('Invoice')); + expect(invoiceMarks).toHaveLength(1); + expect(invoiceMarks[0]).toHaveTextContent('(draft)'); + expect(draftMarks(rowOf('Account'))).toHaveLength(0); + }); + + it('badges the draft-only row in the Studio layout too', async () => { + clientImpl = makeClient(freshServer()); + renderMatrix({ packageId: 'app.a', studio: true }); + + await screen.findByText('Invoice'); + expect(draftMarks(rowOf('Invoice'))).toHaveLength(1); + expect(draftMarks(rowOf('Account'))).toHaveLength(0); + }); + + it("writes the draft-only object's grants into the set's draft", async () => { + const server = freshServer(); + clientImpl = makeClient(server); + renderMatrix({ packageId: 'app.a' }); + + await screen.findByText('Invoice'); + fireEvent.click(within(rowOf('Invoice')).getByLabelText('a_invoice Read')); + + await waitFor(() => expect(server.saved).toHaveLength(1), { timeout: 4000 }); + expect((server.saved[0] as any).objects.a_invoice).toMatchObject({ allowRead: true }); + expect(server.savedOpts[0]).toMatchObject({ mode: 'draft', packageId: 'app.a' }); + }); + + it("reads a draft-only object's fields from its draft", async () => { + clientImpl = makeClient(freshServer()); + renderMatrix({ packageId: 'app.a' }); + + fireEvent.click(await screen.findByRole('button', { name: /a_invoice/ })); + await screen.findByLabelText('a_invoice.amount readable'); + }); +}); + +describe('PermissionMatrixEditPage — environment door (objectstack#26840)', () => { + it('keeps the published list: no preview read, no draft rows, no badge', async () => { + const server = freshServer(); + clientImpl = makeClient(server); + renderMatrix(); + + await screen.findByText('Account'); + expect(screen.queryByText('Invoice')).not.toBeInTheDocument(); + expect(document.querySelectorAll('[data-draft-marker]')).toHaveLength(0); + expect(server.listCalls.length).toBeGreaterThan(0); + expect(server.listCalls.every((c) => !c.preview)).toBe(true); + }); +}); diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx index 420553da249..b5c147f0e6c 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx @@ -28,6 +28,8 @@ function makeClient() { overlayScope: null, }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, 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, diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx index 4cf84a6bbf0..1b0ab51f2db 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx @@ -39,6 +39,8 @@ function makeClient() { overlayScope: null, }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), get: async (type: string) => type === 'object' ? { fields: FIELDS.map((name) => ({ name })) } : null, diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx index ea214dd2ba8..49eb578afed 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx @@ -65,6 +65,8 @@ function makeClient(storedFields: Record): FakeClient { overlayScope: null, }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), get: async (type: string) => (type === 'object' ? { fields: FIELDS.map((name) => ({ name })) } : null), save: async (_type: string, _name: string, payload: Record) => { diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx index fdec25fcac7..5de0f31d37b 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx @@ -44,6 +44,8 @@ function makeClient(server: Server): FakeClient { server.draftReads.push(args); return server.draft ? { type: 'permission', name: 'sales_perms', item: server.draft.body, version: server.draft.version } : null; }, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : type === 'permission' ? [PUBLISHED] : []), get: async () => null, save: async (_type: string, _name: string, payload: Record, saveOpts?: Record) => { diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx index 88b32febf58..1c7027f8051 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx @@ -50,6 +50,8 @@ function makeClient(server: Server) { return { layered: async () => ({ effective: server.set, code: null, overlay: null, overlayScope: null }), getDraft: async () => (server.draft ? { item: server.draft } : null), + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => { if (type === 'object') return server.packageObjects.map((o) => ({ item: o })); return []; diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx index aeceb26c8c8..c0c5a73fc3d 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx @@ -141,6 +141,8 @@ function makeClient( getDraft: async () => (opts.draft ? { type: 'permission', name: 'sales_perms', item: opts.draft } : null), // The package declares `a_account` only — which is what narrows the draft // and is therefore what makes `payload` an unusable merge base. + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), get: async () => null, save: async ( diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx index 7f7bf325baf..c8db7a50d1a 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx @@ -127,6 +127,8 @@ function makeClient(server: Server) { // does for a published set, so the load reads its layers exactly as before // (call #1 above). A set the list does not hold is not asked while its // draft is in hand. + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? [{ item: { name: 'a_account' } }, { item: { name: 'a_contact' } }] diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx index 72aacc176c0..bba616aad46 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx @@ -75,6 +75,8 @@ function makeClient() { overlayScope: null, }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : [], get: async (type: string) => diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx index d99e1936a8f..7707330d706 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx @@ -77,6 +77,8 @@ function makeClient() { overlayScope: null, }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : [], get: async (type: string) => diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.recordScope-26187.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.recordScope-26187.test.tsx index 98838fc423d..ac2696b0edb 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.recordScope-26187.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.recordScope-26187.test.tsx @@ -42,6 +42,8 @@ function makeClient(server: FakeServer) { return { layered: async () => ({ effective: server.set, code: null, overlay: null, overlayScope: null }), getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => type === 'object' ? server.objectNames.map((name) => ({ item: { name } })) : [], get: async (type: string) => (type === 'object' ? { fields: [] } : null), diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx index f1ea910bac0..d748d0994ad 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx @@ -72,6 +72,8 @@ function makeClient(server: Server): FakeClient { return { layered: async () => LAYERED, getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), get: async () => null, save: async (_type: string, _name: string, payload: Record) => { diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.saveSurface-12725.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.saveSurface-12725.test.tsx index 3aa86c0f460..0655a67b8c6 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.saveSurface-12725.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.saveSurface-12725.test.tsx @@ -51,6 +51,8 @@ const wire = new MetadataClient({ baseUrl: '', fetch: vi.fn(serve) as unknown as /** The reads both hosts make, answered in memory; the save is the real client's. */ const client = { + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => { if (type === 'permission') return [{ name: SET.name, label: SET.label }]; if (type === 'object') return [{ name: 'a_account', label: 'Account' }]; diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx index daa6118e1b5..93d56b9fadd 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx @@ -50,6 +50,8 @@ function makeClient(server: Server) { // ADR-0086 P2: the package door reads any pending draft first. This fixture // models the published baseline only (no pending draft) → null. getDraft: async () => null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string, opts?: { packageId?: string }) => { if (type === 'object') { // The server scopes to the package — the panel must not see anything diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx index 796f5892598..cc122f4b865 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx @@ -195,6 +195,30 @@ interface ObjectSummary { * `owd` unset renders as the D1 fail-closed default (private). */ owd?: string; + /** + * Served from its pending draft (`_draft`), not from the published version + * (objectstack#26840). Only the package door reads drafts, so it is never + * set at environment scope. Badged with the object picker's draft mark. + */ + draft?: boolean; +} + +/** + * Read one object's merged definition for its field rows and the RLS editor. + * + * objectstack#26840 — under the package door the read goes through the + * draft-overlaid door, as the object list does: a draft-only object has no + * published definition, so the plain read answers it with no fields. The + * server serves the pending draft when there is one and the published object + * otherwise (`useObjectFields`, the same read). The environment door reads + * the published definition, as before. + */ +function readObjectDefinition( + client: ReturnType, + packageId: string | undefined, + objectName: string, +): Promise { + return (packageId ? client.withPreviewDrafts(true) : client).get('object', objectName); } /** @@ -853,7 +877,15 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, : readLayered(), // In package scope, list only the objects this package declares // (ADR-0086 P0) — otherwise the whole environment leaks into the panel. - client.list('object', packageId ? { packageId } : {}).catch(() => []), + // objectstack#26840 — and read the draft-overlaid list there, so a + // package whose objects are still drafts can be secured before its + // first publish: each pending draft is served with a `_draft` mark + // (the object picker's read, `useObjectCatalog`). The environment + // door keeps the published list: only the package door authors drafts. + (packageId + ? client.withPreviewDrafts(true).list('object', { packageId }) + : client.list('object', {}) + ).catch(() => []), draftReadForSave, ]); if (cancelled) return; @@ -892,6 +924,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, label: item?.label, accessDefault: item?.access?.default as ObjectSummary['accessDefault'], owd: typeof item?.sharingModel === 'string' ? item.sharingModel : undefined, + ...(item?._draft === true ? { draft: true } : {}), }; }) .filter((o) => !!o.name) @@ -938,7 +971,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // and misses fields carried inline on a published object, which left // this editor showing "no fields" for objects that clearly have them. // `fields` may come back as an array or as a `{ [name]: def }` map. - const obj = (await client.get('object', objectName)) as + const obj = (await readObjectDefinition(client, packageId, objectName)) as | { fields?: Record | Array } | null; const raw = obj?.fields; @@ -968,7 +1001,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, const loadObjectFields = React.useCallback( async (objectName: string): Promise => { try { - const obj = (await client.get('object', objectName)) as + const obj = (await readObjectDefinition(client, packageId, objectName)) as | { fields?: Record | Array } | null; const raw = obj?.fields; @@ -984,7 +1017,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, return []; } }, - [client], + [client, packageId], ); // Count of blocking CEL parse errors in the RLS editor — gates Save @@ -2385,6 +2418,12 @@ function PermissionTable({ {t('perm.posture.private')} )} + {o.draft && ( + // objectstack#26840 — the object picker's draft mark (`ObjectPicker`). + + {t('engine.inspector.draftSuffix')} + + )} )} + {o.draft && ( + // objectstack#26840 — the object picker's draft mark (`ObjectPicker`). + + {t('engine.inspector.draftSuffix')} + + )} []), listDrafts: vi.fn(async () => []), get: vi.fn(async () => null), diff --git a/ui/packages/app-shell/src/views/metadata-admin/walledCatalogReadOnly-12183.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/walledCatalogReadOnly-12183.test.tsx index 396e2117cd8..dfe56974dde 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/walledCatalogReadOnly-12183.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/walledCatalogReadOnly-12183.test.tsx @@ -106,6 +106,8 @@ function layeredFor(type: string) { /** Stable singleton — the pages' load effects key on the client identity. */ const CLIENT = { + // objectstack#26840 — the Access matrix reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: vi.fn(async (type: string) => { if (type === 'package') return [{ manifest: { id: PACKAGE, scope: 'project', name: 'Showcase' } }]; if (type === 'object') return [{ item: { name: 'a_account', label: 'Account' } }]; diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx index 7be7dd5f7ac..8de96f7d7cc 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessCreateTaken-26836.test.tsx @@ -97,6 +97,8 @@ function makeClient(seed: { published?: Record; drafts?: Record { if (type === 'object') { return [ diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.createAppOpensInterfaces-11794.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.createAppOpensInterfaces-11794.test.tsx index 65ea8566454..0671b9893fa 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.createAppOpensInterfaces-11794.test.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.createAppOpensInterfaces-11794.test.tsx @@ -86,6 +86,8 @@ let server: Server; function makeClient(s: Server) { return { + // objectstack#26840 — the Access matrix reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: vi.fn(async (type: string) => { if (type === 'permission') return [{ name: 'set_a', label: 'Set A' }]; if (type === 'object') return [{ name: 'a_account' }]; diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.pillarNavGuard.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.pillarNavGuard.test.tsx index 7980d9f68e4..7ee906a63a1 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.pillarNavGuard.test.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.pillarNavGuard.test.tsx @@ -128,6 +128,8 @@ function makeClient(server: Server) { // Serves the Access rail (list('permission')), the embedded matrix // (list('object') + object get), the header's app probe (list('app')), // and — after a pillar switch — the Interfaces pillar's app resolution. + // objectstack#26840 — the Access matrix reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => { if (type === 'permission') { return Object.values(server.byName).map((s) => ({ name: s.name, label: s.label })); From 20e78ee40243130816cd65e432a919e2a13a1fc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 03:25:44 +0000 Subject: [PATCH 2/3] test(app-shell): give the matrix's remaining package-door doubles the draft-overlaid read Co-Authored-By: Claude --- .../PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx | 2 ++ .../studio-design/StudioDesignSurface.accessGuard.test.tsx | 2 ++ .../studio-design/StudioDesignSurface.accessTabs-12456.test.tsx | 2 ++ 3 files changed, 6 insertions(+) diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx index f3db10449bf..2ee1d226497 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx @@ -68,6 +68,8 @@ function makeClient(objectPerm: Record, storedFields: Record null, + // objectstack#26840 — the package door reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_project' } }] : []), get: async (type: string) => (type === 'object' ? { fields: FIELDS.map((name) => ({ name })) } : null), save: async (_type: string, _name: string, payload: Record) => { diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessGuard.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessGuard.test.tsx index dd3ea5a6355..16b0286354e 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessGuard.test.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessGuard.test.tsx @@ -97,6 +97,8 @@ function makeClient(server: Server) { // Serves BOTH the pillar rail (list('permission', { packageId })) and the // embedded matrix (list('object', { packageId }) + list('permission', {}) // for the assignable-set allowlist). + // objectstack#26840 — the Access matrix reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => { if (type === 'permission') { return Object.values(server.byName).map((s) => ({ name: s.name, label: s.label })); diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessTabs-12456.test.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessTabs-12456.test.tsx index 574b76c45c0..a19653ac54b 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessTabs-12456.test.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessTabs-12456.test.tsx @@ -69,6 +69,8 @@ function makeClient() { }, }; return { + // objectstack#26840 — the Access matrix reads its objects through the draft-overlaid door. + withPreviewDrafts() { return this; }, list: async (type: string) => { if (type === 'permission') return Object.values(sets).map((s) => ({ name: s.name, label: s.label })); if (type === 'object') { From 7cf38c4ca7ed83f64a029c611352fb47cddd34fa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 03:30:48 +0000 Subject: [PATCH 3/3] test(app-shell): let the matrix's typed doubles answer withPreviewDrafts with the client Co-Authored-By: Claude --- .../PermissionMatrixEditor.autosave-11787.test.tsx | 3 ++- .../PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx | 3 ++- .../PermissionMatrixEditor.fieldWhen-25866.test.tsx | 3 ++- .../PermissionMatrixEditor.firstSavePinned-11773.test.tsx | 3 ++- .../PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx index ed2e33c20c7..78df9fb3fdf 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx @@ -50,7 +50,8 @@ function deferred() { } /** The slice of the metadata client the editor calls here. */ -type FakeClient = Record Promise>; +// `withPreviewDrafts` answers the client itself, not a promise (objectstack#26840). +type FakeClient = Record unknown>; function makeClient(server: Server, opts: { holdFirstSave?: Promise } = {}): FakeClient { return { diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx index 2ee1d226497..1dbf1f56522 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldRuleInherited-26597.test.tsx @@ -25,7 +25,8 @@ interface SavedSet { fields?: Record; } -type FakeClient = Record Promise>; +// `withPreviewDrafts` answers the client itself, not a promise (objectstack#26840). +type FakeClient = Record unknown>; let clientImpl: FakeClient; let saved: SavedSet[] = []; diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx index 9396b2c1c4d..3ec42dc0dbc 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldWhen-25866.test.tsx @@ -25,7 +25,8 @@ interface SavedSet { } /** The slice of the metadata client the editor calls here. */ -type FakeClient = Record Promise>; +// `withPreviewDrafts` answers the client itself, not a promise (objectstack#26840). +type FakeClient = Record unknown>; let clientImpl: FakeClient; let saved: SavedSet[] = []; diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx index 5de0f31d37b..a833423a324 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.firstSavePinned-11773.test.tsx @@ -35,7 +35,8 @@ interface Server { savedOpts: Array | undefined>; } -type FakeClient = Record Promise>; +// `withPreviewDrafts` answers the client itself, not a promise (objectstack#26840). +type FakeClient = Record unknown>; function makeClient(server: Server): FakeClient { return { diff --git a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx index d748d0994ad..7ff07cb3636 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.rlsAddPolicyHold-13113.test.tsx @@ -66,7 +66,8 @@ interface Server { judge: (body: Record) => Refusal | null; } -type FakeClient = Record Promise>; +// `withPreviewDrafts` answers the client itself, not a promise (objectstack#26840). +type FakeClient = Record unknown>; function makeClient(server: Server): FakeClient { return {