Skip to content

Commit 6fc7872

Browse files
fix(app-shell): pair-added permission sets read Custom in the Studio Access header (#29835)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb3FmxaqPgrTCbqWhPEB9xY2)_ Fixes #29809 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Studio「权限」中,定制包(`.custom`)里的权限集(「新建类似」复制的,或在定制包里新建的)头部标签不再显示「包内置」,而是与左侧列表的「新增」一致,显示自定义标签。 - 为什么改:管理员打开自己复制的权限集,头部却说它是包内置的,无法判断这是谁的权限集、升级包时会不会被覆盖。 - 风险与回滚:只改头部标签选哪个词,判据取自左侧列表已用的定制标记(`customizationMarks.ts`),不新增读取;基础包自带的权限集、被定制的基础包权限集仍显示「包内置」;Setup 与元数据管理页不传标记,行为不变。回滚即还原本 PR。 - 先看:`PermissionMatrixEditor.tsx` 的 `shippedByPackage` 一行;`StudioDesignSurface.tsx` 中 Access 宿主传入 `customizationMark` 的一行;`StudioDesignSurface.accessPairCopy-29674.test.tsx` 新增的三个用例。 - 两分钟验证:`cd ui && pnpm exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.accessPairCopy-29674.test.tsx -t 29809`,应见 3 个用例通过(复制件、定制包新建件为自定义,基础包权限集为包内置)。 - 需要你做的:无,席位落地。 ## What changes - **The provenance rule** (`PermissionMatrixEditor.tsx`): `shippedByPackage` was `!!packageId || codeIsArtifact`. The pair's package door supplies a `packageId`, so every set a base's `.custom` pair holds read "Package" in the Studio header while the rail marked the same row "Added". It is now `customizationMark !== 'added' && (!!packageId || codeIsArtifact)`. Both badge sites (the page badge and the Studio header chip) still read that one value; no badge string, legend or key is touched. - **The mark comes from the host** (`StudioDesignSurface.tsx`, `AccessPillar`): a new optional page prop `customizationMark` (type `CustomizationMarkKind` from `customizationMarks.ts`). The pillar passes `railMarks?.markOf(...)` for the open set, the same call the rail row's `RailCustomizationMark` makes, so the rule is the one `customizationMarks.ts` holds (ADR-0146 D3) and is not derived a second time. - **Unchanged:** a set the read-only base ships still reads Package; a customized base set (a served override) still reads Package, since the base ships it; Setup's permission page and the metadata admin pass no mark. ## Tests - `StudioDesignSurface.accessPairCopy-29674.test.tsx`, one new top-level describe in the existing real-client harness (real `StudioDesignSurface`, `AccessPillar` and `PermissionMatrixEditPage` over the real metadata client and a fetch stub; base read-only, pair writable): - a New similar copy in the pair: the header chip reads Custom and the rail row carries the `added` mark; - a set created in the pair (its draft only, listed from the pair's drafts): the same; - CONTROL: the base's own set reads Package and its rail row carries no mark. - `StudioDesignSurface.accessTabs-12456.test.tsx` (the Package pin for a package's own set) stays green, unchanged. - Ablations, each through `scripts/ablation-replace.mjs` with the on-disk proof and a proven restore to HEAD: - the rule back to `!!packageId || codeIsArtifact`: the two pair cases red, the control green; - the host passing `customizationMark={null}`: the two pair cases red, the control green. <details><summary>Test runs (fix at 26d194d; re-run at the merged head 27943c8)</summary> - New describe plus `accessTabs-12456`: Test Files 2 passed (2), Tests 16 passed (16). - Every ui test file naming `PermissionMatrixEdit`, `AccessPillar` or `perm-provenance-badge` (68 files): Test Files 68 passed (68), Tests 474 passed (474). - `pnpm --filter @object-ui/app-shell run type-check`: exit 0. `eslint` on the three touched files: 0 errors (the warnings are on lines this diff does not touch). - At 27943c8 (after merging main, closure rebuilt): the same 68 files, Test Files 68 passed (68), Tests 474 passed (474); app-shell type-check exit 0; `node scripts/check-sdui-manifest.mjs` exit 0. </details> ## Acceptance notes - While the base's customization read is in flight, or after it fails, the rail draws no mark, so the header keeps its previous answer (Package) for a pair set. The two agree; no fallback to the row's `addedIn` was added, since that would be a second copy of the rule (seat ruling on the plan). - The chip's words are untouched here. The card #29729 renames the Custom badge's text, and whichever PR lands second merges main. - Not measured live: no dev server run. The real-client harness drives the surface end to end; the card measured the rail's "Added" live on `601832b1a6`, and an `added` mark now decides the header regardless of the layered `code` reading. ## Verification <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at 26d194d: 35 derived families, all run locally, all exit 0, `node scripts/check-sdui-manifest.mjs` among them. `--ran` reconciliation: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED". The merge of main touched none of this diff's files. Changed lines 86 (4 files), under the 3000 threshold. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jQi6dVYD8adyv8MhKt)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86d0a8c commit 6fc7872

4 files changed

Lines changed: 82 additions & 4 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): Studio Access no longer calls a permission set the `.custom` pair added "Package"
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A permission set held by a read-only base's `.custom` pair (a New similar copy, or a set created there) opened in Studio Access with a "Package" chip in its header, while the rail marked the same row "Added" and the page saved its edits as drafts in the pair. The header chip now reads the customization mark the rail draws, from the same rule (`customizationMarks.ts`): a set the pair adds reads "Custom", and a set the base itself ships, or a customized base set, still reads "Package". Setup and the metadata admin pass no mark and are unchanged. While the base's customization read is loading or has failed, neither the rail nor the header shows a mark, and the header keeps its previous answer.

