Skip to content

Commit dc4755c

Browse files
fix(spec): the permission-set form asks for the label first, and requires it (#28152)
🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V Fixes #27864 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:新建权限集表单的 Identity 区改为先填「显示名称」(Label,必填),再填 API 名称 (Name),Name 的提示改为「由显示名称生成;snake_case」。 - 为什么改:原表单先问机器名、显示名称可留空,管理员要先敲一个 API 名,还能存出没有人类可读名称的权限集,和 Console 从 Label 自动生成 Name 的流程相反。 - 风险与回滚:只动表单布局与提示文字,PermissionSetSchema 与 Name 的生成逻辑都不变,API 仍接受无 Label 的权限集;回滚即 revert 本 PR。 - 先看:packages/spec/src/security/permission.form.ts(Identity 区前两行),packages/spec/src/security/permission-form-label-first.test.ts,packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts(permission.fields)。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/security/permission-form-label-first.test.ts`,预期 6 个用例全绿;或在 Console 新建权限集,首个输入框是带必填星号的 Label,输入后 Name 自动填好。 - 需要你做的:无,席位落地。 ## What changes - `packages/spec/src/security/permission.form.ts`: the Identity section lists `label` first with `required: true`, then `name` (still required). The `name` help now reads "Derived from the label when you create the set; snake_case". A code comment records that `required` on a form row is a layout flag and that `PermissionSetSchema` still accepts a set with no label. - `packages/spec/src/security/permission-form-label-first.test.ts` (new): pins that the form is the one `METADATA_FORM_REGISTRY.permission` serves, that the Identity section opens on `label` then `name`, that `label` precedes `name` across the whole form, that both rows are required (with a control that `description` is not), that the `name` help names the derivation, and that the schema still parses a set with no label. - `packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts`: regenerated with `node scripts/check-i18n-bundles.mjs --write --filter=platform-objects` (the `permission.fields` keys now come `label` first, and the English `name` help is the new source text). The zh-CN, ja-JP and es-ES `name` help leaves are re-translated by hand to say the same thing, since merge mode keeps a stale translated leaf as it is. - `.changeset/27864-permission-set-form-label-first.md`: `@objectstack/spec` patch and `@objectstack/platform-objects` patch, carrying the two declaration lines. ## Tests - New test, six cases, green on the branch; against the `origin/main` form (ablation: form restored from `549203d040`, test unchanged) it is red on four of six (order, whole-form order, required flag, help text), the two that stay green being the registry identity and the accept-set control. Form restored from `HEAD` and byte-compared afterwards. - `@objectstack/spec`: build, `check:generated` (exit 0), `typecheck` (exit 0), full `vitest run`: Test Files 844 passed (844), Tests 23964 passed, 1 todo. - `@objectstack/platform-objects`: full `vitest run`: Test Files 101 passed (101), Tests 1434 passed. - `node scripts/check-i18n-bundles.mjs`: every bundle set in sync, platform-objects 11 bundles. ## Acceptance notes - Measured house pattern: every other `*.form.ts` in `packages/spec/src` that carries both rows lists `name` before `label` (agent, skill, tool, flow, field, hook, object, position, email-template, action, app, dashboard, dataset, page, report, view), and none words its `name` help as derived from the label. This form follows the card's Wanted, not that pattern; whether the others should follow is not this card's scope. - The Console's create registration for `permission` (`ui/packages/app-shell/src/views/metadata-admin/anchors.ts`) already declares `createFields: ['label', 'name']` and the label-to-name slugify rule; the served form's section order is what put Name first on screen. No `ui/` change here. - `required` on a form row is read only by the renderer: `/meta/types` serves the form as layout beside the Zod-derived schema, and the save path validates with `PermissionSetSchema`, where `label` stays optional. No API accept set moves, which is why `Clause-②: no`. ## Verification Independent review: none run (quota); the seat reviews the diff (no contract-review face is expected unless `packages/spec/src` counts as a published schema here — the seat decides). --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8882857 commit dc4755c

7 files changed

Lines changed: 100 additions & 18 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
fix(spec): the permission-set form asks for the label first, and requires it
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The Identity section of `permissionForm` (the form `/meta/types` serves for the `permission` kind) listed `name`, the API name, before an optional `label`, so an administrator creating a permission set typed a machine name first and could save a set with no human label, while the Console's create form fills `name` from the label. The section now lists `label` first and required, then `name`, whose help says it is derived from the label when the set is created.
12+
13+
`required` on a form row is a layout flag the renderer reads: `PermissionSetSchema` is unchanged and still accepts a set with no label, and nothing changes in how `name` is derived.
14+
15+
`@objectstack/platform-objects` carries the regenerated metadata-form bundles: the `permission` form's `label` row now precedes `name` in every locale, the English `name` help is the new source text, and the zh-CN, ja-JP and es-ES `name` help is translated to match.

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,14 +2385,14 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
23852385
}
23862386
},
23872387
fields: {
2388-
name: {
2389-
label: "Name",
2390-
helpText: "Machine name (snake_case)"
2391-
},
23922388
label: {
23932389
label: "Label",
23942390
helpText: "Display label for admins"
23952391
},
2392+
name: {
2393+
label: "Name",
2394+
helpText: "Derived from the label when you create the set; snake_case"
2395+
},
23962396
description: {
23972397
label: "Description",
23982398
helpText: "Human-readable description shown in Setup."

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,14 +2385,14 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
23852385
}
23862386
},
23872387
fields: {
2388-
name: {
2389-
label: "Nombre",
2390-
helpText: "Nombre de máquina (snake_case)"
2391-
},
23922388
label: {
23932389
label: "Etiqueta",
23942390
helpText: "Etiqueta mostrada para administradores"
23952391
},
2392+
name: {
2393+
label: "Nombre",
2394+
helpText: "Se deriva de la etiqueta al crear el conjunto; snake_case"
2395+
},
23962396
description: {
23972397
label: "Descripción",
23982398
helpText: "Descripción legible que se muestra en Setup."

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,14 +2385,14 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
23852385
}
23862386
},
23872387
fields: {
2388-
name: {
2389-
label: "名前",
2390-
helpText: "マシン名(snake_case)"
2391-
},
23922388
label: {
23932389
label: "表示名",
23942390
helpText: "管理者向け表示ラベル"
23952391
},
2392+
name: {
2393+
label: "名前",
2394+
helpText: "作成時に表示名から自動生成されます。snake_case"
2395+
},
23962396
description: {
23972397
label: "説明",
23982398
helpText: "Setup に表示される説明文。"

‎packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,14 +2385,14 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
23852385
}
23862386
},
23872387
fields: {
2388-
name: {
2389-
label: "名称",
2390-
helpText: "唯一标识符(snake_case)"
2391-
},
23922388
label: {
23932389
label: "显示名称",
23942390
helpText: "面向管理员的显示标签"
23952391
},
2392+
name: {
2393+
label: "名称",
2394+
helpText: "创建权限集时由显示名称自动生成;snake_case"
2395+
},
23962396
description: {
23972397
label: "描述",
23982398
helpText: "在 Setup 中展示的可读描述。"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #27864 — the New Permission Set form asks for the Label first, and requires it.
5+
*
6+
* The form opened on Name (the API name) with an optional Label below it, so an
7+
* administrator typed a machine name first and could save a set with no human
8+
* label, while the Console's create form derives `name` from the label. The
9+
* Identity section now lists `label` first and required, with `name` after it.
10+
*
11+
* `required` on a form row is a layout flag the renderer reads: the set's
12+
* schema still accepts a set with no label, which the last case pins so that a
13+
* later move of the accept set is a deliberate edit, not a form side effect.
14+
*/
15+
16+
import { describe, expect, it } from 'vitest';
17+
18+
import { METADATA_FORM_REGISTRY } from '../system/metadata-form-registry';
19+
import { permissionForm } from './permission.form';
20+
import { PermissionSetSchema } from './permission.zod';
21+
22+
type Row = { field: string; required?: boolean; helpText?: string };
23+
24+
const sections = (permissionForm.sections ?? []) as Array<{ label?: string; fields?: Array<Row | string> }>;
25+
const identity = sections.find((s) => s.label === 'Identity');
26+
const rows = (identity?.fields ?? []).map((f) => (typeof f === 'string' ? { field: f } : f)) as Row[];
27+
const row = (key: string) => rows.find((r) => r.field === key);
28+
29+
describe('permission-set form — Label first and required, Name after it', () => {
30+
it('is the form `/meta/types` serves for the permission kind', () => {
31+
expect(METADATA_FORM_REGISTRY.permission).toBe(permissionForm);
32+
});
33+
34+
it('opens on the Identity section, whose first two rows are label then name', () => {
35+
expect(sections[0]?.label).toBe('Identity');
36+
expect(rows.slice(0, 2).map((r) => r.field)).toEqual(['label', 'name']);
37+
});
38+
39+
it('lists label before name across the whole form', () => {
40+
const order = sections.flatMap((s) => (s.fields ?? []).map((f) => (typeof f === 'string' ? f : f.field)));
41+
expect(order.indexOf('label')).toBeGreaterThanOrEqual(0);
42+
expect(order.indexOf('label')).toBeLessThan(order.indexOf('name'));
43+
});
44+
45+
it('requires the label, and still requires the name', () => {
46+
expect(row('label')?.required).toBe(true);
47+
expect(row('name')?.required).toBe(true);
48+
// CONTROL: a neighbouring optional row is not swept into `required`.
49+
expect(row('description')?.required).toBeUndefined();
50+
});
51+
52+
it("the name's help says it comes from the label", () => {
53+
expect(row('name')?.helpText).toMatch(/derived from the label/i);
54+
expect(row('name')?.helpText).toMatch(/snake_case/);
55+
});
56+
57+
it('moves no accept set: the schema still parses a set with no label', () => {
58+
const r = PermissionSetSchema.safeParse({ name: 'crm_sales_rep', objects: {} });
59+
expect(r.success).toBe(true);
60+
// CONTROL: the same parse with a label is also clean.
61+
expect(PermissionSetSchema.safeParse({ name: 'crm_sales_rep', label: 'Sales Rep', objects: {} }).success).toBe(true);
62+
});
63+
});

‎packages/spec/src/security/permission.form.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ export const permissionForm = defineForm({
3030
'Permission sets are the only capability container: a user gets the union of every set they hold, so sets only ever add access. Positions distribute sets to people.',
3131
columns: 2,
3232
fields: [
33-
{ field: 'name', required: true, colSpan: 1, helpText: 'Machine name (snake_case)' },
34-
{ field: 'label', colSpan: 1, helpText: 'Display label for admins' },
33+
// Label first and required: an admin names the set in words, and the
34+
// Console's create form fills `name` from the label until `name` is
35+
// edited. `required` on a row is a form-layout flag: `PermissionSetSchema`
36+
// still accepts a set with no label, so no API accept set moves.
37+
{ field: 'label', required: true, colSpan: 1, helpText: 'Display label for admins' },
38+
{ field: 'name', required: true, colSpan: 1, helpText: 'Derived from the label when you create the set; snake_case' },
3539
// #19331 — four declared keys had no control, so the only door to any of
3640
// them was the Source tab's free-text JSON. `description` is persisted on
3741
// sys_permission_set and shown in Setup; of the other three, `packageId`

0 commit comments

Comments
 (0)