From 04c7b1cdfc48ecd72e7a20d6f0709c29d17ca089 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:37:46 +0000 Subject: [PATCH 1/2] fix(spec): the permission-set form asks for the label first, and requires it The Identity section listed name (the API name) before an optional label, so an administrator typed a machine name first and could save a set with no human label while the Console derives name from the label. Label now comes first and is required; name follows, and its help says it derives from the label. The form's required flag is layout only: PermissionSetSchema still accepts a set with no label. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../permission-form-label-first.test.ts | 63 +++++++++++++++++++ packages/spec/src/security/permission.form.ts | 8 ++- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/spec/src/security/permission-form-label-first.test.ts diff --git a/packages/spec/src/security/permission-form-label-first.test.ts b/packages/spec/src/security/permission-form-label-first.test.ts new file mode 100644 index 00000000000..c49c3247337 --- /dev/null +++ b/packages/spec/src/security/permission-form-label-first.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #27864 — the New Permission Set form asks for the Label first, and requires it. + * + * The form opened on Name (the API name) with an optional Label below it, so an + * administrator typed a machine name first and could save a set with no human + * label, while the Console's create form derives `name` from the label. The + * Identity section now lists `label` first and required, with `name` after it. + * + * `required` on a form row is a layout flag the renderer reads: the set's + * schema still accepts a set with no label, which the last case pins so that a + * later move of the accept set is a deliberate edit, not a form side effect. + */ + +import { describe, expect, it } from 'vitest'; + +import { METADATA_FORM_REGISTRY } from '../system/metadata-form-registry'; +import { permissionForm } from './permission.form'; +import { PermissionSetSchema } from './permission.zod'; + +type Row = { field: string; required?: boolean; helpText?: string }; + +const sections = (permissionForm.sections ?? []) as Array<{ label?: string; fields?: Array }>; +const identity = sections.find((s) => s.label === 'Identity'); +const rows = (identity?.fields ?? []).map((f) => (typeof f === 'string' ? { field: f } : f)) as Row[]; +const row = (key: string) => rows.find((r) => r.field === key); + +describe('permission-set form — Label first and required, Name after it', () => { + it('is the form `/meta/types` serves for the permission kind', () => { + expect(METADATA_FORM_REGISTRY.permission).toBe(permissionForm); + }); + + it('opens on the Identity section, whose first two rows are label then name', () => { + expect(sections[0]?.label).toBe('Identity'); + expect(rows.slice(0, 2).map((r) => r.field)).toEqual(['label', 'name']); + }); + + it('lists label before name across the whole form', () => { + const order = sections.flatMap((s) => (s.fields ?? []).map((f) => (typeof f === 'string' ? f : f.field))); + expect(order.indexOf('label')).toBeGreaterThanOrEqual(0); + expect(order.indexOf('label')).toBeLessThan(order.indexOf('name')); + }); + + it('requires the label, and still requires the name', () => { + expect(row('label')?.required).toBe(true); + expect(row('name')?.required).toBe(true); + // CONTROL: a neighbouring optional row is not swept into `required`. + expect(row('description')?.required).toBeUndefined(); + }); + + it("the name's help says it comes from the label", () => { + expect(row('name')?.helpText).toMatch(/derived from the label/i); + expect(row('name')?.helpText).toMatch(/snake_case/); + }); + + it('moves no accept set: the schema still parses a set with no label', () => { + const r = PermissionSetSchema.safeParse({ name: 'crm_sales_rep', objects: {} }); + expect(r.success).toBe(true); + // CONTROL: the same parse with a label is also clean. + expect(PermissionSetSchema.safeParse({ name: 'crm_sales_rep', label: 'Sales Rep', objects: {} }).success).toBe(true); + }); +}); diff --git a/packages/spec/src/security/permission.form.ts b/packages/spec/src/security/permission.form.ts index e4d656de222..15f6545efa8 100644 --- a/packages/spec/src/security/permission.form.ts +++ b/packages/spec/src/security/permission.form.ts @@ -30,8 +30,12 @@ export const permissionForm = defineForm({ '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.', columns: 2, fields: [ - { field: 'name', required: true, colSpan: 1, helpText: 'Machine name (snake_case)' }, - { field: 'label', colSpan: 1, helpText: 'Display label for admins' }, + // Label first and required: an admin names the set in words, and the + // Console's create form fills `name` from the label until `name` is + // edited. `required` on a row is a form-layout flag: `PermissionSetSchema` + // still accepts a set with no label, so no API accept set moves. + { field: 'label', required: true, colSpan: 1, helpText: 'Display label for admins' }, + { field: 'name', required: true, colSpan: 1, helpText: 'Derived from the label when you create the set; snake_case' }, // #19331 — four declared keys had no control, so the only door to any of // them was the Source tab's free-text JSON. `description` is persisted on // sys_permission_set and shown in Setup; of the other three, `packageId` From 74184aba4b0a8f8744f2add0790390e402202f51 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:42:51 +0000 Subject: [PATCH 2/2] chore(platform-objects): regenerate the metadata-form bundles for the label-first permission form The permission form's label row now precedes name in every locale's bundle, the English name help is the new source text, and the zh-CN, ja-JP and es-ES name help is re-translated by hand to say the name derives from the label. Adds the changeset for both moved packages. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../27864-permission-set-form-label-first.md | 15 +++++++++++++++ .../translations/en.metadata-forms.generated.ts | 8 ++++---- .../es-ES.metadata-forms.generated.ts | 8 ++++---- .../ja-JP.metadata-forms.generated.ts | 8 ++++---- .../zh-CN.metadata-forms.generated.ts | 8 ++++---- 5 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 .changeset/27864-permission-set-form-label-first.md diff --git a/.changeset/27864-permission-set-form-label-first.md b/.changeset/27864-permission-set-form-label-first.md new file mode 100644 index 00000000000..3ca197ac60b --- /dev/null +++ b/.changeset/27864-permission-set-form-label-first.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': patch +'@objectstack/platform-objects': patch +--- + +fix(spec): the permission-set form asks for the label first, and requires it + +Clause-②: no +Metadata-protocol: none + +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. + +`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. + +`@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. diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index b62315a4bea..df7e31eddc1 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -2385,14 +2385,14 @@ export const enMetadataForms: NonNullable = { } }, fields: { - name: { - label: "Name", - helpText: "Machine name (snake_case)" - }, label: { label: "Label", helpText: "Display label for admins" }, + name: { + label: "Name", + helpText: "Derived from the label when you create the set; snake_case" + }, description: { label: "Description", helpText: "Human-readable description shown in Setup." diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index e11be0b8168..2b9c9a539b9 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -2385,14 +2385,14 @@ export const esESMetadataForms: NonNullable = } }, fields: { - name: { - label: "Nombre", - helpText: "Nombre de máquina (snake_case)" - }, label: { label: "Etiqueta", helpText: "Etiqueta mostrada para administradores" }, + name: { + label: "Nombre", + helpText: "Se deriva de la etiqueta al crear el conjunto; snake_case" + }, description: { label: "Descripción", helpText: "Descripción legible que se muestra en Setup." diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index bb1a2770392..0a06dd2c999 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -2385,14 +2385,14 @@ export const jaJPMetadataForms: NonNullable = } }, fields: { - name: { - label: "名前", - helpText: "マシン名(snake_case)" - }, label: { label: "表示名", helpText: "管理者向け表示ラベル" }, + name: { + label: "名前", + helpText: "作成時に表示名から自動生成されます。snake_case" + }, description: { label: "説明", helpText: "Setup に表示される説明文。" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index f82866361da..5cc857948e6 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -2385,14 +2385,14 @@ export const zhCNMetadataForms: NonNullable = } }, fields: { - name: { - label: "名称", - helpText: "唯一标识符(snake_case)" - }, label: { label: "显示名称", helpText: "面向管理员的显示标签" }, + name: { + label: "名称", + helpText: "创建权限集时由显示名称自动生成;snake_case" + }, description: { label: "描述", helpText: "在 Setup 中展示的可读描述。"