‎ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import { useDraftAutoSave, type DraftSend } from '../studio-design/useDraftAutoS
106106
// objectui#12456 — the Studio Access pillar's presentation draws the shared item header.
107107
import { StudioItemHeader, StudioTab } from '../studio-design/StudioItemHeader.js';
108108
import { STUDIO_KIND_CHIP } from '../studio-design/studio-tokens.js';
109+
import type { CustomizationMarkKind } from '../studio-design/customizationMarks.js';
109110
import { useDisplayLocale } from '@object-ui/i18n';
110111
import {
111112
t as translate,
@@ -580,13 +581,23 @@ export interface PermissionMatrixEditPageProps {
580581
* is the routed page's. Omitted (Setup's permission page): unchanged.
581582
*/
582583
studio?: PermissionStudioChrome;
584+
/**
585+
* objectstack#29809 (ADR-0146 D3) — the customization mark the host's rail
586+
* draws for this set (`customizationMarks.ts`, the one rule): on a read-only
587+
* base, `'added'` for a set its `<base>.custom` pair holds (a New similar
588+
* copy or a set created there), `'customized'` for a served override of a
589+
* base set. Set only by the Studio Access pillar; omitted (Setup, the
590+
* metadata admin) or `null` (no mark, or the read has not answered),
591+
* nothing changes. Read by the provenance badge alone.
592+
*/
593+
customizationMark?: CustomizationMarkKind | null;
583594
}
584595

585596
/* ────────────────────────────────────────────────────────────────── */
586597
/* Component */
587598
/* ────────────────────────────────────────────────────────────────── */
588599

