You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit da6f0ce
Browse filesBrowse the repository at this point in the historyBrowse files
feat(studio): New similar on a read-only base's permission set, opening the copy where the clone door landed it (#28669)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb3QLmcF7caVjcoG4FiNkm5U)_Fixes#28573
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:Studio「访问」支柱里,只读基线包(如 HotCRM)的权限集多了一个「新建类似」按钮。它打开 Setup
已有的复制对话框,只提交机器名和显示名给权限集复制接口;服务端答复副本落在哪个包(通常是 `hotcrm.custom`),Studio
就打开那个包的访问支柱并选中副本。Setup 目录页的按钮文字也从「克隆后自定义」改为「新建类似」。
- **为什么改**:ADR-0146 D6 规定客户自己的权限集是基线旁的新集合,放在 `.custom` 包里。服务端已由
#28563(PR #28636)把复制目标改到 `.custom`,这张卡(N-20b)补上 Studio 侧的入口。以前 Studio
从不提供复制,只有 Setup 目录页有。
- **风险与回滚**:没有新的写入路径,只调用已有的复制接口。答复没写包名或副本名时什么都不打开,只提示。墙隔离姿态或缺
`manage_metadata` 时按钮置灰并给出原因。访问支柱现在按包重新挂载(与其他支柱一致)。回滚即 revert 本 PR。
- **先看**:`studio-design/StudioDesignSurface.tsx` 的
`AccessPillar`(`onNewSimilarSaved`
和按钮的禁用判断)、`metadata-admin/permission-set-clone.ts`(读答复里的
`package`)、`i18n/src/namespaces/studio/{en,zh}.ts`(文案)。
- **两分钟验证**:`cd ui && pnpm exec vitest run
packages/app-shell/src/views/studio-design/StudioDesignSurface.accessNewSimilar-28573.test.tsx`,预期
9 个用例全绿。
- **需要你做的**:无,席位落地。
## What changes
- **New similar in Studio's Access pillar.** On a read-only package, the
open permission set offers "New similar" in its header. It opens the
existing `PermissionSetCloneDialog`, which posts exactly `{ name, label
}` to `POST /api/v1/security/permission-sets/:name/clone`. The client
sends no package and no facet.
- **The copy opens where the answer says it landed.**
`clonePermissionSet` now reads the answer's `package` (the shape #28636
added: `{ permissionSet, package, notice }`), as sent: a string, `null`,
or absent. With a package, the pillar navigates to
`/studio/<package>/access?surface=permission:<copy>`, and the server's
notice rides the navigation state and shows on the copy
(`access-new-similar-strip`). With no package (absent or `null`) or no
copy name, the strip says so and nothing opens. The landing is never
derived from the base.
- **Refusals and gates.** Refusals are the dialog's own (shown verbatim,
unchanged). Under a walled posture (`useWalledCatalogReadOnly`) the
button is disabled with `engine.catalog.readOnly.walled`; without
`manage_metadata` (`useCanAuthorMetadata`) with
`engine.edit.capabilityReadOnly`. The sentence is the button's
accessible description and its title.
- **Access pillar keyed by package.** `<AccessPillar key={packageId}>`,
as the Data, Automations and Interfaces pillars already are. Without it,
the hop to the copy's package kept the base's open set, rail and
`?surface=` capture, because the route keeps the surface mounted.
- **One label.** `perm.clone.action` now reads "New similar" (zh 新建类似),
on Setup's catalog too. The guidance strings that named the old label
(`perm.readOnly.artifact.hint`, `perm.clone.hint`,
`perm.clone.guidance`, `perm.save.locked`, `perm.clone.dialogTitle`) now
name the new one. They also stop calling the copy "an ordinary
permission set authored in this system": since #28636 the server saves a
base's copy in the package that holds your changes. Setup's `../COPY`
hop is unchanged.
- **Strings.** Three new keys,
`engine.studio.access.newSimilar.{noPackage,done,dismiss}`, all in the
studio namespace (`ui/packages/i18n/src/namespaces/studio/{en,zh}.ts`,
typed `Record<string, string>`). No `locales/*` pack carries this
family, so the change is `Clause-②: no` (the seat amended the claim,
6557419813).
## Tests
- New:
`studio-design/StudioDesignSurface.accessNewSimilar-28573.test.tsx`, 9
cases. It covers the en label with no override gesture on the base, the
exact `{ name, label }` body, and the copy opening in `hotcrm.custom`'s
Access pillar, selected, with the done line and the notice. It also
covers a package that is absent or `null` (opens nothing, says so), no
copy name, a 409 verbatim, walled disabled, capability disabled, and a
writable package's set offering no New similar. The real `AccessPillar`,
`PermissionMatrixEditPage` and `PermissionSetCloneDialog` are routed
under `/studio/:packageId/access` and keyed by package, as
`StudioDesignSurface` mounts them.
- Updated: `PermissionMatrixEditor.cloneToCustomize.test.tsx` and
`packagedItemWriteGate-12169.test.tsx` read "New similar" / 新建类似 for
Setup's button and guidance.
- Ablation, run once: `if (typeof copy.package !== 'string') {` was
replaced by `if (false as boolean) {` through
`scripts/ablation-replace.mjs`. The anchor went 1 → 0, and the absent
and `null` package cases turned red (2 failed | 7 passed). Restore
showed the blob equal to HEAD and `git diff HEAD` empty.
## Acceptance notes
- The keyed remount is pinned through the test's own route wrapper,
which mirrors `StudioDesignSurface`'s mount. The surface itself is not
rendered in that suite.
- After a copy lands, the server's notice is shown on the copy's Access
pillar until dismissed or until another set is selected. It rides
`location.state`, read once at mount, and only when it names the
pillar's own package.
- Card item 3 says "all locale packs": the `perm.clone.*` family lives
only in the studio namespace, which has en and zh. Other locales fall
back to en, as for every studio string.
## Verification
<details><summary>Builds and type-check (at d224186, after merging
origin/main dd6349e)</summary>
- `turbo run build --concurrency=2 --filter='@object-ui/app-shell^...'
--filter=@objectstack/formula --filter=@objectstack/sdui-parser
--filter=@objectstack/lint --filter=@objectstack/client`: 64/64 tasks,
lock VERDICT command-exit 0.
- `pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run
type-check`: both Done, VERDICT command-exit 0.
</details>
<details><summary>Tests</summary>
- Touched suites at d224186: 4 files, 75 tests passed (the new suite,
cloneToCustomize, packagedItemWriteGate-12169,
walledCatalogReadOnly-12183).
- Consumer sweep at the pre-merge head b5bb340: every app-shell test
file naming `AccessPillar`, `PermissionSetCloneDialog`,
`permission-set-clone`, `perm.clone` or `StudioDesignSurface`. That is
172 files in three locked batches (86 + 43 + 43): 451 + 227 + 447 = 1125
tests passed. The incoming main commits touched `PackagedAutomationPage`
and `packages/spec/src/api`, neither of which these files read. CI's
Test UI runs the rest of app-shell.
- `pnpm exec vitest run packages/i18n/`: 94 files, 1438 passed, 13
skipped.
</details>
<details><summary>Derived gates (dispatch-gates --commands --repo
objectstack-ai/objectstack, at d224186)</summary>
All 34 derived commands exit 0, reconciled with `dispatch-gates --ran`.
Among them: `node scripts/check-sdui-manifest.mjs` (exit 0, no registry
change), `pnpm --dir ui exec vitest run scripts/` (169 files, 4900
tests), `pnpm check:type-check-debt` (0 errors above record), `pnpm
check:query-options-erasure`, `pnpm check:nul-bytes`, and `pnpm
check:cross-package-test-inputs`. The PR-body gates
(`check-adr-0087-registration --base origin/main`,
`check-changeset-no-major --base origin/main`,
`check-closing-keyword-parity`) ran against this body through
`GITHUB_EVENT_PATH`.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No spec change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (Left to CI's Lint UI.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jUn8pWsnfJNsrCm68c)_
Co-authored-by: Claude <noreply@anthropic.com>
feat(studio): "New similar" on a read-only base's permission set in the Access pillar, opening the copy where the clone door landed it
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
A customer's own permission set is a new set in the package that holds their changes (ADR-0146 D6). Since the clone door lands a copy of a read-only package X's set in X's paired `X.custom`, Studio can offer that gesture where the base is designed.
12
+
13
+
-**New similar in Studio.** In the Access pillar of a read-only package, the open permission set offers "New similar". It asks for a machine name and a label (the same dialog as Setup's catalog) and posts exactly `{ name, label }` to `POST /security/permission-sets/:name/clone`. The client sends no package and no facet: the server decides where the copy lands.
14
+
-**The copy opens where the answer says.** The door's answer names the package the copy was saved in. The pillar opens that package's Access pillar with the copy selected (`/studio/<package>/access?surface=permission:<copy>`), and shows the server's notice there as sent. An answer that names no package (absent or `null`) or no copy says so, and opens nothing.
15
+
-**Refusals and gates.** A refusal (400, 403, 404, 409) is shown in the server's words in the dialog. Under a walled posture, or for a caller without `manage_metadata`, the action is drawn disabled with the console's own sentence for why. The base's sets stay read-only, and no override gesture is drawn on a permission set.
16
+
-**One label.** The action reads "New similar" (zh: 新建类似) on Setup's catalog too, and the guidance strings that name it say the server saves the copy in the package that holds your changes. Setup's post-clone hop is unchanged.
17
+
-**Access pillar keyed by package.** Like its sibling pillars, it remounts when the package changes, so opening the copy in another package's pillar no longer keeps the base's open set.
0 commit comments