diff --git a/.changeset/sys-user-detail-header-actions.md b/.changeset/sys-user-detail-header-actions.md new file mode 100644 index 000000000..c2a2d46d8 --- /dev/null +++ b/.changeset/sys-user-detail-header-actions.md @@ -0,0 +1,5 @@ +--- +'@objectstack/platform-objects': patch +--- + +`sys_user` account-management actions (Ban/Unban, Unlock Account, Set Password, Set Platform Role, Impersonate) now also surface on the user record-detail header (`record_header`, overflowing into the ⋯ "More" menu), not just the Users list row menu — so a platform admin can manage an account from an open user record without navigating back to the list. diff --git a/packages/platform-objects/src/identity/sys-user.object.ts b/packages/platform-objects/src/identity/sys-user.object.ts index b9c74e09e..51c13c187 100644 --- a/packages/platform-objects/src/identity/sys-user.object.ts +++ b/packages/platform-objects/src/identity/sys-user.object.ts @@ -77,15 +77,17 @@ export const SysUser = ObjectSchema.create({ // These actions hit /api/v1/auth/admin/* endpoints that are only // wired when `auth.plugins.admin` is enabled. When the plugin is // disabled the actions still render (schema is static) but server - // returns 404. UI surfaces them under the row menu so platform - // admins can manage accounts without dropping to SQL or + // returns 404. UI surfaces them under the row menu AND the + // record-detail header (`record_header`, overflowing into the ⋯ + // "More" menu) so platform admins can manage an account from either + // the Users list or an open user record — without dropping to SQL or // a custom Setup wizard. { name: 'ban_user', label: 'Ban User', icon: 'ban', variant: 'danger', - locations: ['list_item'], + locations: ['list_item', 'record_header'], type: 'api', target: '/api/v1/auth/admin/ban-user', recordIdParam: 'userId', @@ -101,7 +103,7 @@ export const SysUser = ObjectSchema.create({ label: 'Unban User', icon: 'check-circle-2', variant: 'secondary', - locations: ['list_item'], + locations: ['list_item', 'record_header'], type: 'api', target: '/api/v1/auth/admin/unban-user', recordIdParam: 'userId', @@ -116,7 +118,7 @@ export const SysUser = ObjectSchema.create({ label: 'Unlock Account', icon: 'lock-open', variant: 'secondary', - locations: ['list_item'], + locations: ['list_item', 'record_header'], type: 'api', target: '/api/v1/auth/admin/unlock-user', recordIdParam: 'userId', @@ -185,7 +187,7 @@ export const SysUser = ObjectSchema.create({ label: 'Set Password', icon: 'key-round', variant: 'secondary', - locations: ['list_item'], + locations: ['list_item', 'record_header'], type: 'api', // #2766 V1 — same path as better-auth's stock route, but served by the // plugin-auth wrapper registered ahead of the catch-all: it accepts the @@ -228,7 +230,7 @@ export const SysUser = ObjectSchema.create({ label: 'Set Platform Role', icon: 'shield-check', variant: 'secondary', - locations: ['list_item'], + locations: ['list_item', 'record_header'], type: 'api', target: '/api/v1/auth/admin/set-role', recordIdParam: 'userId', @@ -243,7 +245,7 @@ export const SysUser = ObjectSchema.create({ label: 'Impersonate User', icon: 'user-cog', variant: 'secondary', - locations: ['list_item'], + locations: ['list_item', 'record_header'], type: 'api', target: '/api/v1/auth/admin/impersonate-user', recordIdParam: 'userId', diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index 5e7ffb6f5..763d3fdb8 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -190,6 +190,23 @@ describe('@objectstack/platform-objects', () => { 'dnsRecordValue', ]); }); + + it('SysUser admin actions surface on the record-detail header, not just the row menu', () => { + // An admin viewing an open user record must be able to run the same + // account-management actions available from the Users list row menu — + // otherwise they have to navigate back to the list. Each admin action is + // therefore declared on BOTH `list_item` and `record_header` (the detail + // header overflows extras into the ⋯ "More" menu). `record_header` is the + // only detail-surface location objectui consumes (it does NOT read + // `record_more`), so these must use `record_header` specifically. + const adminActions = ['ban_user', 'unban_user', 'unlock_user', 'set_user_password', 'set_user_role', 'impersonate_user']; + for (const name of adminActions) { + const a = (SysUser.actions ?? []).find((x) => x.name === name); + expect(a, `${name} action must exist`).toBeTruthy(); + expect(a?.locations, `${name} locations`).toContain('list_item'); + expect(a?.locations, `${name} must also surface on the detail header`).toContain('record_header'); + } + }); }); describe('SETUP_APP (ADR-0029 D7 shell)', () => {