589-
export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, publishNonce, onOpenOwd, onDirtyChange, readOnly: readOnlyProp = false, embedded = false, studio }: PermissionMatrixEditPageProps) {
600+
export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, publishNonce, onOpenOwd, onDirtyChange, readOnly: readOnlyProp = false, embedded = false, studio, customizationMark = null }: PermissionMatrixEditPageProps) {
590601
// objectstack ADR-0146 D1 (#28561) — Studio's "All metadata" area mounts this
591602
// editor through the routed item page, which hands it no `readOnly`: a
592603
// read-only package's area is the host gate here, as a pillar's prop is.
@@ -724,7 +735,12 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
724735
// envelope's code artifact), else it was authored in this environment. Spec
725736
// 18 retired the authored `managedBy` (objectstack#23891), and no served
726737
// datum names a "platform" set, so there are two answers (objectui#12753).
727-
const shippedByPackage = !!packageId || codeIsArtifact;
738+
// objectstack#29809 (ADR-0146 D3) — a base's `<base>.custom` pair is a
739+
// package too, so its door's `packageId` alone called a set the pair adds
740+
// "Package" beside the rail's "Added". The host's mark answers that, by the
741+
// rule its rail draws from: an added set is the environment's own, whatever
742+
// door opened it. A customized base set stays the base's ("Package").
743+
const shippedByPackage = customizationMark !== 'added' && (!!packageId || codeIsArtifact);
728744
const artifactArmOpen = !!entry?.allowEnvironmentOverlay && servedEditable !== false;
729745
const typeTierOpen = !!(entry?.allowEnvironmentOverlay || entry?.allowRuntimeCreate);
730746
const canWriteByType = artifactTierApplies ? artifactArmOpen : typeTierOpen;
@@ -1960,7 +1976,8 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
19601976
this editor already fetches (`codeIsArtifact`, the artifact
19611977
gate's own input). At the environment scope there is no
19621978
`packageId`, so a set a code package ships read "Custom" beside
1963-
its own lock. */}
1979+
its own lock. A set a base's pair adds reads "Custom" (the host's
1980+
`customizationMark`, objectstack#29809). */}
19641981
<Badge variant="outline" className="text-[10px] shrink-0" data-testid="perm-provenance-badge">
19651982
{shippedByPackage ? t('perm.badge.package') : t('perm.badge.custom')}
19661983
</Badge>

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.accessPairCopy-29674.test.tsx‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
* - an edit to a base object's grant saves the copy's draft at the pair,
2121
* keeping every grant it holds; no write names the base;
2222
* - CONTROL: the base's own (read-only) set lists the base's objects and
23-
* reads no pair list for them.
23+
* reads no pair list for them;
24+
* - objectstack#29809: the header's provenance chip follows the rail's mark,
25+
* Custom for a set the pair holds (copied or created there), Package for
26+
* the base's own set.
2427
*/
2528

2629
import '@testing-library/jest-dom/vitest';
@@ -273,3 +276,48 @@ describe('CONTROL: the base’s own set (objectstack#29674)', () => {
273276
expect(header()).not.toHaveTextContent(tFormat('engine.studio.access.bannerPair', 'en', { package: 'Showcase' }));
274277
});
275278
});
279+
280+
/**
281+
* objectstack#29809 (ADR-0146 D3) — the header's provenance chip and the rail
282+
* row's customization mark tell one story: a set the pair holds reads Custom
283+
* beside the rail's Added, and the base's own set still reads Package with no
284+
* mark. Same harness: the real surface, pillar and page.
285+
*/
286+
describe('the header’s provenance chip follows the rail’s mark (objectstack#29809)', () => {
287+
/** A set created in the pair: its draft, never published, listed from the pair’s drafts. */
288+
const CREATED = 'pair_auditor';
289+
const accessRail = () => screen.getByTestId('studio-rail-access');
290+
/** The rail row holding `text`, and the kind of mark it carries (`null`: none). */
291+
const railMarkOf = (text: string) =>
292+
within(accessRail())
293+
.getByText(text)
294+
.closest('button')
295+
?.querySelector('[data-testid="customization-mark"]')
296+
?.getAttribute('data-kind') ?? null;
297+
const chip = () => within(header()).getByTestId('perm-provenance-badge');
298+
299+
it('a New similar copy in the pair: Custom in the header, Added in the rail', async () => {
300+
renderAccess(COPY);
301+
await copyOpen();
302+
await waitFor(() => expect(railMarkOf('My contributor')).toBe('added'), { timeout: 8000 });
303+
expect(chip()).toHaveTextContent(t('perm.badge.custom', 'en'));
304+
expect(chip()).not.toHaveTextContent(t('perm.badge.package', 'en'));
305+
});
306+
307+
it('a set created in the pair: Custom in the header, Added in the rail', async () => {
308+
srv.pairDrafts.set(`permission/${CREATED}`, { name: CREATED, label: 'Pair auditor', objects: {}, fields: {} });
309+
renderAccess(CREATED);
310+
await waitFor(() => expect(within(header()).getAllByText(CREATED).length).toBeGreaterThan(0), { timeout: 8000 });
311+
await waitFor(() => expect(railMarkOf(CREATED)).toBe('added'), { timeout: 8000 });
312+
expect(chip()).toHaveTextContent(t('perm.badge.custom', 'en'));
313+
});
314+
315+
it('CONTROL: the base’s own set reads Package, and its rail row carries no mark', async () => {
316+
renderAccess(BASE_SET.name);
317+
await waitFor(() => expect(within(header()).getByText('Contributor')).toBeInTheDocument(), { timeout: 8000 });
318+
// The read has answered: the copy's row is marked, the base set's is not.
319+
await waitFor(() => expect(railMarkOf('My contributor')).toBe('added'), { timeout: 8000 });
320+
expect(railMarkOf('Contributor')).toBeNull();
321+
expect(chip()).toHaveTextContent(t('perm.badge.package', 'en'));
322+
});
323+
});

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12572,6 +12572,9 @@ export function AccessPillar({
1257212572
readOnly={readOnly}
1257312573
onDirtyChange={setMatrixDirty}
1257412574
embedded
12575+
// objectstack#29809 — the header's provenance chip reads the
12576+
// mark this set's rail row draws, so the two tell one story.
12577+
customizationMark={railMarks?.markOf({ type: 'permission', name: current, addedIn: currentAddedIn }) ?? null}
1257512578
studio={{
1257612579
tab: accessTab,
1257712580
onTabChange: setAccessTab,

0 commit comments

Comments
 (0)