diff --git a/.eslintrc.json b/.eslintrc.json index e3a1a4bd52..8bc6adebaa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,10 +43,8 @@ "import/no-unresolved": "off", "react/prop-types": "off", "react/react-in-jsx-scope": "off", - "jsx-a11y/click-events-have-key-events": "warn", "jsx-a11y/no-autofocus": "off", - "@typescript-eslint/no-unused-vars": [ "error", { diff --git a/apps/gitness/src/pages-v2/user-management/user-management-container.tsx b/apps/gitness/src/pages-v2/user-management/user-management-container.tsx index 29cdf39146..92f735ef87 100644 --- a/apps/gitness/src/pages-v2/user-management/user-management-container.tsx +++ b/apps/gitness/src/pages-v2/user-management/user-management-container.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { useQueryClient } from '@tanstack/react-query' @@ -9,65 +9,32 @@ import { useAdminUpdateUserMutation, useUpdateUserAdminMutation } from '@harnessio/code-service-client' -import { - AdminDialog, - CreateUserDialog, - DeleteUserDialog, - DialogLabels, - EditUserDialog, - ResetPasswordDialog, - UserManagementPage, - UsersProps -} from '@harnessio/ui/views' - -import { parseAsInteger, useQueryState } from '../../framework/hooks/useQueryState' +import { IDataHandlers, UserManagementPage } from '@harnessio/ui/views' + +import { useQueryState } from '../../framework/hooks/useQueryState' +import usePaginationQueryStateWithStore from '../../hooks/use-pagination-query-state-with-store' import { useTranslationStore } from '../../i18n/stores/i18n-store' -import { generateAlphaNumericHash } from '../pull-request/pull-request-utils' import { useAdminListUsersStore } from './stores/admin-list-store' +import { promisifyMutation } from './utils/promisify-mutation' export const UserManagementPageContainer = () => { - const [queryPage, setQueryPage] = useQueryState('page', parseAsInteger.withDefault(1)) - const { setUsers, setTotalPages, setPage, page, password, setUser, setPassword, setGeteneratePassword } = - useAdminListUsersStore() const queryClient = useQueryClient() - const [isDeleteUserDialogOpen, setDeleteUserDialogOpen] = useState(false) - const [isEditUserDialogOpen, setEditUserDialogOpen] = useState(false) - const [isAdminDialogOpen, setAdminDialogOpen] = useState(false) - const [isResetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false) - const [isCreateUserDialogOpen, setCreateUserDialogOpen] = useState(false) - - const handleDialogOpen = (user: UsersProps | null, dialogTypeLabel: string) => { - if (user) setUser(user) - - switch (dialogTypeLabel) { - case DialogLabels.DELETE_USER: - setDeleteUserDialogOpen(true) - break - case DialogLabels.EDIT_USER: - setEditUserDialogOpen(true) - break - case DialogLabels.TOGGLE_ADMIN: - setAdminDialogOpen(true) - break - case DialogLabels.RESET_PASSWORD: - setGeteneratePassword(false) - setPassword(generateAlphaNumericHash(10)) - setResetPasswordDialogOpen(true) - break - case DialogLabels.CREATE_USER: - setPassword(generateAlphaNumericHash(10)) - setCreateUserDialogOpen(true) - setGeteneratePassword(true) - break - default: - break - } - } + const { setUsers, setTotalPages, setPage, page, password } = useAdminListUsersStore() - const { data: { body: userData, headers } = {} } = useAdminListUsersQuery({ + const [query, setQuery] = useQueryState('query') + const { queryPage } = usePaginationQueryStateWithStore({ page, setPage }) + + const { + isFetching, + error, + data: { body: userData, headers } = {} + } = useAdminListUsersQuery({ queryParams: { - page: queryPage + page: queryPage, + // TODO: add search functionality by query parameter + //@ts-expect-error - query is not typed + query: query ?? '' } }) @@ -80,45 +47,41 @@ export const UserManagementPageContainer = () => { } }, [userData, setUsers, setTotalPages, headers]) - useEffect(() => { - setQueryPage(page) - }, [queryPage, page, setPage]) - - const { mutate: updateUser, isLoading: isUpdatingUser } = useAdminUpdateUserMutation( + const { + mutate: updateUser, + isLoading: isUpdatingUser, + error: updateUserError + } = useAdminUpdateUserMutation( {}, { onSuccess: () => { - setEditUserDialogOpen(false) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) - const { mutate: deleteUser, isLoading: isDeletingUser } = useAdminDeleteUserMutation( + const { + mutate: deleteUser, + isLoading: isDeletingUser, + error: deleteUserError + } = useAdminDeleteUserMutation( {}, { onSuccess: () => { - setDeleteUserDialogOpen(false) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) - const { mutate: updateUserAdmin, isLoading: isUpdatingUserAdmin } = useUpdateUserAdminMutation( + const { + mutate: updateUserAdmin, + isLoading: isUpdatingUserAdmin, + error: updateUserAdminError + } = useUpdateUserAdminMutation( {}, { onSuccess: () => { - setAdminDialogOpen(false) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) @@ -131,18 +94,13 @@ export const UserManagementPageContainer = () => { {}, { onSuccess: () => { - setCreateUserDialogOpen(false) - setResetPasswordDialogOpen(true) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) - const handleCreateUser = (data: { uid: string; email: string; display_name: string }) => { - createUser({ + const handleCreateUser: IDataHandlers['handleCreateUser'] = data => { + return promisifyMutation(createUser, { body: { uid: data.uid, email: data.email, @@ -152,8 +110,8 @@ export const UserManagementPageContainer = () => { }) } - const handleUpdateUser = (data: { email: string; displayName: string; userID: string }) => { - updateUser({ + const handleUpdateUser: IDataHandlers['handleUpdateUser'] = data => { + return promisifyMutation(updateUser, { user_uid: data.userID, body: { email: data.email, @@ -162,14 +120,14 @@ export const UserManagementPageContainer = () => { }) } - const handleDeleteUser = (userUid: string) => { - deleteUser({ + const handleDeleteUser: IDataHandlers['handleDeleteUser'] = userUid => { + return promisifyMutation(deleteUser, { user_uid: userUid }) } - const handleUpdateUserAdmin = (userUid: string, isAdmin: boolean) => { - updateUserAdmin({ + const handleUpdateUserAdmin: IDataHandlers['handleUpdateUserAdmin'] = (userUid, isAdmin) => { + return promisifyMutation(updateUserAdmin, { user_uid: userUid, body: { admin: isAdmin @@ -177,8 +135,8 @@ export const UserManagementPageContainer = () => { }) } - const handleUpdatePassword = (userId: string) => { - updateUser({ + const handleUpdatePassword: IDataHandlers['handleUpdatePassword'] = userId => { + return promisifyMutation(updateUser, { user_uid: userId, body: { password: password @@ -186,47 +144,40 @@ export const UserManagementPageContainer = () => { }) } + const handlers = { + handleUpdateUser, + handleDeleteUser, + handleUpdateUserAdmin, + handleUpdatePassword, + handleCreateUser + } + + const loadingStates = { + isFetchingUsers: isFetching, + isUpdatingUser, + isDeletingUser, + isUpdatingUserAdmin, + isCreatingUser + } + + const errorStates = { + fetchUsersError: error?.message?.toString() ?? '', + updateUserError: updateUserError?.message?.toString() ?? '', + deleteUserError: deleteUserError?.message?.toString() ?? '', + updateUserAdminError: updateUserAdminError?.message?.toString() ?? '', + createUserError: createUserError?.message?.toString() ?? '' + } + return ( <> <UserManagementPage useAdminListUsersStore={useAdminListUsersStore} useTranslationStore={useTranslationStore} - handleDialogOpen={handleDialogOpen} - /> - - <DeleteUserDialog - open={isDeleteUserDialogOpen} - useAdminListUsersStore={useAdminListUsersStore} - onClose={() => setDeleteUserDialogOpen(false)} - isDeleting={isDeletingUser} - handleDeleteUser={handleDeleteUser} - /> - <EditUserDialog - open={isEditUserDialogOpen} - useAdminListUsersStore={useAdminListUsersStore} - isSubmitting={isUpdatingUser} - onClose={() => setEditUserDialogOpen(false)} - handleUpdateUser={handleUpdateUser} - /> - <AdminDialog - open={isAdminDialogOpen} - useAdminListUsersStore={useAdminListUsersStore} - onClose={() => setAdminDialogOpen(false)} - isLoading={isUpdatingUserAdmin} - updateUserAdmin={handleUpdateUserAdmin} - /> - <ResetPasswordDialog - open={isResetPasswordDialogOpen} - useAdminListUsersStore={useAdminListUsersStore} - onClose={() => setResetPasswordDialogOpen(false)} - handleUpdatePassword={handleUpdatePassword} - /> - <CreateUserDialog - open={isCreateUserDialogOpen} - onClose={() => setCreateUserDialogOpen(false)} - isLoading={isCreatingUser} - apiError={createUserError?.message?.toString() ?? ''} - handleCreateUser={handleCreateUser} + handlers={handlers} + loadingStates={loadingStates} + errorStates={errorStates} + searchQuery={query} + setSearchQuery={setQuery} /> </> ) diff --git a/apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts b/apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts new file mode 100644 index 0000000000..74400f7464 --- /dev/null +++ b/apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts @@ -0,0 +1,10 @@ +export type MutationFn<T> = (params: T, options: { onSuccess: () => void; onError: (error: any) => void }) => void + +export const promisifyMutation = <T>(mutation: MutationFn<T>, params: T): Promise<void> => { + return new Promise<void>((resolve, reject) => { + mutation(params, { + onSuccess: () => resolve(), + onError: error => reject(error) + }) + }) +} diff --git a/packages/ui/locales/en/views.json b/packages/ui/locales/en/views.json index d2276fe731..ece0cf72d5 100644 --- a/packages/ui/locales/en/views.json +++ b/packages/ui/locales/en/views.json @@ -192,6 +192,8 @@ "createOrImportRepos": "Create new or import an existing repository.", "noWebhooks": "No webhooks yet", "noWebhooksDescription": "Add or manage webhooks to automate tasks and connect external services to your project.", + "noUsers": "No Users Found", + "noUsersDescription": "There are no users in this scope. Click on the button below to start adding them.", "commit": "Commit", "noLabels": "No labels yet", "noLabelsDescription": "Use labels to organize, prioritize, and categorize tasks efficiently." @@ -403,6 +405,71 @@ "edit": "Edit webhook", "delete": "Delete webhook" }, + "userManagement": { + "userId": "User ID", + "userIdHint": "User ID cannot be changed once created", + "enterUsername": "Enter user name", + "enterEmail": "Enter email address", + "email": "Email", + "enterDisplayName": "Enter display name", + "displayName": "Display Name", + "inviting": "Inviting...", + "inviteNewUser": "Invite New User", + "addNewUser": "Add a new user", + "deletingUser": "Deleting user...", + "confirmDelete": "Yes, delete user", + "deleteConfirmation": "Are you sure you want to delete {{name}}?", + "deleteWarning": "This will permanently delete the user \"{{name}}\" from the system.", + "updateUser": "Update User", + "removeAdminMessage": "This will remove the admin tag for \"{{name}}\"", + "grantAdminMessage": "This will grant admin privileges to \"{{name}}\"", + "removingAdmin": "Removing admin...", + "grantingAdmin": "Granting admin...", + "removeAdmin": "Yes, remove admin", + "grantAdmin": "Yes, grant admin", + "updateAdminRights": "Update admin rights", + "passwordGeneratedMessage": "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.", + "resetPasswordMessage": "A new password will be generated to assist {{name}} in resetting their current password.", + "resettingPassword": "Resetting Password...", + "resetPassword": "Reset password for {{name}}", + "newUserButton": "New user", + "searchPlaceholder": "Search", + "usersHeader": "Users", + "tabs": { + "active": "Active users", + "inactive": "Pending users" + }, + "usersList": { + "user": "User", + "email": "Email", + "roleBinding": "Role binding" + }, + "roles": { + "admin": "Admin", + "user": "User" + }, + "actions": { + "removeAdmin": "Remove Admin", + "setAsAdmin": "Set as Admin", + "resetPassword": "Reset Password", + "editUser": "Edit User", + "deleteUser": "Delete User" + }, + "forms": { + "userIdRequired": "Please provide a user ID", + "emailRequired": "Please enter a valid email address", + "displayNameRequired": "Please provide a display name", + "passwordGenerateSuccess": "Your password has been generated. Please make sure to copy and store your password somewhere safe.", + "saving": "Saving...", + "generating": "Generating...", + "close": "Close" + }, + "validation": { + "emailInvalid": "Please enter a valid email", + "displayNameRequired": "Display name is required", + "userIdRequired": "User ID is required" + } + }, "labelData": { "create": "Create labels" } diff --git a/packages/ui/locales/es/views.json b/packages/ui/locales/es/views.json index 834c2ba1ad..1d6ece9148 100644 --- a/packages/ui/locales/es/views.json +++ b/packages/ui/locales/es/views.json @@ -192,6 +192,8 @@ "createOrImportRepos": "Create new or import an existing repository.", "noWebhooks": "No webhooks yet", "noWebhooksDescription": "Add or manage webhooks to automate tasks and connect external services to your project.", + "noUsers": "No Users Found", + "noUsersDescription": "There are no users in this scope. Click on the button below to start adding them.", "commit": "Commit", "noLabels": "No labels yet", "noLabelsDescription": "Use labels to organize, prioritize, and categorize tasks efficiently." @@ -391,6 +393,41 @@ "edit": "Edit webhook", "delete": "Delete webhook" }, + "userManagement": { + "userId": "User ID", + "userIdHint": "User ID cannot be changed once created", + "enterUsername": "Enter user name", + "enterEmail": "Enter email address", + "email": "Email", + "enterDisplayName": "Enter display name", + "displayName": "Display Name", + "inviting": "Inviting...", + "inviteNewUser": "Invite New User", + "addNewUser": "Add a new user", + "deletingUser": "Deleting user...", + "confirmDelete": "Yes, delete user", + "deleteConfirmation": "Are you sure you want to delete {{name}}?", + "deleteWarning": "This will permanently delete the user \"{{name}}\" from the system.", + "updateUser": "Update User", + "removeAdminMessage": "This will remove the admin tag for \"{{name}}\"", + "grantAdminMessage": "This will grant admin privileges to \"{{name}}\"", + "removingAdmin": "Removing admin...", + "grantingAdmin": "Granting admin...", + "removeAdmin": "Yes, remove admin", + "grantAdmin": "Yes, grant admin", + "updateAdminRights": "Update admin rights", + "passwordGeneratedMessage": "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.", + "resetPasswordMessage": "A new password will be generated to assist {{name}} in resetting their current password.", + "resettingPassword": "Resetting Password...", + "resetPassword": "Reset password for {{name}}", + "newUserButton": "New user", + "searchPlaceholder": "Search", + "usersHeader": "Users", + "tabs": { + "active": "Active users", + "inactive": "Pending users" + } + }, "labelData": { "create": "Create labels" }, diff --git a/packages/ui/locales/fr/views.json b/packages/ui/locales/fr/views.json index 59ec7298ad..9cb787d648 100644 --- a/packages/ui/locales/fr/views.json +++ b/packages/ui/locales/fr/views.json @@ -192,6 +192,8 @@ "createOrImportRepos": "Créer un nouveau dépôt ou importer un dépôt existant.", "noWebhooks": "No webhooks yet", "noWebhooksDescription": "Add or manage webhooks to automate tasks and connect external services to your project.", + "noUsers": "No Users Found", + "noUsersDescription": "There are no users in this scope. Click on the button below to start adding them.", "commit": "Validation", "noLabels": "No labels yet", "noLabelsDescription": "Use labels to organize, prioritize, and categorize tasks efficiently." @@ -397,6 +399,41 @@ "edit": "Edit webhook", "delete": "Delete webhook" }, + "userManagement": { + "userId": "User ID", + "userIdHint": "User ID cannot be changed once created", + "enterUsername": "Enter user name", + "enterEmail": "Enter email address", + "email": "Email", + "enterDisplayName": "Enter display name", + "displayName": "Display Name", + "inviting": "Inviting...", + "inviteNewUser": "Invite New User", + "addNewUser": "Add a new user", + "deletingUser": "Deleting user...", + "confirmDelete": "Yes, delete user", + "deleteConfirmation": "Are you sure you want to delete {{name}}?", + "deleteWarning": "This will permanently delete the user \"{{name}}\" from the system.", + "updateUser": "Update User", + "removeAdminMessage": "This will remove the admin tag for \"{{name}}\"", + "grantAdminMessage": "This will grant admin privileges to \"{{name}}\"", + "removingAdmin": "Removing admin...", + "grantingAdmin": "Granting admin...", + "removeAdmin": "Yes, remove admin", + "grantAdmin": "Yes, grant admin", + "updateAdminRights": "Update admin rights", + "passwordGeneratedMessage": "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.", + "resetPasswordMessage": "A new password will be generated to assist {{name}} in resetting their current password.", + "resettingPassword": "Resetting Password...", + "resetPassword": "Reset password for {{name}}", + "newUserButton": "New user", + "searchPlaceholder": "Search", + "usersHeader": "Users", + "tabs": { + "active": "Active users", + "inactive": "Pending users" + } + }, "labelData": { "create": "Create labels" } diff --git a/packages/ui/src/views/user-management/components/admin-dialog.tsx b/packages/ui/src/views/user-management/components/admin-dialog.tsx deleted file mode 100644 index 998ff3b0de..0000000000 --- a/packages/ui/src/views/user-management/components/admin-dialog.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { AlertDialog, Button } from '@/components' - -import { IRemoveAdminDialogProps } from '../types' - -// Form Remove/Add Admin Dialog -export const AdminDialog: React.FC<IRemoveAdminDialogProps> = ({ - useAdminListUsersStore, - open, - onClose, - isLoading, - updateUserAdmin -}) => { - const { user } = useAdminListUsersStore() - const isAdmin = user?.admin ?? false - - return ( - <AlertDialog.Root open={open} onOpenChange={onClose}> - <AlertDialog.Trigger asChild></AlertDialog.Trigger> - <AlertDialog.Content> - <AlertDialog.Header> - <AlertDialog.Title> - {isAdmin ? `Are you sure you want to remove ` : `Are you sure you want to grant `} - {user?.display_name} - {isAdmin ? ` as an admin?` : ` admin privileges?`} - </AlertDialog.Title> - <AlertDialog.Description> - {isAdmin - ? `This will remove the admin tag for "${user?.display_name}".` - : `This will grant admin privileges to "${user?.display_name}".`} - </AlertDialog.Description> - </AlertDialog.Header> - <AlertDialog.Footer> - <Button variant="outline" onClick={onClose}> - Cancel - </Button> - - <Button - size="default" - theme={isAdmin ? 'error' : 'primary'} - className="self-start" - onClick={() => { - updateUserAdmin(user?.uid ?? '', !isAdmin) - }} - > - {isLoading - ? isAdmin - ? 'Removing admin...' - : 'Granting admin...' - : isAdmin - ? 'Yes, remove admin' - : 'Yes, grant admin'} - </Button> - </AlertDialog.Footer> - </AlertDialog.Content> - </AlertDialog.Root> - ) -} diff --git a/packages/ui/src/views/user-management/components/create-user-dialog.tsx b/packages/ui/src/views/user-management/components/create-user-dialog.tsx deleted file mode 100644 index c7ab20f898..0000000000 --- a/packages/ui/src/views/user-management/components/create-user-dialog.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { SubmitHandler, useForm } from 'react-hook-form' - -import { - AlertDialog, - Button, - ButtonGroup, - ControlGroup, - Fieldset, - FormWrapper, - Icon, - Input, - Label, - Spacer, - Text -} from '@/components' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' - -const newUserSchema = z.object({ - uid: z.string().min(1, { message: 'Please provide a user ID' }), - email: z.string().email({ message: 'Please enter a valid email address' }), - display_name: z.string().min(1, { message: 'Please provide a display name' }) -}) - -export type NewUserFields = z.infer<typeof newUserSchema> - -export function CreateUserDialog({ - handleCreateUser, - isLoading, - apiError, - open, - onClose -}: { - handleCreateUser: (data: NewUserFields) => void - isLoading: boolean - apiError: string | null - open: boolean - onClose: () => void -}) { - const { - register, - handleSubmit, - formState: { errors } - } = useForm<NewUserFields>({ - resolver: zodResolver(newUserSchema), - mode: 'onChange', - defaultValues: { - uid: '', - email: '', - display_name: '' - } - }) - - const onSubmit: SubmitHandler<NewUserFields> = data => { - handleCreateUser(data) - } - - return ( - <AlertDialog.Root open={open} onOpenChange={onClose}> - <AlertDialog.Content> - <AlertDialog.Header> - <AlertDialog.Title>Add a new user</AlertDialog.Title> - </AlertDialog.Header> - <AlertDialog.Description asChild> - <FormWrapper onSubmit={handleSubmit(onSubmit)}> - <Fieldset> - {/* USER ID */} - <ControlGroup> - <span className="flex items-center"> - <Label htmlFor="memberName">User ID</Label> - <Icon name="info-circle" height={15} className="ml-3 text-tertiary-background" /> - <Text size={1} className="ml-1 text-tertiary-background"> - User ID cannot be changed once created - </Text> - </span> - <Spacer size={2} /> - - <Input - id="memberName" - {...register('uid')} - placeholder="Enter user name" - // label="User ID" - error={errors.uid?.message?.toString()} - /> - </ControlGroup> - - {/* EMAIL */} - <ControlGroup> - <Input - id="email" - {...register('email')} - placeholder="Enter email address" - label="Email" - error={errors.email?.message?.toString()} - /> - </ControlGroup> - - {/* ROLE */} - <ControlGroup> - <Input - id="displayName" - {...register('display_name')} - placeholder="Enter display name" - label="Display Name" - error={errors.display_name?.message?.toString()} - /> - </ControlGroup> - - {apiError && ( - <> - <Text size={1} className="text-destructive"> - {apiError?.toString()} - </Text> - </> - )} - {/* SAVE BUTTON */} - <AlertDialog.Footer> - <ControlGroup> - <ButtonGroup> - <> - <Button size="sm" type="submit" disabled={isLoading}> - {isLoading ? 'Inviting...' : 'Invite New User'} - </Button> - <Button - size="sm" - variant="outline" - type="button" - onClick={() => { - onClose() - }} - disabled={isLoading} - > - Cancel - </Button> - </> - </ButtonGroup> - </ControlGroup> - </AlertDialog.Footer> - </Fieldset> - </FormWrapper> - </AlertDialog.Description> - </AlertDialog.Content> - </AlertDialog.Root> - ) -} diff --git a/packages/ui/src/views/user-management/components/delete-user-dialog.tsx b/packages/ui/src/views/user-management/components/delete-user-dialog.tsx deleted file mode 100644 index 2db2b46c69..0000000000 --- a/packages/ui/src/views/user-management/components/delete-user-dialog.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { AlertDialog, Button, Spacer } from '@/components' - -import { IDeleteDialogProps } from '../types' - -export const DeleteUserDialog: React.FC<IDeleteDialogProps> = ({ - useAdminListUsersStore, - onClose, - isDeleting, - handleDeleteUser, - open -}) => { - const { user } = useAdminListUsersStore() - return ( - <AlertDialog.Root open={open} onOpenChange={onClose}> - <AlertDialog.Trigger asChild></AlertDialog.Trigger> - <AlertDialog.Content> - <AlertDialog.Header> - <AlertDialog.Title>Are you sure you want to delete {user?.display_name}?</AlertDialog.Title> - <AlertDialog.Description> - This will permanently delete the user "{user?.display_name}" from the system. - </AlertDialog.Description> - </AlertDialog.Header> - <Spacer size={3} /> - <AlertDialog.Footer> - {!isDeleting && ( - <Button variant="outline" onClick={onClose}> - Cancel - </Button> - )} - - <Button - size="default" - theme="error" - className="self-start" - onClick={() => { - handleDeleteUser(user!.uid ?? '') - }} - disabled={isDeleting} - > - {isDeleting ? 'Deleting user...' : 'Yes, delete user'} - </Button> - </AlertDialog.Footer> - </AlertDialog.Content> - </AlertDialog.Root> - ) -} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user-form.tsx b/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user-form.tsx new file mode 100644 index 0000000000..fcd4a87f63 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user-form.tsx @@ -0,0 +1,98 @@ +import { SubmitHandler, useForm } from 'react-hook-form' + +import { Button, ButtonGroup, ControlGroup, Fieldset, FormWrapper, Icon, Input, Label, Spacer } from '@/components' +import { zodResolver } from '@hookform/resolvers/zod' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { newUserSchema } from './schemas' +import { ICreateUserFormProps, NewUserFields } from './types' + +export function CreateUserForm({ handleCreateUser, onClose }: ICreateUserFormProps) { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { loadingStates, errorStates } = useStates() + + const { isCreatingUser } = loadingStates + const { createUserError } = errorStates + + const { + register, + handleSubmit, + formState: { errors } + } = useForm<NewUserFields>({ + resolver: zodResolver(newUserSchema), + mode: 'onChange', + defaultValues: { + uid: '', + email: '', + display_name: '' + } + }) + + const onSubmit: SubmitHandler<NewUserFields> = data => { + handleCreateUser(data) + } + + return ( + <FormWrapper onSubmit={handleSubmit(onSubmit)}> + <Fieldset> + {/* USER ID */} + <ControlGroup> + <span className="flex items-center"> + <Label htmlFor="memberName">{t('views:userManagement.userId', 'User ID')}</Label> + <Icon name="info-circle" height={15} className="ml-3 text-tertiary-background" /> + <span className="ml-1 text-xs"> + {t('views:userManagement.userIdHint', 'User ID cannot be changed once created')} + </span> + </span> + <Spacer size={2} /> + + <Input + id="memberName" + {...register('uid')} + placeholder={t('views:userManagement.enterUsername', 'Enter user name')} + error={errors.uid?.message?.toString()} + /> + </ControlGroup> + + {/* EMAIL */} + <ControlGroup> + <Input + id="email" + {...register('email')} + placeholder={t('views:userManagement.enterEmail', 'Enter email address')} + label={t('views:userManagement.email', 'Email')} + error={errors.email?.message?.toString()} + /> + </ControlGroup> + + {/* DISPLAY NAME */} + <ControlGroup> + <Input + id="displayName" + {...register('display_name')} + placeholder={t('views:userManagement.enterDisplayName', 'Enter display name')} + label={t('views:userManagement.displayName', 'Display Name')} + error={errors.display_name?.message?.toString()} + /> + </ControlGroup> + + {createUserError && <span className="text-xs text-destructive">{createUserError}</span>} + + <ButtonGroup className="justify-end"> + <Button size="sm" variant="outline" type="button" onClick={onClose} disabled={isCreatingUser}> + {t('common:cancel', 'Cancel')} + </Button> + <Button size="sm" type="submit" disabled={isCreatingUser}> + {isCreatingUser + ? t('views:userManagement.inviting', 'Inviting...') + : t('views:userManagement.inviteNewUser', 'Invite New User')} + </Button> + </ButtonGroup> + </Fieldset> + </FormWrapper> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx b/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx new file mode 100644 index 0000000000..9c9cc036df --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx @@ -0,0 +1,22 @@ +import { Dialog } from '@/components' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { CreateUserForm } from './create-user-form' +import { ICreateUserDialogProps } from './types' + +export function CreateUserDialog({ handleCreateUser, open, onClose }: ICreateUserDialogProps) { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + return ( + <Dialog.Root open={open} onOpenChange={onClose}> + <Dialog.Content className="w-[576px] max-w-[576px]"> + <Dialog.Header> + <Dialog.Title>{t('views:userManagement.addNewUser', 'Add a new user')}</Dialog.Title> + </Dialog.Header> + <CreateUserForm handleCreateUser={handleCreateUser} onClose={onClose} /> + </Dialog.Content> + </Dialog.Root> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts new file mode 100644 index 0000000000..4b38bf2520 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts @@ -0,0 +1,2 @@ +export { CreateUserDialog } from './create-user' +export type { ICreateUserDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts b/packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts new file mode 100644 index 0000000000..067878d3b7 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const newUserSchema = z.object({ + uid: z.string().min(1, { message: 'Please provide a user ID' }), + email: z.string().email({ message: 'Please enter a valid email address' }), + display_name: z.string().min(1, { message: 'Please provide a display name' }) +}) diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts new file mode 100644 index 0000000000..fd889feae1 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts @@ -0,0 +1,15 @@ +import { newUserSchema } from '@views/user-management/components/dialogs/components/create-user/schemas' +import { z } from 'zod' + +export type NewUserFields = z.infer<typeof newUserSchema> + +export interface ICreateUserDialogProps { + handleCreateUser: (data: NewUserFields) => void + open: boolean + onClose: () => void +} + +export interface ICreateUserFormProps { + handleCreateUser: (data: NewUserFields) => void + onClose: () => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user-form.tsx b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user-form.tsx new file mode 100644 index 0000000000..54f12a1655 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user-form.tsx @@ -0,0 +1,43 @@ +import { Button, ButtonGroup } from '@/components' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { IDeleteUserFormProps } from './types' + +export function DeleteUserForm({ handleDeleteUser, onClose }: IDeleteUserFormProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + + const { isDeletingUser } = loadingStates + const { deleteUserError } = errorStates + + return ( + <> + {deleteUserError && <span className="text-xs text-destructive">{deleteUserError}</span>} + + <ButtonGroup className="justify-end"> + {!isDeletingUser && ( + <Button variant="outline" onClick={onClose}> + {t('common:cancel', 'Cancel')} + </Button> + )} + + <Button + size="default" + theme="error" + className="self-start" + onClick={() => handleDeleteUser(user!.uid ?? '')} + disabled={isDeletingUser} + > + {isDeletingUser + ? t('views:userManagement.deletingUser', 'Deleting user...') + : t('views:userManagement.confirmDelete', 'Yes, delete user')} + </Button> + </ButtonGroup> + </> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx new file mode 100644 index 0000000000..68d531ad7b --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx @@ -0,0 +1,36 @@ +import { Dialog } from '@/components' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { DeleteUserForm } from './delete-user-form' +import { IDeleteDialogProps } from './types' + +export function DeleteUserDialog({ onClose, handleDeleteUser, open }: IDeleteDialogProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + return ( + <Dialog.Root open={open} onOpenChange={onClose}> + <Dialog.Content className="w-[576px] max-w-[576px]"> + <Dialog.Header> + <Dialog.Title> + {t('views:userManagement.deleteConfirmation', 'Are you sure you want to delete {{name}}?', { + name: user?.display_name + })} + </Dialog.Title> + <Dialog.Description> + {t( + 'views:userManagement.deleteWarning', + 'This will permanently delete the user "{{name}}" from the system.', + { + name: user?.display_name + } + )} + </Dialog.Description> + </Dialog.Header> + <DeleteUserForm handleDeleteUser={handleDeleteUser} onClose={onClose} /> + </Dialog.Content> + </Dialog.Root> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts new file mode 100644 index 0000000000..a3fb5c9ae0 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts @@ -0,0 +1,2 @@ +export { DeleteUserDialog } from './delete-user' +export type { IDeleteDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts new file mode 100644 index 0000000000..8cf707c679 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts @@ -0,0 +1,10 @@ +export interface IDeleteDialogProps { + open: boolean + onClose: () => void + handleDeleteUser: (userUid: string) => void +} + +export interface IDeleteUserFormProps { + handleDeleteUser: (userUid: string) => void + onClose: () => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user-form.tsx b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user-form.tsx new file mode 100644 index 0000000000..a972b79586 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user-form.tsx @@ -0,0 +1,85 @@ +import { SubmitHandler, useForm } from 'react-hook-form' + +import { Button, ButtonGroup, ControlGroup, Fieldset, FormWrapper, Input } from '@/components' +import { zodResolver } from '@hookform/resolvers/zod' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { editUserSchema } from './schemas' +import { IEditUserFormProps, IEditUserFormType } from './types' + +export function EditUserForm({ handleUpdateUser, onClose }: IEditUserFormProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + + const { isUpdatingUser } = loadingStates + const { updateUserError } = errorStates + + const { + register, + handleSubmit, + formState: { errors } + } = useForm<IEditUserFormType>({ + resolver: zodResolver(editUserSchema), + mode: 'onChange', + defaultValues: { + userID: user?.uid || '', + email: user?.email || '', + displayName: user?.display_name || '' + } + }) + + const onSubmit: SubmitHandler<IEditUserFormType> = data => { + handleUpdateUser(data) + } + + return ( + <FormWrapper onSubmit={handleSubmit(onSubmit)}> + <Fieldset> + <ControlGroup> + <Input + id="userID" + {...register('userID')} + readOnly + className="cursor-not-allowed" + caption={t('views:userManagement.userIdHint', 'User ID cannot be changed once created')} + error={errors.userID?.message?.toString()} + /> + </ControlGroup> + + <ControlGroup> + <Input + id="email" + {...register('email')} + label={t('views:userManagement.email', 'Email')} + error={errors.email?.message?.toString()} + /> + </ControlGroup> + + <ControlGroup> + <Input + id="displayName" + {...register('displayName')} + label={t('views:userManagement.displayName', 'Display Name')} + error={errors.displayName?.message?.toString()} + /> + </ControlGroup> + + {updateUserError && <span className="text-xs text-destructive">{updateUserError}</span>} + + <ButtonGroup className="justify-end"> + <Button variant="outline" onClick={onClose} disabled={isUpdatingUser}> + {t('common:cancel', 'Cancel')} + </Button> + <Button type="submit" disabled={isUpdatingUser}> + {isUpdatingUser ? t('common:saving', 'Saving...') : t('common:save', 'Save')} + </Button> + </ButtonGroup> + </Fieldset> + </FormWrapper> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx new file mode 100644 index 0000000000..8f5283e5f2 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx @@ -0,0 +1,22 @@ +import { Dialog } from '@/components' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { EditUserForm } from './edit-user-form' +import { IEditUserDialogProps } from './types' + +export function EditUserDialog({ handleUpdateUser, open, onClose }: IEditUserDialogProps) { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + return ( + <Dialog.Root open={open} onOpenChange={onClose}> + <Dialog.Content className="w-[576px] max-w-[576px]"> + <Dialog.Header> + <Dialog.Title>{t('views:userManagement.updateUser', 'Update User')}</Dialog.Title> + </Dialog.Header> + <EditUserForm handleUpdateUser={handleUpdateUser} onClose={onClose} /> + </Dialog.Content> + </Dialog.Root> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts new file mode 100644 index 0000000000..52b9ce0467 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts @@ -0,0 +1,2 @@ +export { EditUserDialog } from './edit-user' +export type { IEditUserDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts new file mode 100644 index 0000000000..6a1edbe3de --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const editUserSchema = z.object({ + userID: z.string(), + email: z.string().email({ message: 'Please enter a valid email' }), + displayName: z.string().min(1, { message: 'Display name is required' }) +}) diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts new file mode 100644 index 0000000000..036682df5b --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +import { editUserSchema } from './schemas' + +export type IEditUserFormType = z.infer<typeof editUserSchema> + +export interface IEditUserFormProps { + handleUpdateUser: (data: IEditUserFormType) => void + onClose: () => void +} + +export interface IEditUserDialogProps { + open: boolean + onClose: () => void + handleUpdateUser: (data: IEditUserFormType) => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts new file mode 100644 index 0000000000..71a738e62e --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts @@ -0,0 +1,2 @@ +export { RemoveAdminDialog } from './remove-admin' +export type { IRemoveAdminDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin-form.tsx b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin-form.tsx new file mode 100644 index 0000000000..5013d79b34 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin-form.tsx @@ -0,0 +1,70 @@ +import { SubmitHandler, useForm } from 'react-hook-form' + +import { Button, ButtonGroup, ControlGroup, Fieldset, FormWrapper } from '@/components' +import { zodResolver } from '@hookform/resolvers/zod' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { removeAdminSchema } from './schemas' +import { IRemoveAdminFormProps, IRemoveAdminFormType } from './types' + +export function RemoveAdminForm({ handleUpdateUserAdmin, onClose }: IRemoveAdminFormProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + + const { isUpdatingUserAdmin } = loadingStates + const { updateUserAdminError } = errorStates + + const isAdmin = user?.admin ?? false + + const { handleSubmit } = useForm<IRemoveAdminFormType>({ + resolver: zodResolver(removeAdminSchema), + mode: 'onChange', + defaultValues: { + userId: user?.uid || '' + } + }) + + const onSubmit: SubmitHandler<IRemoveAdminFormType> = data => { + handleUpdateUserAdmin(data.userId, !isAdmin) + } + + return ( + <FormWrapper onSubmit={handleSubmit(onSubmit)}> + <Fieldset> + <ControlGroup> + <span> + {isAdmin + ? t('views:userManagement.removeAdminMessage', 'This will remove the admin tag for "{{name}}"', { + name: user?.display_name + }) + : t('views:userManagement.grantAdminMessage', 'This will grant admin privileges to "{{name}}"', { + name: user?.display_name + })} + </span> + </ControlGroup> + + {updateUserAdminError && <span className="text-xs text-destructive">{updateUserAdminError}</span>} + + <ButtonGroup className="justify-end"> + <Button variant="outline" onClick={onClose} disabled={isUpdatingUserAdmin}> + {t('common:cancel', 'Cancel')} + </Button> + <Button type="submit" theme={isAdmin ? 'error' : 'primary'} disabled={isUpdatingUserAdmin}> + {isUpdatingUserAdmin + ? isAdmin + ? t('views:userManagement.removingAdmin', 'Removing admin...') + : t('views:userManagement.grantingAdmin', 'Granting admin...') + : isAdmin + ? t('views:userManagement.removeAdmin', 'Yes, remove admin') + : t('views:userManagement.grantAdmin', 'Yes, grant admin')} + </Button> + </ButtonGroup> + </Fieldset> + </FormWrapper> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx new file mode 100644 index 0000000000..9bb4c4a4ce --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx @@ -0,0 +1,22 @@ +import { Dialog } from '@/components' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { RemoveAdminForm } from './remove-admin-form' +import { IRemoveAdminDialogProps } from './types' + +export function RemoveAdminDialog({ handleUpdateUserAdmin, open, onClose }: IRemoveAdminDialogProps) { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + return ( + <Dialog.Root open={open} onOpenChange={onClose}> + <Dialog.Content className="w-[576px] max-w-[576px]"> + <Dialog.Header> + <Dialog.Title>{t('views:userManagement.updateAdminRights', 'Update admin rights')}</Dialog.Title> + </Dialog.Header> + <RemoveAdminForm handleUpdateUserAdmin={handleUpdateUserAdmin} onClose={onClose} /> + </Dialog.Content> + </Dialog.Root> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/schemas.ts b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/schemas.ts new file mode 100644 index 0000000000..3e9aa5ed1a --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/schemas.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const removeAdminSchema = z.object({ + userId: z.string().min(1, { message: 'User ID is required' }) +}) diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts new file mode 100644 index 0000000000..0e322c6be3 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +import { removeAdminSchema } from './schemas' + +export type IRemoveAdminFormType = z.infer<typeof removeAdminSchema> + +export interface IRemoveAdminFormProps { + handleUpdateUserAdmin: (userId: string, isAdmin: boolean) => void + onClose: () => void +} + +export interface IRemoveAdminDialogProps { + open: boolean + onClose: () => void + handleUpdateUserAdmin: (userId: string, isAdmin: boolean) => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts new file mode 100644 index 0000000000..91436678e9 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts @@ -0,0 +1,2 @@ +export { ResetPasswordDialog } from './reset-password' +export type { IResetPasswordDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password-form.tsx b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password-form.tsx new file mode 100644 index 0000000000..933e0d6355 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password-form.tsx @@ -0,0 +1,78 @@ +import { SubmitHandler, useForm } from 'react-hook-form' + +import { Button, ButtonGroup, ControlGroup, CopyButton, Fieldset, FormWrapper, Input } from '@/components' +import { zodResolver } from '@hookform/resolvers/zod' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { resetPasswordSchema } from './schemas' +import { IResetPasswordFormProps, IResetPasswordFormType } from './types' + +export function ResetPasswordForm({ handleUpdatePassword, onClose }: IResetPasswordFormProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user, generatePassword, setGeteneratePassword, password } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + + const { isUpdatingUser } = loadingStates + const { updateUserError } = errorStates + + const { handleSubmit } = useForm<IResetPasswordFormType>({ + resolver: zodResolver(resetPasswordSchema), + mode: 'onChange', + defaultValues: { + userId: user?.uid || '' + } + }) + + const onSubmit: SubmitHandler<IResetPasswordFormType> = data => { + handleUpdatePassword(data.userId) + setGeteneratePassword(true) + } + + return ( + <FormWrapper onSubmit={handleSubmit(onSubmit)}> + <Fieldset> + <ControlGroup> + <span> + {generatePassword + ? t( + 'views:userManagement.passwordGeneratedMessage', + "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again." + ) + : t( + 'views:userManagement.resetPasswordMessage', + 'A new password will be generated to assist {name} in resetting their current password.', + { name: user?.display_name || '' } + )} + </span> + </ControlGroup> + + {generatePassword && ( + <ControlGroup> + <Input id="password" value={password ?? ''} readOnly rightElement={<CopyButton name={password ?? ''} />} /> + </ControlGroup> + )} + + {updateUserError && <span className="text-xs text-destructive">{updateUserError}</span>} + + <ButtonGroup className="justify-end"> + <Button variant="outline" onClick={onClose} disabled={isUpdatingUser}> + {generatePassword ? t('common:close', 'Close') : t('common:cancel', 'Cancel')} + </Button> + {!generatePassword && ( + <Button type="submit" disabled={isUpdatingUser}> + {isUpdatingUser + ? t('views:userManagement.resettingPassword', 'Resetting Password...') + : t('views:userManagement.resetPassword', 'Reset password for {name}', { + name: user?.display_name || '' + })} + </Button> + )} + </ButtonGroup> + </Fieldset> + </FormWrapper> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx new file mode 100644 index 0000000000..1f5b798c72 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx @@ -0,0 +1,27 @@ +import { Dialog } from '@/components' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { ResetPasswordForm } from './reset-password-form' +import { IResetPasswordDialogProps } from './types' + +export function ResetPasswordDialog({ handleUpdatePassword, open, onClose }: IResetPasswordDialogProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + return ( + <Dialog.Root open={open} onOpenChange={onClose}> + <Dialog.Content className="w-[576px] max-w-[576px]"> + <Dialog.Header> + <Dialog.Title> + {t('views:userManagement.resetPassword', 'Reset password for {name}', { + name: user?.display_name || '' + })} + </Dialog.Title> + </Dialog.Header> + <ResetPasswordForm handleUpdatePassword={handleUpdatePassword} onClose={onClose} /> + </Dialog.Content> + </Dialog.Root> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/schemas.ts b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/schemas.ts new file mode 100644 index 0000000000..559d4c70b3 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/schemas.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const resetPasswordSchema = z.object({ + userId: z.string().min(1, { message: 'User ID is required' }) +}) diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts new file mode 100644 index 0000000000..4ed6983209 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +import { resetPasswordSchema } from './schemas' + +export type IResetPasswordFormType = z.infer<typeof resetPasswordSchema> + +export interface IResetPasswordFormProps { + handleUpdatePassword: (userId: string) => void + onClose: () => void +} + +export interface IResetPasswordDialogProps { + open: boolean + onClose: () => void + handleUpdatePassword: (userId: string) => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/dialogs.tsx b/packages/ui/src/views/user-management/components/dialogs/dialogs.tsx new file mode 100644 index 0000000000..e7d88a74e2 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/dialogs.tsx @@ -0,0 +1,46 @@ +import { useDialogHandlers } from '@/views/user-management/components/dialogs/hooks' +import { DialogLabels, IDialogsProps } from '@/views/user-management/components/dialogs/types' +import { useDialogs } from '@views/user-management/providers/DialogsProvider' + +import { CreateUserDialog } from './components/create-user' +import { DeleteUserDialog } from './components/delete-user' +import { EditUserDialog } from './components/edit-user' +import { RemoveAdminDialog } from './components/remove-admin' +import { ResetPasswordDialog } from './components/reset-password' + +export const Dialogs = ({ handlers }: IDialogsProps) => { + const { dialogsOpenState, closeDialog } = useDialogs() + + const { handleDeleteUser, handleUpdateUser, handleUpdatePassword, handleUpdateUserAdmin, handleCreateUser } = + useDialogHandlers(handlers) + + return ( + <> + <DeleteUserDialog + open={dialogsOpenState[DialogLabels.DELETE_USER]} + onClose={() => closeDialog(DialogLabels.DELETE_USER)} + handleDeleteUser={handleDeleteUser} + /> + <EditUserDialog + open={dialogsOpenState[DialogLabels.EDIT_USER]} + onClose={() => closeDialog(DialogLabels.EDIT_USER)} + handleUpdateUser={handleUpdateUser} + /> + <RemoveAdminDialog + open={dialogsOpenState[DialogLabels.TOGGLE_ADMIN]} + onClose={() => closeDialog(DialogLabels.TOGGLE_ADMIN)} + handleUpdateUserAdmin={handleUpdateUserAdmin} + /> + <ResetPasswordDialog + open={dialogsOpenState[DialogLabels.RESET_PASSWORD]} + onClose={() => closeDialog(DialogLabels.RESET_PASSWORD)} + handleUpdatePassword={handleUpdatePassword} + /> + <CreateUserDialog + open={dialogsOpenState[DialogLabels.CREATE_USER]} + onClose={() => closeDialog(DialogLabels.CREATE_USER)} + handleCreateUser={handleCreateUser} + /> + </> + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/index.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/index.ts new file mode 100644 index 0000000000..bc14789109 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useDialogHandlers' +export * from './useDialogData' diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogData/index.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogData/index.ts new file mode 100644 index 0000000000..2e467e1d49 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogData/index.ts @@ -0,0 +1 @@ +export * from './useDialogData' diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogData/useDialogData.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogData/useDialogData.ts new file mode 100644 index 0000000000..e097100f2b --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogData/useDialogData.ts @@ -0,0 +1,40 @@ +import { generateAlphaNumericHash } from '@/utils/utils' +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { UsersProps } from '@/views/user-management/types' +import { useDialogs } from '@views/user-management/providers/DialogsProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +export const useDialogData = () => { + const { useAdminListUsersStore } = useUserManagementStore() + + const { setUser, setPassword, setGeteneratePassword } = useAdminListUsersStore() + + const { openDialog } = useDialogs() + + const prepareDialogData = (user: UsersProps | null, dialogType: DialogLabels) => { + if (user) setUser(user) + + if (dialogType === DialogLabels.RESET_PASSWORD) { + setGeteneratePassword(false) + setPassword(generateAlphaNumericHash(10)) + + return + } + + if (dialogType === DialogLabels.CREATE_USER) { + setGeteneratePassword(true) + setPassword(generateAlphaNumericHash(10)) + + return + } + } + + const handleDialogOpen = (user: UsersProps | null, dialogType: DialogLabels) => { + prepareDialogData(user, dialogType) + openDialog(dialogType) + } + + return { + handleDialogOpen + } +} diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogHandlers/index.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogHandlers/index.ts new file mode 100644 index 0000000000..218cef7fd2 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogHandlers/index.ts @@ -0,0 +1 @@ +export * from './useDialogHandlers' diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogHandlers/useDialogHandlers.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogHandlers/useDialogHandlers.ts new file mode 100644 index 0000000000..c2d1a653a1 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/useDialogHandlers/useDialogHandlers.ts @@ -0,0 +1,46 @@ +import { DialogLabels, IDialogHandlers } from '@/views/user-management/components/dialogs' +import { useDialogs } from '@views/user-management/providers/DialogsProvider' + +export const useDialogHandlers = (handlers: IDialogHandlers) => { + const { closeDialog } = useDialogs() + + return { + handleCreateUser: async (...args: Parameters<typeof handlers.handleCreateUser>) => { + const result = await handlers.handleCreateUser(...args) + + closeDialog(DialogLabels.CREATE_USER) + + return result + }, + + handleUpdateUser: async (...args: Parameters<typeof handlers.handleUpdateUser>) => { + const result = await handlers.handleUpdateUser(...args) + + closeDialog(DialogLabels.EDIT_USER) + + return result + }, + + handleDeleteUser: async (...args: Parameters<typeof handlers.handleDeleteUser>) => { + const result = await handlers.handleDeleteUser(...args) + + closeDialog(DialogLabels.DELETE_USER) + + return result + }, + + handleUpdateUserAdmin: async (...args: Parameters<typeof handlers.handleUpdateUserAdmin>) => { + const result = await handlers.handleUpdateUserAdmin(...args) + + closeDialog(DialogLabels.TOGGLE_ADMIN) + + return result + }, + + handleUpdatePassword: async (...args: Parameters<typeof handlers.handleUpdatePassword>) => { + const result = await handlers.handleUpdatePassword(...args) + + return result + } + } +} diff --git a/packages/ui/src/views/user-management/components/dialogs/index.ts b/packages/ui/src/views/user-management/components/dialogs/index.ts new file mode 100644 index 0000000000..65c7d8cb0b --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/index.ts @@ -0,0 +1,2 @@ +export * from './dialogs' +export * from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/types.ts b/packages/ui/src/views/user-management/components/dialogs/types.ts new file mode 100644 index 0000000000..be0fd2ce81 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/types.ts @@ -0,0 +1,15 @@ +import { IDataHandlers } from '@/views/user-management/types/data-handlers' + +export interface IDialogHandlers extends IDataHandlers {} + +export enum DialogLabels { + DELETE_USER = 'deleteUser', + EDIT_USER = 'editUser', + TOGGLE_ADMIN = 'toggleAdmin', + RESET_PASSWORD = 'resetPassword', + CREATE_USER = 'createUser' +} + +export interface IDialogsProps { + handlers: IDialogHandlers +} diff --git a/packages/ui/src/views/user-management/components/edit-user-dialog.tsx b/packages/ui/src/views/user-management/components/edit-user-dialog.tsx deleted file mode 100644 index ec2e56bb43..0000000000 --- a/packages/ui/src/views/user-management/components/edit-user-dialog.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useEffect } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' - -import { AlertDialog, Button, ButtonGroup, ControlGroup, Fieldset, FormWrapper, Input } from '@/components' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' - -import { IEditUserDialogProps } from '../types' - -export const EditUserDialog: React.FC<IEditUserDialogProps> = ({ - useAdminListUsersStore, - onClose, - isSubmitting, - handleUpdateUser, - open -}) => { - const { user } = useAdminListUsersStore() - const newUserSchema = z.object({ - userID: z.string(), - email: z.string().email({ message: 'Please provide a valid email, ex: example@yourcompany.com' }), - displayName: z.string().min(1, { message: 'Please provide a display name' }) - }) - - type MemberFields = z.infer<typeof newUserSchema> - - const { - handleSubmit, - register, - reset: resetNewMemberForm, - formState: { errors } - } = useForm<MemberFields>({ - resolver: zodResolver(newUserSchema), - mode: 'onChange' - }) - - // Form edit submit handler - const onSubmit: SubmitHandler<MemberFields> = data => { - handleUpdateUser(data) - resetNewMemberForm(data) - } - - useEffect(() => { - resetNewMemberForm({ - userID: user?.uid, - email: user?.email, - displayName: user?.display_name - }) - }, [user, resetNewMemberForm]) - - return ( - <AlertDialog.Root open={open} onOpenChange={onClose}> - <AlertDialog.Content> - <AlertDialog.Header> - <AlertDialog.Title>Update User</AlertDialog.Title> - </AlertDialog.Header> - - {/* Accessibility: Add Description */} - <AlertDialog.Description>Update information for {user?.uid} and confirm changes.</AlertDialog.Description> - <FormWrapper onSubmit={handleSubmit(onSubmit)}> - <Fieldset> - {/* User ID */} - <ControlGroup> - <Input - id="userID" - {...register('userID')} - placeholder="Enter User ID" - value={user?.uid} - readOnly - className="cursor-not-allowed" - label="User ID cannot be changed once created" - error={errors.userID?.message?.toString()} - /> - </ControlGroup> - - {/* EMAIL */} - <ControlGroup> - <Input - id="email" - {...register('email')} - defaultValue={user?.email} - label="Email" - error={errors.email?.message?.toString()} - /> - </ControlGroup> - - {/* Display Name */} - <ControlGroup> - <Input - id="displayName" - {...register('displayName')} - defaultValue={user?.display_name} - placeholder="Enter a display name" - label="Display Name" - error={errors.displayName?.message?.toString()} - /> - </ControlGroup> - - {/* Footer */} - {/* <Spacer size={5} /> */} - <AlertDialog.Footer> - <ControlGroup> - <ButtonGroup> - <> - <Button variant="outline" onClick={onClose} disabled={isSubmitting}> - Cancel - </Button> - <Button type="submit" theme="primary" disabled={isSubmitting}> - {isSubmitting ? 'Saving...' : 'Save'} - </Button> - </> - </ButtonGroup> - </ControlGroup> - </AlertDialog.Footer> - </Fieldset> - </FormWrapper> - </AlertDialog.Content> - </AlertDialog.Root> - ) -} diff --git a/packages/ui/src/views/user-management/components/empty-state/empty-state.tsx b/packages/ui/src/views/user-management/components/empty-state/empty-state.tsx new file mode 100644 index 0000000000..1a7493b84d --- /dev/null +++ b/packages/ui/src/views/user-management/components/empty-state/empty-state.tsx @@ -0,0 +1,31 @@ +import { NoData } from '@/components' +import { DialogLabels } from '@views/user-management' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +import { useDialogData } from '../dialogs/hooks' + +export const EmptyState = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { handleDialogOpen } = useDialogData() + + return ( + <NoData + textWrapperClassName="w-[350px]" + iconName="no-data-members" + title={t('views:noData.noUsers', 'No Users Found')} + description={[ + t( + 'views:noData.noUsersDescription', + 'There are no users in this scope. Click on the button below to start adding them.' + ) + ]} + primaryButton={{ + label: t('views:userManagement.newUserButton', 'New user'), + onClick: () => handleDialogOpen(null, DialogLabels.CREATE_USER) + }} + /> + ) +} diff --git a/packages/ui/src/views/user-management/components/empty-state/index.ts b/packages/ui/src/views/user-management/components/empty-state/index.ts new file mode 100644 index 0000000000..ff47926401 --- /dev/null +++ b/packages/ui/src/views/user-management/components/empty-state/index.ts @@ -0,0 +1 @@ +export { EmptyState } from './empty-state' diff --git a/packages/ui/src/views/user-management/components/index.ts b/packages/ui/src/views/user-management/components/index.ts new file mode 100644 index 0000000000..48d1a756fc --- /dev/null +++ b/packages/ui/src/views/user-management/components/index.ts @@ -0,0 +1 @@ +export * from './main' diff --git a/packages/ui/src/views/user-management/components/index.tsx b/packages/ui/src/views/user-management/components/index.tsx deleted file mode 100644 index ac53434fc6..0000000000 --- a/packages/ui/src/views/user-management/components/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export * from './delete-user-dialog' -export * from './edit-user-dialog' -export * from './admin-dialog' -export * from './reset-password-dialog' -export * from './create-user-dialog' diff --git a/packages/ui/src/views/user-management/components/main.tsx b/packages/ui/src/views/user-management/components/main.tsx new file mode 100644 index 0000000000..54e5305eb7 --- /dev/null +++ b/packages/ui/src/views/user-management/components/main.tsx @@ -0,0 +1,17 @@ +import { IUserManagementPageProps, SandboxLayout } from '@views/index' +import { Dialogs } from '@views/user-management/components/dialogs' +import { Content } from '@views/user-management/components/page-components/content' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +export const UserManagementPageContent = ({ handlers }: Pick<IUserManagementPageProps, 'handlers'>) => { + const { useAdminListUsersStore } = useUserManagementStore() + + const { totalPages, page: currentPage, setPage } = useAdminListUsersStore() + + return ( + <SandboxLayout.Main className="max-w-[1092px]"> + <Content totalPages={totalPages} currentPage={currentPage} setPage={setPage} /> + <Dialogs handlers={handlers} /> + </SandboxLayout.Main> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/actions/actions.tsx b/packages/ui/src/views/user-management/components/page-components/actions/actions.tsx new file mode 100644 index 0000000000..1c721e734c --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/actions/actions.tsx @@ -0,0 +1,33 @@ +import { Button, ListActions, SearchBox } from '@/components' +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { useUserManagementStore } from '@/views/user-management/providers/StoreProvider' +import { useDialogData } from '@views/user-management/components/dialogs/hooks' +import { useSearch } from '@views/user-management/providers/SearchProvider' + +export const Actions = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { searchInput, handleInputChange } = useSearch() + + const { handleDialogOpen } = useDialogData() + + return ( + <ListActions.Root> + <ListActions.Left> + <SearchBox.Root + className="h-8 max-w-[320px]" + placeholder={t('views:userManagement.searchPlaceholder', 'Search')} + value={searchInput || ''} + handleChange={handleInputChange} + /> + </ListActions.Left> + <ListActions.Right> + <Button variant="default" onClick={() => handleDialogOpen(null, DialogLabels.CREATE_USER)}> + {t('views:userManagement.newUserButton', 'New user')} + </Button> + </ListActions.Right> + </ListActions.Root> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/actions/index.ts b/packages/ui/src/views/user-management/components/page-components/actions/index.ts new file mode 100644 index 0000000000..d04c018e24 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/actions/index.ts @@ -0,0 +1 @@ +export { Actions } from './actions' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx new file mode 100644 index 0000000000..a4753ba9d9 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx @@ -0,0 +1,33 @@ +import { NoData } from '@/components' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +export const ErrorState = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { errorStates } = useStates() + const { fetchUsersError } = errorStates + + return ( + <NoData + textWrapperClassName="max-w-[350px]" + iconName="no-data-error" + title={t('views:noData.errorApiTitle', 'Failed to load users')} + description={[ + fetchUsersError || + t( + 'views:noData.errorApiDescription', + 'An error occurred while loading the data. Please try again and reload the page.' + ) + ]} + primaryButton={{ + label: t('views:notFound.button', 'Reload page'), + onClick: () => { + window.location.reload() + } + }} + /> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts new file mode 100644 index 0000000000..3c3607299c --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts @@ -0,0 +1 @@ +export * from './error-state' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts new file mode 100644 index 0000000000..3c22761807 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts @@ -0,0 +1 @@ +export * from './no-search-results' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx new file mode 100644 index 0000000000..0e19570b93 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx @@ -0,0 +1,29 @@ +import { NoData } from '@/components' +import { useUserManagementStore } from '@/views/user-management/providers/StoreProvider' +import { useSearch } from '@views/user-management/providers/SearchProvider' + +export const NoSearchResults = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { handleResetSearch } = useSearch() + + return ( + <NoData + withBorder + textWrapperClassName="max-w-[350px]" + iconName="no-search-magnifying-glass" + title={t('views:noData.noResults', 'No search results')} + description={[ + t('views:noData.noResultsDescription', 'No users match your search. Try adjusting your keywords or filters.', { + type: 'users' + }) + ]} + primaryButton={{ + label: t('views:noData.clearFilters', 'Clear filters'), + onClick: handleResetSearch + }} + /> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts new file mode 100644 index 0000000000..be5b630d99 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts @@ -0,0 +1 @@ +export { UsersList } from './users-list' diff --git a/packages/ui/src/views/user-management/components/users-list.tsx b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx similarity index 50% rename from packages/ui/src/views/user-management/components/users-list.tsx rename to packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx index 61b459c03d..e9f049df7a 100644 --- a/packages/ui/src/views/user-management/components/users-list.tsx +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx @@ -4,31 +4,56 @@ import { AvatarImage, Badge, MoreActionsTooltip, + SkeletonList, Table, TableBody, TableCell, TableHead, TableHeader, - TableRow, - Text + TableRow } from '@/components' import { getInitials } from '@/utils/utils' +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { useDialogData } from '@views/user-management/components/dialogs/hooks' +import { ErrorState } from '@views/user-management/components/page-components/content/components/users-list/components/error-state' +import { NoSearchResults } from '@views/user-management/components/page-components/content/components/users-list/components/no-search-results' +import { useSearch } from '@views/user-management/providers/SearchProvider' +import { useStates } from '@views/user-management/providers/StateProvider/hooks/useStates' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' -import { DialogLabels, UsersProps } from '../types' +export const UsersList = () => { + const { useAdminListUsersStore } = useUserManagementStore() -interface PageProps { - users: UsersProps[] - handleDialogOpen: (user: UsersProps | null, dialogLabel: string) => void -} + const { users } = useAdminListUsersStore() + const { searchQuery } = useSearch() + const { handleDialogOpen } = useDialogData() + + const { loadingStates, errorStates } = useStates() + const { isFetchingUsers } = loadingStates + const { fetchUsersError } = errorStates + + if (isFetchingUsers) { + return <SkeletonList /> + } + + if (fetchUsersError) { + return <ErrorState /> + } + + // here should be additional check for users.length === 0, + // but until backend is not ready I leave only searchQuery to make it possible to see how this component works + // TODO: add additional check for users.length === 0 when backend will be ready + if (searchQuery) { + return <NoSearchResults /> + } -export const UsersList = ({ users, handleDialogOpen }: PageProps) => { return ( <Table variant="asStackedList"> - <TableHeader> + <TableHeader className="h-[46px]"> <TableRow> - <TableHead>User</TableHead> - <TableHead>Display Name</TableHead> - <TableHead>Email</TableHead> + <TableHead className="w-[346px]">User</TableHead> + <TableHead className="w-[346px]">Email</TableHead> + <TableHead className="w-[346px]">Role binding</TableHead> <TableHead> <></> </TableHead> @@ -38,7 +63,7 @@ export const UsersList = ({ users, handleDialogOpen }: PageProps) => { {users && users.map(user => { return ( - <TableRow key={user.uid}> + <TableRow key={user.uid} className="h-[48px]"> {/* NAME */} <TableCell className="my-6 content-center"> <div className="flex items-center gap-2"> @@ -46,34 +71,23 @@ export const UsersList = ({ users, handleDialogOpen }: PageProps) => { {user.avatarUrl && <AvatarImage src={user.avatarUrl} />} <AvatarFallback>{getInitials(user.uid!, 2)}</AvatarFallback> </Avatar> - <Text size={2} weight="medium" wrap="nowrap" truncate className="text-primary"> - {user.uid} - {user.admin && ( - <Badge - variant="outline" - size="xs" - className="m-auto ml-2 h-5 rounded-full bg-tertiary-background/10 p-2 text-center text-xs font-normal text-tertiary-background" - > - Admin - </Badge> - )} - </Text> + <span className="truncate whitespace-nowrap text-sm font-medium text-foreground-8">{user.uid}</span> </div> </TableCell> - {/* DISPLAY NAME */} + {/* EMAIL */} <TableCell className="my-6 content-center"> - <Text size={2} weight="medium" wrap="nowrap" truncate className="text-primary"> - {user.display_name} - </Text> + <div className="flex gap-1.5"> + <span className="truncate whitespace-nowrap text-sm text-foreground-3">{user.email}</span> + </div> </TableCell> - {/* EMAIL */} + {/* ROLE BINDING */} <TableCell className="my-6 content-center"> <div className="flex gap-1.5"> - <Text wrap="nowrap" size={1} truncate className="text-tertiary-background"> - {user.email} - </Text> + <Badge variant="outline" size="sm" theme={user.admin ? 'emphasis' : 'destructive'}> + {user.admin ? 'Admin' : 'User'} + </Badge> </div> </TableCell> diff --git a/packages/ui/src/views/user-management/components/page-components/content/content.tsx b/packages/ui/src/views/user-management/components/page-components/content/content.tsx new file mode 100644 index 0000000000..cc1d09be31 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/content.tsx @@ -0,0 +1,39 @@ +import { PaginationComponent, Spacer } from '@/components' +import { SandboxLayout } from '@/views' +import { Actions } from '@/views/user-management/components/page-components/actions' +import { UsersList } from '@/views/user-management/components/page-components/content/components/users-list' +import { ContentProps } from '@/views/user-management/components/page-components/content/types' +import { Header } from '@/views/user-management/components/page-components/header' +import { EmptyState } from '@views/user-management/components/empty-state/empty-state' +import { useStates } from '@views/user-management/providers/StateProvider' +import { useUserManagementStore } from '@views/user-management/providers/StoreProvider' + +export const Content = ({ totalPages, currentPage, setPage }: ContentProps) => { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { users } = useAdminListUsersStore() + + const { loadingStates } = useStates() + const { isFetchingUsers } = loadingStates + + const { t } = useTranslationStore() + + if (!isFetchingUsers && users?.length === 0) { + return <EmptyState /> + } + + return ( + <SandboxLayout.Content className="mx-auto max-w-[1092px]" paddingClassName="px-0"> + <Header /> + <Actions /> + <Spacer size={4.5} /> + <UsersList /> + <PaginationComponent + totalPages={totalPages} + currentPage={currentPage} + goToPage={(pageNum: number) => setPage(pageNum)} + t={t} + /> + </SandboxLayout.Content> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/index.ts b/packages/ui/src/views/user-management/components/page-components/content/index.ts new file mode 100644 index 0000000000..fa11a30216 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/index.ts @@ -0,0 +1,2 @@ +export { Content } from './content' +export type { ContentProps } from './types' diff --git a/packages/ui/src/views/user-management/components/page-components/content/types.ts b/packages/ui/src/views/user-management/components/page-components/content/types.ts new file mode 100644 index 0000000000..e571fae3be --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/types.ts @@ -0,0 +1,5 @@ +export interface ContentProps { + totalPages: number + currentPage: number + setPage: (page: number) => void +} diff --git a/packages/ui/src/views/user-management/components/page-components/header/header.tsx b/packages/ui/src/views/user-management/components/page-components/header/header.tsx new file mode 100644 index 0000000000..a351a6c80e --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/header/header.tsx @@ -0,0 +1,21 @@ +import { Spacer } from '@/components' +import { useUserManagementStore } from '@/views/user-management/providers/StoreProvider' + +export const Header = () => { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { users } = useAdminListUsersStore() + + const { t } = useTranslationStore() + + return ( + <> + <Spacer size={7} /> + <span className="text-2xl font-medium text-foreground-1"> + {t('views:userManagement.usersHeader', 'Users')}{' '} + <span className="text-2xl font-medium text-foreground-4">({users?.length || 0})</span> + </span> + <Spacer size={6} /> + </> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/header/index.ts b/packages/ui/src/views/user-management/components/page-components/header/index.ts new file mode 100644 index 0000000000..d0dd85a276 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/header/index.ts @@ -0,0 +1 @@ +export { Header } from './header' diff --git a/packages/ui/src/views/user-management/components/reset-password-dialog.tsx b/packages/ui/src/views/user-management/components/reset-password-dialog.tsx deleted file mode 100644 index 35d38c732d..0000000000 --- a/packages/ui/src/views/user-management/components/reset-password-dialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { FC } from 'react' - -import { AlertDialog, Button, CopyButton, Input, Text } from '@/components' - -import { IResetPasswordDialogProps } from '../types' - -export const ResetPasswordDialog: FC<IResetPasswordDialogProps> = ({ - open, - useAdminListUsersStore, - onClose, - handleUpdatePassword -}) => { - const { user, generatePassword, setGeteneratePassword, password } = useAdminListUsersStore() - - const handleResetPassword = () => { - handleUpdatePassword(user?.uid ?? '') - } - - return ( - <AlertDialog.Root open={open} onOpenChange={onClose}> - <AlertDialog.Trigger asChild></AlertDialog.Trigger> - <AlertDialog.Content> - <AlertDialog.Header> - {generatePassword ? ( - <AlertDialog.Title>Reset Password</AlertDialog.Title> - ) : ( - <AlertDialog.Title>Are you sure you want to reset password for {user?.display_name}?</AlertDialog.Title> - )} - <AlertDialog.Description> - {generatePassword ? ( - <Text as="div" color="tertiaryBackground" className="mb-4"> - Your password has been generated. Please make sure to copy and store your password somewhere safe, you - won't be able to see it again. - </Text> - ) : ( - <Text as="div" color="tertiaryBackground" className="mb-4"> - A new password will be generated to assist {user?.display_name} in resetting their current password. - </Text> - )} - {generatePassword && ( - <Input - id="identifier" - value={password ?? ''} - readOnly - rightElement={<CopyButton name={password ?? ''} />} - /> - )} - </AlertDialog.Description> - </AlertDialog.Header> - <AlertDialog.Footer> - <Button variant="outline" onClick={onClose}> - {generatePassword ? `Close` : `Cancel`} - </Button> - {!generatePassword && ( - <Button - size="default" - variant="secondary" - className="self-start" - onClick={() => { - handleResetPassword() - setGeteneratePassword(true) - }} - > - Confirm - </Button> - )} - </AlertDialog.Footer> - </AlertDialog.Content> - </AlertDialog.Root> - ) -} diff --git a/packages/ui/src/views/user-management/index.ts b/packages/ui/src/views/user-management/index.ts index ac8483be3d..ca8312b263 100644 --- a/packages/ui/src/views/user-management/index.ts +++ b/packages/ui/src/views/user-management/index.ts @@ -1,3 +1,4 @@ export * from './user-management-page' -export * from './components' export * from './types' +export * from './components/dialogs/types' +export * from './providers/StoreProvider/types' diff --git a/packages/ui/src/views/user-management/providers/DialogsProvider/DialogsProvider.tsx b/packages/ui/src/views/user-management/providers/DialogsProvider/DialogsProvider.tsx new file mode 100644 index 0000000000..a4fa92c0c3 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/DialogsProvider/DialogsProvider.tsx @@ -0,0 +1,35 @@ +import { createContext, useState } from 'react' + +import { DialogLabels } from '@/views/user-management/components/dialogs' + +import { DialogsContextType, DialogState } from './types' + +export const DialogsContext = createContext<DialogsContextType | undefined>(undefined) + +export const DialogsProvider = ({ children }: { children: React.ReactNode }) => { + const [dialogsOpenState, setDialogsOpenState] = useState<DialogState>({ + [DialogLabels.DELETE_USER]: false, + [DialogLabels.EDIT_USER]: false, + [DialogLabels.TOGGLE_ADMIN]: false, + [DialogLabels.RESET_PASSWORD]: false, + [DialogLabels.CREATE_USER]: false + }) + + const openDialog = (dialogType: DialogLabels) => { + setDialogsOpenState(prev => ({ + ...prev, + [dialogType]: true + })) + } + + const closeDialog = (dialogType: DialogLabels) => { + setDialogsOpenState(prev => ({ + ...prev, + [dialogType]: false + })) + } + + return ( + <DialogsContext.Provider value={{ dialogsOpenState, openDialog, closeDialog }}>{children}</DialogsContext.Provider> + ) +} diff --git a/packages/ui/src/views/user-management/providers/DialogsProvider/hooks/useDialogs.ts b/packages/ui/src/views/user-management/providers/DialogsProvider/hooks/useDialogs.ts new file mode 100644 index 0000000000..f90ccdff62 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/DialogsProvider/hooks/useDialogs.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { DialogsContext } from '@/views/user-management/providers/DialogsProvider' + +export const useDialogs = () => { + const context = useContext(DialogsContext) + + if (!context) { + throw new Error('useDialogs must be used within DialogsProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/DialogsProvider/index.ts b/packages/ui/src/views/user-management/providers/DialogsProvider/index.ts new file mode 100644 index 0000000000..fbde073f95 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/DialogsProvider/index.ts @@ -0,0 +1,2 @@ +export * from './DialogsProvider' +export * from './hooks/useDialogs' diff --git a/packages/ui/src/views/user-management/providers/DialogsProvider/types.ts b/packages/ui/src/views/user-management/providers/DialogsProvider/types.ts new file mode 100644 index 0000000000..b8725b062e --- /dev/null +++ b/packages/ui/src/views/user-management/providers/DialogsProvider/types.ts @@ -0,0 +1,11 @@ +import { DialogLabels } from '@/views/user-management/components/dialogs' + +export type DialogState = { + [K in DialogLabels]: boolean +} + +export interface DialogsContextType { + dialogsOpenState: DialogState + openDialog: (dialogType: DialogLabels) => void + closeDialog: (dialogType: DialogLabels) => void +} diff --git a/packages/ui/src/views/user-management/providers/SearchProvider/SearchProvider.tsx b/packages/ui/src/views/user-management/providers/SearchProvider/SearchProvider.tsx new file mode 100644 index 0000000000..ff0418fc05 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/SearchProvider/SearchProvider.tsx @@ -0,0 +1,31 @@ +import { createContext } from 'react' + +import { useDebounceSearch } from '@hooks/use-debounce-search' + +import { SearchContextType, SearchProviderProps } from './types' + +export const SearchContext = createContext<SearchContextType | undefined>(undefined) + +export const SearchProvider = ({ children, searchQuery, setSearchQuery }: SearchProviderProps) => { + const { + search: searchInput, + handleSearchChange: handleInputChange, + handleResetSearch + } = useDebounceSearch({ + handleChangeSearchValue: (val: string) => setSearchQuery(val.length ? val : null), + searchValue: searchQuery || '' + }) + + return ( + <SearchContext.Provider + value={{ + searchQuery, + searchInput, + handleInputChange, + handleResetSearch + }} + > + {children} + </SearchContext.Provider> + ) +} diff --git a/packages/ui/src/views/user-management/providers/SearchProvider/hooks/useSearch.ts b/packages/ui/src/views/user-management/providers/SearchProvider/hooks/useSearch.ts new file mode 100644 index 0000000000..c7e8b2fd18 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/SearchProvider/hooks/useSearch.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { SearchContext } from '../SearchProvider' + +export const useSearch = () => { + const context = useContext(SearchContext) + + if (!context) { + throw new Error('useSearch must be used within SearchProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/SearchProvider/index.ts b/packages/ui/src/views/user-management/providers/SearchProvider/index.ts new file mode 100644 index 0000000000..c3cceea724 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/SearchProvider/index.ts @@ -0,0 +1,3 @@ +export * from './SearchProvider' +export * from './hooks/useSearch' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/SearchProvider/types.ts b/packages/ui/src/views/user-management/providers/SearchProvider/types.ts new file mode 100644 index 0000000000..a23ea18a97 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/SearchProvider/types.ts @@ -0,0 +1,12 @@ +export interface SearchContextType { + searchQuery: string | null + searchInput: string | null + handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void + handleResetSearch: () => void +} + +export interface SearchProviderProps { + searchQuery: string | null + setSearchQuery: (query: string | null) => void + children: React.ReactNode +} diff --git a/packages/ui/src/views/user-management/providers/StateProvider/StateProvider.tsx b/packages/ui/src/views/user-management/providers/StateProvider/StateProvider.tsx new file mode 100644 index 0000000000..f6327f228d --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StateProvider/StateProvider.tsx @@ -0,0 +1,18 @@ +import { createContext } from 'react' + +import { StateContextType, StateProviderProps } from './types' + +export const StateContext = createContext<StateContextType | undefined>(undefined) + +export const StateProvider = ({ children, loadingStates, errorStates }: StateProviderProps) => { + return ( + <StateContext.Provider + value={{ + loadingStates, + errorStates + }} + > + {children} + </StateContext.Provider> + ) +} diff --git a/packages/ui/src/views/user-management/providers/StateProvider/hooks/useStates.ts b/packages/ui/src/views/user-management/providers/StateProvider/hooks/useStates.ts new file mode 100644 index 0000000000..5ebad9d51c --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StateProvider/hooks/useStates.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { StateContext } from '../StateProvider' + +export const useStates = () => { + const context = useContext(StateContext) + + if (!context) { + throw new Error('useStates must be used within StateProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/StateProvider/index.ts b/packages/ui/src/views/user-management/providers/StateProvider/index.ts new file mode 100644 index 0000000000..24f6b22c80 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StateProvider/index.ts @@ -0,0 +1,3 @@ +export * from './StateProvider' +export * from './hooks/useStates' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/StateProvider/types.ts b/packages/ui/src/views/user-management/providers/StateProvider/types.ts new file mode 100644 index 0000000000..40cfa78cdf --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StateProvider/types.ts @@ -0,0 +1,26 @@ +export interface ILoadingStates { + isFetchingUsers: boolean + isUpdatingUser: boolean + isDeletingUser: boolean + isUpdatingUserAdmin: boolean + isCreatingUser: boolean +} + +export interface IErrorStates { + fetchUsersError: string + updateUserError: string + deleteUserError: string + updateUserAdminError: string + createUserError: string +} + +export interface StateContextType { + loadingStates: ILoadingStates + errorStates: IErrorStates +} + +export interface StateProviderProps { + loadingStates: StateContextType['loadingStates'] + errorStates: StateContextType['errorStates'] + children: React.ReactNode +} diff --git a/packages/ui/src/views/user-management/providers/StoreProvider/StoreProvider.tsx b/packages/ui/src/views/user-management/providers/StoreProvider/StoreProvider.tsx new file mode 100644 index 0000000000..8d006c1d0c --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StoreProvider/StoreProvider.tsx @@ -0,0 +1,15 @@ +import { createContext } from 'react' + +import { StoreContextType, StoreProviderProps } from './types' + +export const StoreContext = createContext<StoreContextType | undefined>(undefined) + +export const UserManagementStoreProvider = ({ + children, + useAdminListUsersStore, + useTranslationStore +}: StoreProviderProps) => { + return ( + <StoreContext.Provider value={{ useAdminListUsersStore, useTranslationStore }}>{children}</StoreContext.Provider> + ) +} diff --git a/packages/ui/src/views/user-management/providers/StoreProvider/hooks/useUserManagementStore.ts b/packages/ui/src/views/user-management/providers/StoreProvider/hooks/useUserManagementStore.ts new file mode 100644 index 0000000000..fc3f011bbb --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StoreProvider/hooks/useUserManagementStore.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { StoreContext } from '@views/user-management/providers/StoreProvider' + +export const useUserManagementStore = () => { + const context = useContext(StoreContext) + + if (!context) { + throw new Error('useUserManagementStore must be used within UserManagementStoreProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/StoreProvider/index.ts b/packages/ui/src/views/user-management/providers/StoreProvider/index.ts new file mode 100644 index 0000000000..4d94e7f8b6 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StoreProvider/index.ts @@ -0,0 +1,3 @@ +export * from './hooks/useUserManagementStore' +export * from './StoreProvider' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/StoreProvider/types.ts b/packages/ui/src/views/user-management/providers/StoreProvider/types.ts new file mode 100644 index 0000000000..fe371bd099 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/StoreProvider/types.ts @@ -0,0 +1,27 @@ +import { TranslationStore, UsersProps } from '@/views' + +export interface IAdminListUsersStore { + users: UsersProps[] + totalPages: number + page: number + password: string | null + user: UsersProps | null + generatePassword: boolean + setPassword: (password: string) => void + setUser: (user: UsersProps) => void + setPage: (data: number) => void + setUsers: (data: UsersProps[]) => void + setTotalPages: (data: Headers) => void + setGeteneratePassword: (data: boolean) => void +} + +export interface StoreProviderProps { + useAdminListUsersStore: () => IAdminListUsersStore + useTranslationStore: () => TranslationStore + children: React.ReactNode +} + +export interface StoreContextType { + useAdminListUsersStore: () => IAdminListUsersStore + useTranslationStore: () => TranslationStore +} diff --git a/packages/ui/src/views/user-management/types.ts b/packages/ui/src/views/user-management/types.ts deleted file mode 100644 index a290a2d028..0000000000 --- a/packages/ui/src/views/user-management/types.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { TranslationStore } from '@/views' - -export interface UsersProps { - admin?: boolean - uid?: string - display_name?: string | undefined - email?: string - created?: number - updated?: number - avatarUrl?: string - blocked?: boolean -} - -export interface IUserManagementPageProps { - useAdminListUsersStore: () => IAdminListUsersStore - useTranslationStore: () => TranslationStore - handleDialogOpen: (user: UsersProps | null, dialogLabel: string) => void -} -export interface IAdminListUsersStore { - users: UsersProps[] - totalPages: number - page: number - password: string | null - user: UsersProps | null - generatePassword: boolean - setPassword: (password: string) => void - setUser: (user: UsersProps) => void - setPage: (data: number) => void - setUsers: (data: UsersProps[]) => void - setTotalPages: (data: Headers) => void - setGeteneratePassword: (data: boolean) => void -} - -export interface IDeleteDialogProps { - open: boolean - onClose: () => void - isDeleting: boolean - handleDeleteUser: (userUid: string) => void - useAdminListUsersStore: () => IAdminListUsersStore -} - -export interface IEditUserDialogProps { - isSubmitting: boolean - onClose: () => void - handleUpdateUser: (data: { email: string; displayName: string; userID: string }) => void - open: boolean - useAdminListUsersStore: () => IAdminListUsersStore -} - -export interface IRemoveAdminDialogProps { - open: boolean - onClose: () => void - isLoading: boolean - updateUserAdmin: (uid: string, admin: boolean) => void - useAdminListUsersStore: () => IAdminListUsersStore -} - -export interface IResetPasswordDialogProps { - onClose: () => void - open: boolean - handleUpdatePassword: (userId: string) => void - useAdminListUsersStore: () => IAdminListUsersStore -} - -export enum DialogLabels { - DELETE_USER = 'deleteUser', - EDIT_USER = 'editUser', - TOGGLE_ADMIN = 'toggleAdmin', - RESET_PASSWORD = 'resetPassword', - CREATE_USER = 'createUser' -} diff --git a/packages/ui/src/views/user-management/types/data-handlers.ts b/packages/ui/src/views/user-management/types/data-handlers.ts new file mode 100644 index 0000000000..2946d17ed8 --- /dev/null +++ b/packages/ui/src/views/user-management/types/data-handlers.ts @@ -0,0 +1,19 @@ +export interface IUpdateUserData { + email: string + displayName: string + userID: string +} + +export interface ICreateUserData { + uid: string + email: string + display_name: string +} + +export interface IDataHandlers { + handleUpdateUser: (data: IUpdateUserData) => Promise<void> + handleDeleteUser: (userUid: string) => Promise<void> + handleUpdateUserAdmin: (userUid: string, isAdmin: boolean) => Promise<void> + handleUpdatePassword: (userId: string) => Promise<void> + handleCreateUser: (data: ICreateUserData) => Promise<void> +} diff --git a/packages/ui/src/views/user-management/types/index.ts b/packages/ui/src/views/user-management/types/index.ts new file mode 100644 index 0000000000..8e9802e57b --- /dev/null +++ b/packages/ui/src/views/user-management/types/index.ts @@ -0,0 +1,2 @@ +export * from './main' +export * from './data-handlers' diff --git a/packages/ui/src/views/user-management/types/main.ts b/packages/ui/src/views/user-management/types/main.ts new file mode 100644 index 0000000000..dd3a7fcb6e --- /dev/null +++ b/packages/ui/src/views/user-management/types/main.ts @@ -0,0 +1,25 @@ +import { TranslationStore } from '@/views' +import { IDialogHandlers } from '@/views/user-management/components/dialogs' +import { IErrorStates, ILoadingStates } from '@/views/user-management/providers/StateProvider/types' +import { IAdminListUsersStore } from '@/views/user-management/providers/StoreProvider' + +export interface UsersProps { + admin?: boolean + uid?: string + display_name?: string | undefined + email?: string + created?: number + updated?: number + avatarUrl?: string + blocked?: boolean +} + +export interface IUserManagementPageProps { + useAdminListUsersStore: () => IAdminListUsersStore + useTranslationStore: () => TranslationStore + handlers: IDialogHandlers + loadingStates: ILoadingStates + errorStates: IErrorStates + searchQuery: string | null + setSearchQuery: (query: string | null) => void +} diff --git a/packages/ui/src/views/user-management/user-management-page.tsx b/packages/ui/src/views/user-management/user-management-page.tsx index a2b8833def..c24fc5c4ee 100644 --- a/packages/ui/src/views/user-management/user-management-page.tsx +++ b/packages/ui/src/views/user-management/user-management-page.tsx @@ -1,53 +1,32 @@ -import { Button, ListActions, PaginationComponent, SearchBox, Spacer, Text } from '@/components' -import { SandboxLayout } from '@/views' +import { DialogsProvider } from '@views/user-management/providers/DialogsProvider' +import { SearchProvider } from '@views/user-management/providers/SearchProvider' +import { StateProvider } from '@views/user-management/providers/StateProvider' +import { UserManagementStoreProvider } from '@views/user-management/providers/StoreProvider' +import { IUserManagementPageProps } from '@views/user-management/types' -import { UsersList } from './components/users-list' -import { DialogLabels, IUserManagementPageProps, UsersProps } from './types' +import { UserManagementPageContent } from './components' export const UserManagementPage: React.FC<IUserManagementPageProps> = ({ useAdminListUsersStore, useTranslationStore, - handleDialogOpen + handlers, + loadingStates, + errorStates, + searchQuery, + setSearchQuery }) => { - const { users: userData, totalPages, page: currentPage, setPage } = useAdminListUsersStore() - const { t } = useTranslationStore() - - const renderUserListContent = () => { - return ( - <> - <UsersList users={userData as UsersProps[]} handleDialogOpen={handleDialogOpen} /> - </> - ) - } - return ( - <SandboxLayout.Main> - <SandboxLayout.Content maxWidth="3xl"> - <Spacer size={10} /> - <Text size={5} weight={'medium'}> - Users - </Text> - <Spacer size={6} /> - <ListActions.Root> - <ListActions.Left> - <SearchBox.Root width="full" className="max-w-96" placeholder="search" /> - </ListActions.Left> - <ListActions.Right> - <Button variant="default" onClick={() => handleDialogOpen(null, DialogLabels.CREATE_USER)}> - New user - </Button> - </ListActions.Right> - </ListActions.Root> - <Spacer size={5} /> - {renderUserListContent()} - <Spacer size={8} /> - <PaginationComponent - totalPages={totalPages} - currentPage={currentPage} - goToPage={(pageNum: number) => setPage(pageNum)} - t={t} - /> - </SandboxLayout.Content> - </SandboxLayout.Main> + <UserManagementStoreProvider + useAdminListUsersStore={useAdminListUsersStore} + useTranslationStore={useTranslationStore} + > + <StateProvider loadingStates={loadingStates} errorStates={errorStates}> + <SearchProvider searchQuery={searchQuery} setSearchQuery={setSearchQuery}> + <DialogsProvider> + <UserManagementPageContent handlers={handlers} /> + </DialogsProvider> + </SearchProvider> + </StateProvider> + </UserManagementStoreProvider> ) }