From 3ec7f33c4c45ba0526324e865a1af789e6683925 Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Fri, 24 Jul 2026 12:12:54 +0200 Subject: [PATCH 1/2] feat(user): Implement user management components - Added new Vue components for user creation, editing, and listing, enhancing the user management interface. - Introduced `UserForm` for handling user data input, including validation and role management. - Implemented user detail view with options to edit and remove users, improving user interaction and management capabilities. - Enhanced user data mapping to include initials for better UI representation. This update significantly improves the user management experience, providing a comprehensive interface for managing user accounts effectively. --- .../{userCreate => user/create}/Provider.vue | 2 +- .../{userEdit => user/edit}/Provider.vue | 2 +- .../user/components/user/{ => item}/Form.vue | 13 +-------- .../components/user/{ => item}/Provider.vue | 25 ++--------------- .../{userList => user/list}/Provider.vue | 28 ++----------------- admin/slices/user/user/data/user.mapper.ts | 14 +++++++++- admin/slices/user/user/domain/user.types.ts | 4 +++ .../user/user/pages/users/[id]/index.vue | 2 +- admin/slices/user/user/pages/users/index.vue | 4 +-- admin/slices/user/user/stores/user.ts | 6 ---- 10 files changed, 28 insertions(+), 72 deletions(-) rename admin/slices/user/user/components/{userCreate => user/create}/Provider.vue (93%) rename admin/slices/user/user/components/{userEdit => user/edit}/Provider.vue (98%) rename admin/slices/user/user/components/user/{ => item}/Form.vue (93%) rename admin/slices/user/user/components/user/{ => item}/Provider.vue (84%) rename admin/slices/user/user/components/{userList => user/list}/Provider.vue (85%) diff --git a/admin/slices/user/user/components/userCreate/Provider.vue b/admin/slices/user/user/components/user/create/Provider.vue similarity index 93% rename from admin/slices/user/user/components/userCreate/Provider.vue rename to admin/slices/user/user/components/user/create/Provider.vue index 226afee0..6ffcbc59 100644 --- a/admin/slices/user/user/components/userCreate/Provider.vue +++ b/admin/slices/user/user/components/user/create/Provider.vue @@ -1,5 +1,5 @@ diff --git a/admin/slices/user/user/pages/users/index.vue b/admin/slices/user/user/pages/users/index.vue index 5d89c30f..e7e9eae3 100644 --- a/admin/slices/user/user/pages/users/index.vue +++ b/admin/slices/user/user/pages/users/index.vue @@ -1,5 +1,3 @@ diff --git a/admin/slices/user/user/stores/user.ts b/admin/slices/user/user/stores/user.ts index 730a020b..bbde843a 100644 --- a/admin/slices/user/user/stores/user.ts +++ b/admin/slices/user/user/stores/user.ts @@ -7,12 +7,6 @@ import type { UserService, } from '#user/domain'; -// Re-export the domain enums/types so consumers importing them from -// `#user/stores/user` (user Form/Provider, userList/Create/Edit) keep working. -// The enums are used as runtime values, so they're value re-exports. -export { ALL_USER_ROLES, UserRoleTypes, UserStatusTypes } from '#user/domain'; -export type { ICreateUserData, IUpdateUserData, IUserData } from '#user/domain'; - const getService = createServiceGetter('$userService'); export const useUserStore = defineStore('user', () => { From 6ed3449baef36b116591bb945123fcc254517e81 Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Fri, 24 Jul 2026 14:32:28 +0200 Subject: [PATCH 2/2] feat(user): Refactor user role management and enhance date components - Updated user role handling to a single role system, simplifying user management. - Introduced new components for displaying time ago and formatted date, improving UI consistency. - Added new utility functions for date formatting and integrated them into user components. - Updated API schemas and data mappers to reflect changes in user role structure. This update streamlines user role management and enhances the user interface for date handling, providing a more cohesive experience. --- .vscode/settings.json | 3 + admin/package.json | 2 +- .../slices/common/components/date/TimeAgo.vue | 25 +++++ .../slices/common/components/date/TimeAt.vue | 12 +++ admin/slices/common/utils/formatDate.ts | 4 + admin/slices/common/utils/index.ts | 1 + .../api/data/repositories/api/schemas.gen.ts | 35 ++++--- .../api/data/repositories/api/sdk.gen.ts | 14 +-- .../api/data/repositories/api/types.gen.ts | 19 ++-- .../components/ui/radio-group/RadioGroup.vue | 24 +++++ .../ui/radio-group/RadioGroupItem.vue | 31 +++++++ .../theme/components/ui/radio-group/index.ts | 2 + admin/slices/user/auth/data/auth.mapper.ts | 6 +- admin/slices/user/auth/domain/auth.types.ts | 2 +- admin/slices/user/auth/stores/auth.ts | 4 +- .../user/components/user/create/Provider.vue | 5 +- .../user/components/user/edit/Provider.vue | 23 ++--- .../user/user/components/user/item/Form.vue | 93 ++++++++----------- .../user/components/user/item/Provider.vue | 15 +-- .../user/components/user/list/Provider.vue | 27 +++--- admin/slices/user/user/data/user.gateway.ts | 8 +- admin/slices/user/user/data/user.mapper.ts | 19 ++-- admin/slices/user/user/domain/user.gateway.ts | 2 +- admin/slices/user/user/domain/user.service.ts | 4 +- admin/slices/user/user/domain/user.types.ts | 11 ++- admin/slices/user/user/pages.ts | 6 ++ admin/slices/user/user/stores/user.ts | 6 +- .../migration.sql | 12 +++ .../slices/setup/init/domain/init.service.ts | 6 +- .../slices/user/auth/domain/auth.service.ts | 16 +++- api/src/slices/user/auth/domain/auth.types.ts | 2 + .../slices/user/auth/guards/roles.guard.ts | 12 ++- api/src/slices/user/user/data/user.gateway.ts | 2 +- api/src/slices/user/user/data/user.mapper.ts | 25 +++-- api/src/slices/user/user/domain/user.types.ts | 36 ++++++- .../slices/user/user/dtos/createUser.dto.ts | 26 ++---- api/src/slices/user/user/dtos/index.ts | 2 +- .../slices/user/user/dtos/updateUser.dto.ts | 6 +- .../user/user/dtos/updateUserRole.dto.ts | 14 +++ .../user/user/dtos/updateUserRoles.dto.ts | 16 ---- api/src/slices/user/user/dtos/user.dto.ts | 5 +- api/src/slices/user/user/user.controller.ts | 37 ++++++-- api/src/slices/user/user/user.prisma | 2 +- .../workflow/data/argo-workflow.gateway.ts | 2 +- .../common/components/layout/Provider.vue | 5 +- .../api/data/repositories/api/schemas.gen.ts | 35 ++++--- .../api/data/repositories/api/sdk.gen.ts | 14 +-- .../api/data/repositories/api/types.gen.ts | 19 ++-- app/slices/user/auth/data/auth.mapper.ts | 17 ++-- app/slices/user/auth/domain/auth.types.ts | 2 +- app/slices/user/auth/stores/auth.ts | 6 +- bun.lock | 14 ++- 52 files changed, 456 insertions(+), 280 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 admin/slices/common/components/date/TimeAgo.vue create mode 100644 admin/slices/common/components/date/TimeAt.vue create mode 100644 admin/slices/common/utils/formatDate.ts create mode 100644 admin/slices/common/utils/index.ts create mode 100644 admin/slices/setup/theme/components/ui/radio-group/RadioGroup.vue create mode 100644 admin/slices/setup/theme/components/ui/radio-group/RadioGroupItem.vue create mode 100644 admin/slices/setup/theme/components/ui/radio-group/index.ts create mode 100644 admin/slices/user/user/pages.ts create mode 100644 api/prisma/migrations/20260724131635_user_single_role/migration.sql create mode 100644 api/src/slices/user/user/dtos/updateUserRole.dto.ts delete mode 100644 api/src/slices/user/user/dtos/updateUserRoles.dto.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..42e62935 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "prisma.pinToPrisma6": true +} \ No newline at end of file diff --git a/admin/package.json b/admin/package.json index b78bc01f..f9b2f036 100644 --- a/admin/package.json +++ b/admin/package.json @@ -21,7 +21,7 @@ "@tanstack/vue-table": "^8.21.3", "@unovis/ts": "^1.6.4", "@unovis/vue": "^1.6.4", - "@vueuse/core": "^14.2.1", + "@vueuse/core": "^14.3.0", "axios": "^1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/admin/slices/common/components/date/TimeAgo.vue b/admin/slices/common/components/date/TimeAgo.vue new file mode 100644 index 00000000..560c6745 --- /dev/null +++ b/admin/slices/common/components/date/TimeAgo.vue @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/admin/slices/common/components/date/TimeAt.vue b/admin/slices/common/components/date/TimeAt.vue new file mode 100644 index 00000000..9bfce14e --- /dev/null +++ b/admin/slices/common/components/date/TimeAt.vue @@ -0,0 +1,12 @@ + + + diff --git a/admin/slices/common/utils/formatDate.ts b/admin/slices/common/utils/formatDate.ts new file mode 100644 index 00000000..10694400 --- /dev/null +++ b/admin/slices/common/utils/formatDate.ts @@ -0,0 +1,4 @@ +export const formatDateTime = (date?: string) => { + if (!date) return ''; + return new Date(date).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' }); +}; \ No newline at end of file diff --git a/admin/slices/common/utils/index.ts b/admin/slices/common/utils/index.ts new file mode 100644 index 00000000..60c9bc22 --- /dev/null +++ b/admin/slices/common/utils/index.ts @@ -0,0 +1 @@ +export * from './formatDate'; \ No newline at end of file diff --git a/admin/slices/setup/api/data/repositories/api/schemas.gen.ts b/admin/slices/setup/api/data/repositories/api/schemas.gen.ts index ff51c746..76385a04 100644 --- a/admin/slices/setup/api/data/repositories/api/schemas.gen.ts +++ b/admin/slices/setup/api/data/repositories/api/schemas.gen.ts @@ -1552,6 +1552,11 @@ export const SetAgentChannelsDtoSchema = { required: ["channels"], } as const; +export const AssignableUserRoleTypesSchema = { + type: "string", + enum: ["Admin", "User"], +} as const; + export const CreateUserDtoSchema = { type: "object", properties: { @@ -1568,12 +1573,13 @@ export const CreateUserDtoSchema = { example: "strongPassword1", minLength: 8, }, - roles: { - type: "array", - example: ["User"], - items: { - $ref: "#/components/schemas/UserRoleTypes", - }, + role: { + example: "User", + allOf: [ + { + $ref: "#/components/schemas/AssignableUserRoleTypes", + }, + ], }, }, required: ["name", "email", "password"], @@ -1602,18 +1608,19 @@ export const UpdateUserDtoSchema = { }, } as const; -export const UpdateUserRolesDtoSchema = { +export const UpdateUserRoleDtoSchema = { type: "object", properties: { - roles: { - type: "array", - example: ["User"], - items: { - $ref: "#/components/schemas/UserRoleTypes", - }, + role: { + example: "User", + allOf: [ + { + $ref: "#/components/schemas/AssignableUserRoleTypes", + }, + ], }, }, - required: ["roles"], + required: ["role"], } as const; export const SaveTemplateFileDtoSchema = { diff --git a/admin/slices/setup/api/data/repositories/api/sdk.gen.ts b/admin/slices/setup/api/data/repositories/api/sdk.gen.ts index 66301824..0a0d8a01 100644 --- a/admin/slices/setup/api/data/repositories/api/sdk.gen.ts +++ b/admin/slices/setup/api/data/repositories/api/sdk.gen.ts @@ -156,7 +156,7 @@ import type { UserControllerRemoveData, UserControllerFindByIdData, UserControllerUpdateData, - UserControllerUpdateRolesData, + UserControllerUpdateRoleData, TemplateFileControllerListData, TemplateFileControllerReadData, TemplateFileControllerSaveData, @@ -2300,7 +2300,7 @@ export class UsersService { } /** - * Invite a new user + * Create a user (admin sets the password) */ public static userControllerCreate( options: Options, @@ -2352,7 +2352,7 @@ export class UsersService { } /** - * Update user (name, email, password, status). Use /roles to change roles. + * Update user (name, email, password, status). Use /role to change the role. */ public static userControllerUpdate( options: Options, @@ -2372,17 +2372,17 @@ export class UsersService { } /** - * Replace the user's roles. Owner only. + * Set the user's role. Owner only. */ - public static userControllerUpdateRoles( - options: Options, + public static userControllerUpdateRole( + options: Options, ) { return (options.client ?? _heyApiClient).put< unknown, unknown, ThrowOnError >({ - url: "/users/{id}/roles", + url: "/users/{id}/role", ...options, headers: { "Content-Type": "application/json", diff --git a/admin/slices/setup/api/data/repositories/api/types.gen.ts b/admin/slices/setup/api/data/repositories/api/types.gen.ts index e4d193e6..fefc72e0 100644 --- a/admin/slices/setup/api/data/repositories/api/types.gen.ts +++ b/admin/slices/setup/api/data/repositories/api/types.gen.ts @@ -718,11 +718,16 @@ export type SetAgentChannelsDto = { channels: Array; }; +export enum AssignableUserRoleTypes { + ADMIN = "Admin", + USER = "User", +} + export type CreateUserDto = { name: string; email: string; password: string; - roles?: Array; + role?: AssignableUserRoleTypes; }; export type UpdateUserDto = { @@ -732,8 +737,8 @@ export type UpdateUserDto = { status?: "active" | "invited" | "disabled"; }; -export type UpdateUserRolesDto = { - roles: Array; +export type UpdateUserRoleDto = { + role: AssignableUserRoleTypes; }; export type SaveTemplateFileDto = { @@ -2914,16 +2919,16 @@ export type UserControllerUpdateResponses = { 200: unknown; }; -export type UserControllerUpdateRolesData = { - body: UpdateUserRolesDto; +export type UserControllerUpdateRoleData = { + body: UpdateUserRoleDto; path: { id: string; }; query?: never; - url: "/users/{id}/roles"; + url: "/users/{id}/role"; }; -export type UserControllerUpdateRolesResponses = { +export type UserControllerUpdateRoleResponses = { 200: unknown; }; diff --git a/admin/slices/setup/theme/components/ui/radio-group/RadioGroup.vue b/admin/slices/setup/theme/components/ui/radio-group/RadioGroup.vue new file mode 100644 index 00000000..6f6c27a4 --- /dev/null +++ b/admin/slices/setup/theme/components/ui/radio-group/RadioGroup.vue @@ -0,0 +1,24 @@ + + + diff --git a/admin/slices/setup/theme/components/ui/radio-group/RadioGroupItem.vue b/admin/slices/setup/theme/components/ui/radio-group/RadioGroupItem.vue new file mode 100644 index 00000000..31544ecf --- /dev/null +++ b/admin/slices/setup/theme/components/ui/radio-group/RadioGroupItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/admin/slices/setup/theme/components/ui/radio-group/index.ts b/admin/slices/setup/theme/components/ui/radio-group/index.ts new file mode 100644 index 00000000..7cb0e3b1 --- /dev/null +++ b/admin/slices/setup/theme/components/ui/radio-group/index.ts @@ -0,0 +1,2 @@ +export { default as RadioGroup } from "./RadioGroup.vue" +export { default as RadioGroupItem } from "./RadioGroupItem.vue" diff --git a/admin/slices/user/auth/data/auth.mapper.ts b/admin/slices/user/auth/data/auth.mapper.ts index 974b847f..73a650d9 100644 --- a/admin/slices/user/auth/data/auth.mapper.ts +++ b/admin/slices/user/auth/data/auth.mapper.ts @@ -4,7 +4,7 @@ const EMPTY_USER: IAuthUser = { id: '', name: '', email: '', - roles: [], + role: '', status: '', }; @@ -22,9 +22,7 @@ export class AuthMapper { id: o.id, name: str(o.name), email: str(o.email), - roles: Array.isArray(o.roles) - ? o.roles.filter((r): r is string => typeof r === 'string') - : [], + role: str(o.role), status: str(o.status), }; } diff --git a/admin/slices/user/auth/domain/auth.types.ts b/admin/slices/user/auth/domain/auth.types.ts index 08046456..15b7146b 100644 --- a/admin/slices/user/auth/domain/auth.types.ts +++ b/admin/slices/user/auth/domain/auth.types.ts @@ -4,7 +4,7 @@ export interface IAuthUser { id: string; name: string; email: string; - roles: string[]; + role: string; status: string; } diff --git a/admin/slices/user/auth/stores/auth.ts b/admin/slices/user/auth/stores/auth.ts index 45477fe7..3b077772 100644 --- a/admin/slices/user/auth/stores/auth.ts +++ b/admin/slices/user/auth/stores/auth.ts @@ -22,9 +22,7 @@ export const useAuthStore = defineStore('auth', () => { const isAuthenticated = computed(() => !!accessToken.value); const hasAdminAccess = computed(() => - !!user.value?.roles?.some((role) => - (ADMIN_ROLES as readonly string[]).includes(role), - ), + (ADMIN_ROLES as readonly string[]).includes(user.value?.role ?? ''), ); function applyToken(token: string | null) { diff --git a/admin/slices/user/user/components/user/create/Provider.vue b/admin/slices/user/user/components/user/create/Provider.vue index 6ffcbc59..2d985c7d 100644 --- a/admin/slices/user/user/components/user/create/Provider.vue +++ b/admin/slices/user/user/components/user/create/Provider.vue @@ -27,13 +27,12 @@ function onCancel() {
-

