From 165e82050d8fb7c3ed00cb7c42ed4d3dbe73b1ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 11:43:34 +0000 Subject: [PATCH] feat(app-shell): Studio retires the two delegated-admin switches the spec retired The permission-set editor's Delegated Admin Scope facet no longer draws the "Manage bindings" and "Author env sets" switches. `@objectstack/spec` 18 retired `adminScope.manageBindings` and `adminScope.authorEnvironmentSets` as tombstones that refuse any value, so flipping either made the set unsaveable. Their four i18n strings go, the help text stops offering to manage bindings, and the remaining manage-assignments switch writes its key by name, without the cast that let a tombstoned key compile. The anchor pin now covers the one remaining manage switch, and three new cases pin that the facet draws neither retired switch and that flipping every switch it draws writes a scope the spec accepts. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude --- .changeset/12406-retire-admin-switches.md | 11 +++ ...onAdvancedFacets.adminScopeAnchor.test.tsx | 90 ++++++++++++++++--- .../PermissionAdvancedFacets.tsx | 44 ++++----- .../src/views/metadata-admin/i18n.ts | 8 +- 4 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 .changeset/12406-retire-admin-switches.md diff --git a/.changeset/12406-retire-admin-switches.md b/.changeset/12406-retire-admin-switches.md new file mode 100644 index 0000000000..b54b214ae8 --- /dev/null +++ b/.changeset/12406-retire-admin-switches.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': patch +--- + +Studio's permission-set editor no longer offers the two delegated-admin switches that `@objectstack/spec` 18 retired (objectui#12406, the objectui half of objectstack#23488). + +`adminScope.manageBindings` and `adminScope.authorEnvironmentSets` are now retired tombstones in the spec: the protocol refuses either key whatever it holds, `false` included, because no door ever honoured them. The Delegated Admin Scope facet drew a "Manage bindings" and an "Author env sets" switch that wrote them, so once a business unit was named, flipping either one made the whole permission set unsaveable. Both switches are gone, with their English and Chinese captions, and the facet's help text no longer offers to "manage bindings". "Manage assignments", "Include subtree", the business unit and the assignable permission sets are unchanged. + +A permission set stored with either key is cleaned by the server, not by this editor: the server reads stored and draft rows through the spec's stored-row conversion, which drops both keys before the body reaches Studio, and a re-save then stores the clean shape. + +**Clause-②: no.** Nothing on `@object-ui/app-shell`'s published entry changes: the facet component and the metadata-admin dictionary are internal to the package. The change removes two controls that wrote keys the spec already refuses, so no accepted input narrows and nothing widens. diff --git a/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.adminScopeAnchor.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.adminScopeAnchor.test.tsx index bf6bb49e7d..f20224c54a 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.adminScopeAnchor.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.adminScopeAnchor.test.tsx @@ -5,8 +5,8 @@ * framework spec refuses, and that a draft already carrying one says so on its * own collapsed header (objectui#9464). * - * `AdminScopeSchema` makes `businessUnit` the one REQUIRED key — the other five - * carry defaults — so `{ includeSubtree: true }` is refused wholesale. The facet + * `AdminScopeSchema` makes `businessUnit` the one REQUIRED key — the other live + * keys carry defaults — so `{ includeSubtree: true }` is refused wholesale. The facet * patched the draft key by key from every control, so flipping any switch * BEFORE naming a business unit wrote exactly that object, and the section's own * badge counted only `businessUnit` / `assignablePermissionSets` and so read @@ -39,7 +39,15 @@ type Draft = Record; * second interaction sees what the first one wrote. */ let latest: Draft = {}; -function Harness({ initial, sets }: { initial: Draft; sets: string[] }) { +function Harness({ + initial, + sets, + only, +}: { + initial: Draft; + sets: string[]; + only?: "admin"; +}) { const [draft, setDraft] = React.useState(initial); // In an effect, not during render: `react-hooks/globals` bans the latter, and // `userEvent` flushes effects before the assertion reads this back. @@ -53,6 +61,7 @@ function Harness({ initial, sets }: { initial: Draft; sets: string[] }) { writable allSetNames={sets} t={t} + only={only} /> ); } @@ -103,15 +112,9 @@ describe("PermissionAdvancedFacets · delegated admin scope (objectui#9464)", () expect(written).toBeUndefined(); }); - it("the three manage-* switches are inert for the same reason", async () => { + it("the manage-assignments switch is inert for the same reason", async () => { const user = await mountFacet({}); - for (const key of [ - "perm.admin.manageAssignments", - "perm.admin.manageBindings", - "perm.admin.authorEnvironmentSets", - ]) { - await user.click(switchFor(key)); - } + await user.click(switchFor("perm.admin.manageAssignments")); expect(latest.adminScope).toBeUndefined(); }); @@ -154,3 +157,68 @@ describe("PermissionAdvancedFacets · delegated admin scope (objectui#9464)", () expect(within(adminHeader()).queryByText("1")).toBeNull(); }); }); + +/** + * objectui#12406 — `manageBindings` and `authorEnvironmentSets` are retired + * tombstones in `@objectstack/spec` (objectstack#23488): the spec refuses + * either key whatever it holds, so the facet draws no switch for either and + * every write it can make is a scope the spec accepts. + * + * Driven through the facet as the Studio Access pillar mounts it (`only`), so + * the section is located by its own test id and every switch inside it is + * found by ROLE, not by caption: a retired switch drawn back under any caption + * is still a switch this sweep clicks. + */ +describe("PermissionAdvancedFacets · the retired delegated-admin keys (objectui#12406)", () => { + const RETIRED = ["manageBindings", "authorEnvironmentSets"] as const; + + async function mountAdmin(initial: Draft, sets: string[] = []) { + const user = userEvent.setup(); + latest = initial; + render(); + return { user, section: screen.getByTestId("perm-facet-admin") }; + } + + it("CONTROL — the installed spec refuses each retired key at its own path, false included", () => { + for (const key of RETIRED) { + for (const value of [true, false]) { + const verdict = AdminScopeSchema.safeParse({ businessUnit: "bu_sales", [key]: value }); + expect(verdict.success, `${key}: ${value}`).toBe(false); + expect(verdict.error?.issues.some((i) => i.path[0] === key)).toBe(true); + } + } + }); + + it("draws the manage-assignments switch and neither retired switch", async () => { + const { section } = await mountAdmin({ adminScope: { businessUnit: "bu_sales" } }); + const captions = within(section) + .getAllByRole("switch") + .map((s) => s.closest("label")?.textContent); + expect(captions).toEqual(["perm.admin.includeSubtree", "perm.admin.manageAssignments"]); + expect(within(section).queryByText("perm.admin.manageBindings")).toBeNull(); + expect(within(section).queryByText("perm.admin.authorEnvironmentSets")).toBeNull(); + }); + + it("flipping every switch it draws writes a scope the spec accepts, carrying neither retired key", async () => { + const { user, section } = await mountAdmin({ adminScope: { businessUnit: "bu_sales" } }, [ + "ps_support", + ]); + for (const control of within(section).getAllByRole("switch")) { + await user.click(control); + } + await user.click(within(section).getByRole("button", { name: "ps_support" })); + + const written = latest.adminScope as Record; + expect(RETIRED.filter((key) => key in written)).toEqual([]); + expect( + AdminScopeSchema.safeParse(written).success, + `the editor wrote ${JSON.stringify(written)}, which the spec refuses`, + ).toBe(true); + expect(written).toEqual({ + businessUnit: "bu_sales", + includeSubtree: true, + manageAssignments: true, + assignablePermissionSets: ["ps_support"], + }); + }); +}); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx b/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx index 249523948b..9636eb5846 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx @@ -46,8 +46,7 @@ import type { * of the framework spec this editor authors, checked against the spec schemas * rather than sampled from live data (objectstack#7130): RLS policies * `{name,object,operation,using,check,enabled}`; admin scope - * `{businessUnit,includeSubtree,manage*,authorEnvironmentSets, - * assignablePermissionSets[]}`. + * `{businessUnit,includeSubtree,manageAssignments,assignablePermissionSets[]}`. */ /** @@ -102,7 +101,7 @@ function stripRetiredRlsKeys(policy: RlsPolicy): RlsPolicy { /** * The delegated-admin scope AS THIS EDITOR HOLDS IT MID-EDIT. * - * The six keys are `@objectstack/spec/security`'s `AdminScope` — the AUTHORING + * The keys are `@objectstack/spec/security`'s `AdminScope` — the AUTHORING * side (`z.input`), which is what this editor writes — and they are TAKEN from * it rather than restated (objectui#7265). The header above already said these * shapes were "checked against the spec schemas"; a hand-written copy can only @@ -119,6 +118,17 @@ function stripRetiredRlsKeys(policy: RlsPolicy): RlsPolicy { * * ⚠️ Requiredness is the ONLY thing relaxed. A draft that reaches Save still has * to satisfy the server's `AdminScopeSchema`, which this type does not promise. + * + * `manageBindings` and `authorEnvironmentSets` are retired tombstones in the + * spec (objectstack#23488): still keys of the type, typed so that no value can + * be written to them, and refused at parse whatever they hold. This editor + * draws no control for either and writes each live key by name, so the + * tombstone's type is what refuses a write of either here. It strips neither + * from a loaded scope: the server serves stored and draft rows through the + * spec's stored-row conversion `permission-admin-scope-binding-authoring-removed`, + * which drops both before a body reaches this editor; the spec's retirement + * test pins that strip in its case "a STORED `permission` row carrying both + * keys replays clean through the rehydration seam" (objectui#12406). */ type AdminScope = Partial; @@ -386,8 +396,8 @@ export function PermissionAdvancedFacets({ * Delegated admin scope: the boundary has to exist before anything can be * scoped to it (objectui#9464). * - * `businessUnit` is the ONE key `AdminScopeSchema` requires — the other five - * carry defaults — so a scope written before one is named is an object the + * `businessUnit` is the ONE key `AdminScopeSchema` requires — the other live + * keys carry defaults — so a scope written before one is named is an object the * framework refuses WHOLESALE, and what it blocks is the author's next * whole-record Save, not just this section. Every DEPENDENT control therefore * writes through `setScopeDetail` instead of `setScope`, and carries the same @@ -683,22 +693,14 @@ export function PermissionAdvancedFacets({

)}
- {( - [ - ['manageAssignments', 'perm.admin.manageAssignments'], - ['manageBindings', 'perm.admin.manageBindings'], - ['authorEnvironmentSets', 'perm.admin.authorEnvironmentSets'], - ] as const - ).map(([key, label]) => ( - - ))} +
diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index 13f9492c98..fa6889ace7 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -2473,7 +2473,7 @@ const ENGINE_STRINGS_EN: Record = { 'perm.tabs.vis.default_off': 'Default off', 'perm.admin.title': 'Delegated Admin Scope', 'perm.admin.help': - 'Lets holders administer a business-unit subtree (assign users, manage bindings) without full admin.', + 'Lets holders administer a business-unit subtree (assign users) without full admin.', 'perm.admin.businessUnit': 'Business unit', // The dependent controls' disabled reason (objectui#9464): `businessUnit` is // the one key the framework's admin-scope shape requires, so a scope written @@ -2482,8 +2482,6 @@ const ENGINE_STRINGS_EN: Record = { 'Name a business unit first — a scope without one is refused when the record is saved.', 'perm.admin.includeSubtree': 'Include subtree', 'perm.admin.manageAssignments': 'Manage assignments', - 'perm.admin.manageBindings': 'Manage bindings', - 'perm.admin.authorEnvironmentSets': 'Author env sets', 'perm.admin.assignableSets': 'Assignable permission sets', 'perm.admin.noSets': 'No permission sets loaded.', 'perm.loading': 'Loading permission set {name}…', @@ -5856,15 +5854,13 @@ const ENGINE_STRINGS_ZH: Record = { 'perm.tabs.vis.default_on': '默认开启', 'perm.tabs.vis.default_off': '默认关闭', 'perm.admin.title': '委派管理范围', - 'perm.admin.help': '允许持有者管理某业务单元子树(分配用户、管理绑定),无需完全管理员权限。', + 'perm.admin.help': '允许持有者管理某业务单元子树(分配用户),无需完全管理员权限。', 'perm.admin.businessUnit': '业务单元', // objectui#9464 — 见 EN 表同键注释。 'perm.admin.businessUnitRequired': '请先填写业务单元 — 没有业务单元的范围在保存记录时会被拒绝。', 'perm.admin.includeSubtree': '包含子树', 'perm.admin.manageAssignments': '管理分配', - 'perm.admin.manageBindings': '管理绑定', - 'perm.admin.authorEnvironmentSets': '编辑环境权限集', 'perm.admin.assignableSets': '可分配权限集', 'perm.admin.noSets': '未加载权限集。', 'perm.loading': '加载权限集 {name}…',