Invite user

+

Create user

Grant access to this workspace.

- diff --git a/admin/slices/user/user/components/user/edit/Provider.vue b/admin/slices/user/user/components/user/edit/Provider.vue index db0d0e62..8de121cf 100644 --- a/admin/slices/user/user/components/user/edit/Provider.vue +++ b/admin/slices/user/user/components/user/edit/Provider.vue @@ -14,14 +14,9 @@ const { data: user, pending } = await useAsyncData( () => userStore.fetchById(props.id), ); -const submitting = ref(false); +const roleLocked = computed(() => user.value?.role === UserRoleTypes.Owner); -function rolesEqual(a: UserRoleTypes[], b: UserRoleTypes[]) { - if (a.length !== b.length) return false; - const sa = [...a].sort(); - const sb = [...b].sort(); - return sa.every((r, i) => r === sb[i]); -} +const submitting = ref(false); async function onSubmit(values: ICreateUserData) { if (!user.value) return; @@ -31,10 +26,10 @@ async function onSubmit(values: ICreateUserData) { if (values.email && values.email !== user.value.email) patch.email = values.email; if (values.password) patch.password = values.password; - const rolesChanged = !rolesEqual(values.roles, user.value.roles); + const roleChanged = !roleLocked.value && values.role !== user.value.role; const profileChanged = Object.keys(patch).length > 0; - if (!rolesChanged && !profileChanged) { + if (!roleChanged && !profileChanged) { await navigateTo(`/users/${props.id}`); return; } @@ -42,7 +37,7 @@ async function onSubmit(values: ICreateUserData) { submitting.value = true; try { if (profileChanged) await userStore.update(props.id, patch); - if (rolesChanged) await userStore.updateRoles(props.id, values.roles); + if (roleChanged) await userStore.updateRole(props.id, values.role); } finally { submitting.value = false; } @@ -68,18 +63,18 @@ function onCancel() {