mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-11-03 03:37:45 +00:00
Merge pull request #139 from stephb9959/main
[WIFI-11866] User role and user edit fixes
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(31)",
|
"version": "2.8.0(32)",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(31)",
|
"version": "2.8.0(32)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.11",
|
"@chakra-ui/icons": "^2.0.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(31)",
|
"version": "2.8.0(32)",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.tsx",
|
"main": "index.tsx",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { User } from '../../models/User';
|
import { User } from '../../models/User';
|
||||||
|
import { UserRole } from './Users';
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { Note } from 'models/Note';
|
import { Note } from 'models/Note';
|
||||||
@@ -141,6 +142,7 @@ export const useUpdateAccount = ({ user }: { user?: User }) => {
|
|||||||
};
|
};
|
||||||
mobiles?: { number: string }[];
|
mobiles?: { number: string }[];
|
||||||
};
|
};
|
||||||
|
userRole?: UserRole;
|
||||||
notes?: Note[];
|
notes?: Note[];
|
||||||
}) => axiosSec.put(`user/${user?.id ?? userInfo?.id}`, userInfo),
|
}) => axiosSec.put(`user/${user?.id ?? userInfo?.id}`, userInfo),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,62 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { User } from 'models/User';
|
import { AtLeast } from 'models/General';
|
||||||
|
import { Note } from 'models/Note';
|
||||||
|
|
||||||
|
export type UserRole =
|
||||||
|
| 'root'
|
||||||
|
| 'admin'
|
||||||
|
| 'subscriber'
|
||||||
|
| 'partner'
|
||||||
|
| 'csr'
|
||||||
|
| 'system'
|
||||||
|
| 'installer'
|
||||||
|
| 'noc'
|
||||||
|
| 'accounting';
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
avatar: string;
|
||||||
|
blackListed: boolean;
|
||||||
|
creationDate: number;
|
||||||
|
currentLoginURI: string;
|
||||||
|
currentPassword: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
id: string;
|
||||||
|
lastEmailCheck: number;
|
||||||
|
lastLogin: number;
|
||||||
|
lastPasswordChange: number;
|
||||||
|
lastPasswords: string[];
|
||||||
|
locale: string;
|
||||||
|
location: string;
|
||||||
|
modified: number;
|
||||||
|
name: string;
|
||||||
|
notes: Note[];
|
||||||
|
oauthType: string;
|
||||||
|
oauthUserInfo: string;
|
||||||
|
owner: string;
|
||||||
|
securityPolicy: string;
|
||||||
|
securityPolicyChange: number;
|
||||||
|
signingUp: string;
|
||||||
|
suspended: boolean;
|
||||||
|
userRole: UserRole;
|
||||||
|
userTypeProprietaryInfo: {
|
||||||
|
authenticatorSecret: string;
|
||||||
|
mfa: {
|
||||||
|
enabled: boolean;
|
||||||
|
method?: 'authenticator' | 'sms' | 'email' | '';
|
||||||
|
};
|
||||||
|
mobiles: { number: string }[];
|
||||||
|
};
|
||||||
|
validated: boolean;
|
||||||
|
validationDate: number;
|
||||||
|
validationEmail: string;
|
||||||
|
validationURI: string;
|
||||||
|
waitingForEmailCheck: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const getAvatarPromises = (userList: User[]) => {
|
const getAvatarPromises = (userList: User[]) => {
|
||||||
const promises = userList.map(async (user) => {
|
const promises = userList.map(async (user) => {
|
||||||
@@ -18,12 +71,9 @@ const getAvatarPromises = (userList: User[]) => {
|
|||||||
return promises;
|
return promises;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (users: unknown) => void }) => {
|
const getUsers = async () => {
|
||||||
const { t } = useTranslation();
|
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
return useQuery(['get-users'], () => axiosSec.get('users').then(({ data }) => data.users), {
|
|
||||||
onSuccess: async (users) => {
|
|
||||||
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
||||||
results.map((response) => {
|
results.map((response) => {
|
||||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||||
@@ -37,9 +87,14 @@ export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (use
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newUsers = users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] }));
|
return users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] })) as User[];
|
||||||
setUsersWithAvatars(newUsers);
|
};
|
||||||
},
|
|
||||||
|
export const useGetUsers = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return useQuery(['users'], getUsers, {
|
||||||
onError: (e: AxiosError) => {
|
onError: (e: AxiosError) => {
|
||||||
if (!toast.isActive('users-fetching-error'))
|
if (!toast.isActive('users-fetching-error'))
|
||||||
toast({
|
toast({
|
||||||
@@ -62,7 +117,10 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
return useQuery(['get-user', id], () => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data), {
|
return useQuery(
|
||||||
|
['get-user', id],
|
||||||
|
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
||||||
|
{
|
||||||
enabled,
|
enabled,
|
||||||
onError: (e: AxiosError) => {
|
onError: (e: AxiosError) => {
|
||||||
if (!toast.isActive('user-fetching-error'))
|
if (!toast.isActive('user-fetching-error'))
|
||||||
@@ -79,7 +137,8 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
|||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refresh: () => void }) => {
|
export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refresh: () => void }) => {
|
||||||
@@ -124,3 +183,45 @@ export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSe
|
|||||||
|
|
||||||
export const useResetPassword = ({ id }: { id: string }) =>
|
export const useResetPassword = ({ id }: { id: string }) =>
|
||||||
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
||||||
|
|
||||||
|
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
||||||
|
export const useDeleteUser = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(deleteUser, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = async (newUser: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
email: string;
|
||||||
|
currentPassword: string;
|
||||||
|
notes?: { note: string }[];
|
||||||
|
userRole: string;
|
||||||
|
emailValidation: boolean;
|
||||||
|
changePassword: boolean;
|
||||||
|
}) => axiosSec.post(`user/0${newUser.emailValidation ? '?email_verification=true' : ''}`, newUser);
|
||||||
|
export const useCreateUser = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(createUser, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifyUser = async (newUser: AtLeast<User, 'id'>) => axiosSec.put(`user/${newUser.id}`, newUser);
|
||||||
|
export const useUpdateUser = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(modifyUser, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Ref, useCallback, useMemo, useState } from 'react';
|
|||||||
import { FormikProps } from 'formik';
|
import { FormikProps } from 'formik';
|
||||||
import { FormType } from '../models/Form';
|
import { FormType } from '../models/Form';
|
||||||
|
|
||||||
export const useFormRef = () => {
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const useFormRef = <Type = Record<string, unknown>>() => {
|
||||||
const [form, setForm] = useState<FormType>({
|
const [form, setForm] = useState<FormType>({
|
||||||
submitForm: () => {},
|
submitForm: () => {},
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
@@ -10,7 +11,7 @@ export const useFormRef = () => {
|
|||||||
dirty: false,
|
dirty: false,
|
||||||
});
|
});
|
||||||
const formRef = useCallback(
|
const formRef = useCallback(
|
||||||
(node: FormikProps<Record<string, unknown>> | undefined) => {
|
(node: FormikProps<Type>) => {
|
||||||
if (
|
if (
|
||||||
node &&
|
node &&
|
||||||
(form.submitForm !== node.submitForm ||
|
(form.submitForm !== node.submitForm ||
|
||||||
@@ -22,7 +23,7 @@ export const useFormRef = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[form],
|
[form],
|
||||||
) as Ref<FormikProps<Record<string, unknown>>> | undefined;
|
) as Ref<FormikProps<Type>>;
|
||||||
|
|
||||||
const toReturn = useMemo(() => ({ form, formRef }), [form]);
|
const toReturn = useMemo(() => ({ form, formRef }), [form]);
|
||||||
|
|
||||||
|
|||||||
1
src/models/General.ts
Normal file
1
src/models/General.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Note } from './Note';
|
import { Note } from './Note';
|
||||||
|
|
||||||
|
|
||||||
export type UserRole =
|
export type UserRole =
|
||||||
| 'root'
|
| 'root'
|
||||||
| 'admin'
|
| 'admin'
|
||||||
@@ -11,13 +12,31 @@ export type UserRole =
|
|||||||
| 'noc'
|
| 'noc'
|
||||||
| 'accounting';
|
| 'accounting';
|
||||||
|
|
||||||
export interface User {
|
export type User = {
|
||||||
name: string;
|
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
blackListed: boolean;
|
||||||
|
creationDate: number;
|
||||||
|
currentLoginURI: string;
|
||||||
|
currentPassword: string;
|
||||||
description: string;
|
description: string;
|
||||||
currentPassword?: string;
|
|
||||||
id: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
|
id: string;
|
||||||
|
lastEmailCheck: number;
|
||||||
|
lastLogin: number;
|
||||||
|
lastPasswordChange: number;
|
||||||
|
lastPasswords: string[];
|
||||||
|
locale: string;
|
||||||
|
location: string;
|
||||||
|
modified: number;
|
||||||
|
name: string;
|
||||||
|
notes: Note[];
|
||||||
|
oauthType: string;
|
||||||
|
oauthUserInfo: string;
|
||||||
|
owner: string;
|
||||||
|
securityPolicy: string;
|
||||||
|
securityPolicyChange: number;
|
||||||
|
signingUp: string;
|
||||||
|
suspended: boolean;
|
||||||
userRole: UserRole;
|
userRole: UserRole;
|
||||||
userTypeProprietaryInfo: {
|
userTypeProprietaryInfo: {
|
||||||
authenticatorSecret: string;
|
authenticatorSecret: string;
|
||||||
@@ -27,6 +46,9 @@ export interface User {
|
|||||||
};
|
};
|
||||||
mobiles: { number: string }[];
|
mobiles: { number: string }[];
|
||||||
};
|
};
|
||||||
suspended: boolean;
|
validated: boolean;
|
||||||
notes: Note[];
|
validationDate: number;
|
||||||
}
|
validationEmail: string;
|
||||||
|
validationURI: string;
|
||||||
|
waitingForEmailCheck: boolean;
|
||||||
|
};
|
||||||
|
|||||||
89
src/pages/Profile/DeleteButton.tsx
Normal file
89
src/pages/Profile/DeleteButton.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertIcon,
|
||||||
|
AlertTitle,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Spinner,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DeleteButton } from '../../components/Buttons/DeleteButton';
|
||||||
|
import { Modal } from '../../components/Modals/Modal';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
import { useDeleteUser } from 'hooks/Network/Users';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
const DeleteProfileButton = ({ isDisabled }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const deleteUser = useDeleteUser();
|
||||||
|
const modalProps = useDisclosure();
|
||||||
|
|
||||||
|
const onDeleteClick = () =>
|
||||||
|
deleteUser.mutate(user?.id ?? '', {
|
||||||
|
onSuccess: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
logout();
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onOpen = () => {
|
||||||
|
deleteUser.reset();
|
||||||
|
modalProps.onOpen();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteButton isCompact isDisabled={isDisabled} onClick={onOpen} />
|
||||||
|
<Modal {...modalProps} title={t('profile.delete_account')}>
|
||||||
|
<Box>
|
||||||
|
{deleteUser.isSuccess ? (
|
||||||
|
<Center>
|
||||||
|
<Alert status="success">
|
||||||
|
<AlertIcon />
|
||||||
|
<AlertDescription>
|
||||||
|
{t('Your profile is now deleted, we will now log you out...')} <Spinner />
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Center>
|
||||||
|
{deleteUser.error ? (
|
||||||
|
<Alert status="error">
|
||||||
|
<AlertIcon />
|
||||||
|
<Box>
|
||||||
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{axios.isAxiosError(deleteUser.error) ? deleteUser.error.response?.data?.ErrorDescription : ''}
|
||||||
|
</AlertDescription>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert status="warning">
|
||||||
|
<AlertIcon />
|
||||||
|
<AlertDescription>{t('profile.delete_warning')}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
<Center my={8}>
|
||||||
|
<Button onClick={onDeleteClick} isLoading={deleteUser.isLoading} colorScheme="red">
|
||||||
|
{t('profile.delete_account_confirm')}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteProfileButton;
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { Box, Center, Flex, Heading, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
|
import { Box, Center, Flex, Heading, HStack, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
|
||||||
|
import axios from 'axios';
|
||||||
import { Form, Formik, FormikProps } from 'formik';
|
import { Form, Formik, FormikProps } from 'formik';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
import DeleteProfileButton from './DeleteButton';
|
||||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||||
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||||
import { StringField } from 'components/Form/Fields/StringField';
|
import { StringField } from 'components/Form/Fields/StringField';
|
||||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { testRegex } from 'helpers/formTests';
|
import { testRegex } from 'helpers/formTests';
|
||||||
import { useUpdateAccount } from 'hooks/Network/Account';
|
import { useUpdateAccount } from 'hooks/Network/Account';
|
||||||
|
import { UserRole } from 'hooks/Network/Users';
|
||||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||||
import { useFormModal } from 'hooks/useFormModal';
|
import { useFormModal } from 'hooks/useFormModal';
|
||||||
import { useFormRef } from 'hooks/useFormRef';
|
import { useFormRef } from 'hooks/useFormRef';
|
||||||
@@ -70,13 +74,16 @@ const GeneralInformationProfile = () => {
|
|||||||
<CardHeader mb={2}>
|
<CardHeader mb={2}>
|
||||||
<Heading size="md">{t('profile.your_profile')}</Heading>
|
<Heading size="md">{t('profile.your_profile')}</Heading>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<HStack>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
onClick={form.submitForm}
|
onClick={form.submitForm}
|
||||||
isLoading={form.isSubmitting}
|
isLoading={form.isSubmitting}
|
||||||
isDisabled={!form.isValid || !form.dirty}
|
isDisabled={!form.isValid || !form.dirty}
|
||||||
hidden={!isEditing}
|
hidden={!isEditing}
|
||||||
/>
|
/>
|
||||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} ml={2} />
|
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} />
|
||||||
|
<DeleteProfileButton isDisabled={isEditing} />
|
||||||
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody display="block">
|
<CardBody display="block">
|
||||||
{!user ? (
|
{!user ? (
|
||||||
@@ -89,6 +96,7 @@ const GeneralInformationProfile = () => {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
newPassword?: string;
|
newPassword?: string;
|
||||||
|
userRole: UserRole;
|
||||||
}>
|
}>
|
||||||
key={formKey}
|
key={formKey}
|
||||||
initialValues={
|
initialValues={
|
||||||
@@ -97,11 +105,13 @@ const GeneralInformationProfile = () => {
|
|||||||
description: user?.description ?? '',
|
description: user?.description ?? '',
|
||||||
firstName: user?.name.split(' ')[0] ?? '',
|
firstName: user?.name.split(' ')[0] ?? '',
|
||||||
lastName: user?.name.split(' ')[1] ?? '',
|
lastName: user?.name.split(' ')[1] ?? '',
|
||||||
|
userRole: user?.userRole,
|
||||||
} as {
|
} as {
|
||||||
description: string;
|
description: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
newPassword?: string;
|
newPassword?: string;
|
||||||
|
userRole: UserRole;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
innerRef={
|
innerRef={
|
||||||
@@ -111,17 +121,19 @@ const GeneralInformationProfile = () => {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
newPassword?: string;
|
newPassword?: string;
|
||||||
|
userRole: UserRole;
|
||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
validationSchema={FormSchema(t, { passRegex: passwordPattern })}
|
validationSchema={FormSchema(t, { passRegex: passwordPattern })}
|
||||||
onSubmit={async ({ description, firstName, lastName, newPassword }, { setSubmitting }) => {
|
onSubmit={async ({ description, firstName, lastName, newPassword, userRole }, { setSubmitting }) => {
|
||||||
await updateUser.mutateAsync(
|
await updateUser.mutateAsync(
|
||||||
{
|
{
|
||||||
id: user?.id,
|
id: user?.id,
|
||||||
description,
|
description,
|
||||||
name: `${firstName} ${lastName}`,
|
name: `${firstName} ${lastName}`,
|
||||||
currentPassword: newPassword,
|
currentPassword: newPassword,
|
||||||
|
userRole: user?.userRole === 'root' ? userRole : undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -139,13 +151,45 @@ const GeneralInformationProfile = () => {
|
|||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
toast({
|
||||||
|
id: 'account-update-error',
|
||||||
|
title: t('common.error'),
|
||||||
|
description: e.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
<Flex>
|
||||||
<StringField name="email" label={t('common.email')} isDisabled />
|
<StringField name="email" label={t('common.email')} isDisabled />
|
||||||
|
<Box w={8} />
|
||||||
|
<SelectField
|
||||||
|
name="userRole"
|
||||||
|
label={t('user.role')}
|
||||||
|
options={[
|
||||||
|
{ value: 'accounting', label: 'Accounting' },
|
||||||
|
{ value: 'admin', label: 'Admin' },
|
||||||
|
{ value: 'csr', label: 'CSR' },
|
||||||
|
{ value: 'installer', label: 'Installer' },
|
||||||
|
{ value: 'noc', label: 'NOC' },
|
||||||
|
{ value: 'root', label: 'Root' },
|
||||||
|
{ value: 'system', label: 'System' },
|
||||||
|
]}
|
||||||
|
isRequired
|
||||||
|
isDisabled={isSubmitting || !isEditing || user?.userRole !== 'root'}
|
||||||
|
w="max-content"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<Flex my={4}>
|
<Flex my={4}>
|
||||||
<StringField
|
<StringField
|
||||||
name="firstName"
|
name="firstName"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip, useToast } from '@chakra-ui/react';
|
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip, useToast } from '@chakra-ui/react';
|
||||||
import axios from 'axios';
|
|
||||||
import { Wrench } from 'phosphor-react';
|
import { Wrench } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useSendUserEmailValidation, useSuspendUser, useResetMfa, useResetPassword } from 'hooks/Network/Users';
|
import { useResetMfa, useResetPassword, useSendUserEmailValidation, useSuspendUser } from 'hooks/Network/Users';
|
||||||
import { useMutationResult } from 'hooks/useMutationResult';
|
import { useMutationResult } from 'hooks/useMutationResult';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -32,7 +32,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
if (axios.isAxiosError(e)) onError(e);
|
onError(e as AxiosError);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const handleResetMfaClick = () =>
|
const handleResetMfaClick = () =>
|
||||||
@@ -49,7 +49,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
if (axios.isAxiosError(e)) onError(e);
|
onError(e as AxiosError);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
if (axios.isAxiosError(e)) onError(e);
|
onError(e as AxiosError);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
|
||||||
import { Box, Flex, Link, useToast, SimpleGrid } from '@chakra-ui/react';
|
|
||||||
import { Formik, Form } from 'formik';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
|
||||||
import { StringField } from 'components/Form/Fields/StringField';
|
|
||||||
import { ToggleField } from 'components/Form/Fields/ToggleField';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
import { testRegex } from 'helpers/formTests';
|
|
||||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
createUser: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
refreshUsers: PropTypes.func.isRequired,
|
|
||||||
formRef: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateUserSchema = (t, { passRegex }) =>
|
|
||||||
Yup.object().shape({
|
|
||||||
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
|
||||||
name: Yup.string().required('Required'),
|
|
||||||
description: Yup.string(),
|
|
||||||
currentPassword: Yup.string()
|
|
||||||
.required(t('form.required'))
|
|
||||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex))
|
|
||||||
.default(''),
|
|
||||||
note: Yup.string(),
|
|
||||||
userRole: Yup.string(),
|
|
||||||
});
|
|
||||||
const CreateUserNonRootSchema = (t, { passRegex }) =>
|
|
||||||
Yup.object().shape({
|
|
||||||
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
|
||||||
name: Yup.string().required('Required'),
|
|
||||||
description: Yup.string(),
|
|
||||||
currentPassword: Yup.string()
|
|
||||||
.required(t('form.required'))
|
|
||||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex))
|
|
||||||
.default(''),
|
|
||||||
note: Yup.string(),
|
|
||||||
userRole: Yup.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const CreateUserForm = ({ isOpen, onClose, createUser, refreshUsers, formRef }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const toast = useToast();
|
|
||||||
const { user } = useAuth();
|
|
||||||
const [formKey, setFormKey] = useState(uuid());
|
|
||||||
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
|
||||||
|
|
||||||
const createParameters = ({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
email,
|
|
||||||
currentPassword,
|
|
||||||
note,
|
|
||||||
userRole,
|
|
||||||
emailValidation,
|
|
||||||
changePassword,
|
|
||||||
}) => {
|
|
||||||
if (userRole === 'root') {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
currentPassword,
|
|
||||||
userRole,
|
|
||||||
description: description.length > 0 ? description : undefined,
|
|
||||||
notes: note.length > 0 ? [{ note }] : undefined,
|
|
||||||
emailValidation,
|
|
||||||
changePassword,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
currentPassword,
|
|
||||||
userRole,
|
|
||||||
description: description.length > 0 ? description : undefined,
|
|
||||||
notes: note.length > 0 ? [{ note }] : undefined,
|
|
||||||
emailValidation,
|
|
||||||
changePassword,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFormKey(uuid());
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
innerRef={formRef}
|
|
||||||
key={formKey}
|
|
||||||
initialValues={{
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
email: '',
|
|
||||||
currentPassword: '',
|
|
||||||
note: '',
|
|
||||||
userRole: user.userRole === 'admin' ? 'csr' : user.userRole,
|
|
||||||
changePassword: true,
|
|
||||||
emailValidation: true,
|
|
||||||
}}
|
|
||||||
validationSchema={
|
|
||||||
user?.userRole === 'root'
|
|
||||||
? CreateUserSchema(t, { passRegex: passwordPattern })
|
|
||||||
: CreateUserNonRootSchema(t, { passRegex: passwordPattern })
|
|
||||||
}
|
|
||||||
onSubmit={(formData, { setSubmitting, resetForm }) =>
|
|
||||||
createUser.mutateAsync(createParameters(formData), {
|
|
||||||
onSuccess: () => {
|
|
||||||
setSubmitting(false);
|
|
||||||
resetForm();
|
|
||||||
toast({
|
|
||||||
id: 'user-creation-success',
|
|
||||||
title: t('common.success'),
|
|
||||||
description: t('crud.success_create_obj', {
|
|
||||||
obj: t('user.title'),
|
|
||||||
}),
|
|
||||||
status: 'success',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
refreshUsers();
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
toast({
|
|
||||||
id: uuid(),
|
|
||||||
title: t('common.error'),
|
|
||||||
description: t('crud.error_create_obj', {
|
|
||||||
obj: t('user.title'),
|
|
||||||
e: e?.response?.data?.ErrorDescription,
|
|
||||||
}),
|
|
||||||
status: 'error',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
setSubmitting(false);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ errors, touched }) => (
|
|
||||||
<Form>
|
|
||||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
|
||||||
<StringField name="email" label={t('common.email')} errors={errors} touched={touched} isRequired />
|
|
||||||
<StringField name="name" label={t('common.name')} errors={errors} touched={touched} isRequired />
|
|
||||||
<SelectField
|
|
||||||
name="userRole"
|
|
||||||
label={t('user.role')}
|
|
||||||
errors={errors}
|
|
||||||
touched={touched}
|
|
||||||
options={[
|
|
||||||
{ value: 'accounting', label: 'Accounting' },
|
|
||||||
{ value: 'admin', label: 'Admin' },
|
|
||||||
{ value: 'csr', label: 'CSR' },
|
|
||||||
{ value: 'installer', label: 'Installer' },
|
|
||||||
{ value: 'noc', label: 'NOC' },
|
|
||||||
{ value: 'root', label: 'Root' },
|
|
||||||
{ value: 'system', label: 'System' },
|
|
||||||
]}
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
<StringField
|
|
||||||
name="currentPassword"
|
|
||||||
label={t('user.password')}
|
|
||||||
errors={errors}
|
|
||||||
touched={touched}
|
|
||||||
isRequired
|
|
||||||
hideButton
|
|
||||||
/>
|
|
||||||
<ToggleField name="changePassword" label={t('users.change_password')} errors={errors} touched={touched} />
|
|
||||||
<ToggleField name="emailValidation" label={t('users.email_validation')} errors={errors} touched={touched} />
|
|
||||||
<StringField name="description" label={t('common.description')} errors={errors} touched={touched} />
|
|
||||||
<StringField name="note" label={t('common.note')} errors={errors} touched={touched} />
|
|
||||||
</SimpleGrid>
|
|
||||||
<Flex justifyContent="center" alignItems="center" maxW="100%" mt={4} mb={6}>
|
|
||||||
<Box w="100%">
|
|
||||||
<Link href={passwordPolicyLink} isExternal>
|
|
||||||
{t('login.password_policy')}
|
|
||||||
<ExternalLinkIcon mx="2px" />
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CreateUserForm.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default CreateUserForm;
|
|
||||||
197
src/pages/UsersPage/Table/CreateUserModal/Form.tsx
Normal file
197
src/pages/UsersPage/Table/CreateUserModal/Form.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
|
import { Box, Flex, Link, useToast, SimpleGrid } from '@chakra-ui/react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Formik, Form, FormikProps } from 'formik';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { SelectField } from '../../../../components/Form/Fields/SelectField';
|
||||||
|
import { StringField } from '../../../../components/Form/Fields/StringField';
|
||||||
|
import { ToggleField } from '../../../../components/Form/Fields/ToggleField';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
import { testRegex } from 'helpers/formTests';
|
||||||
|
import { useCreateUser } from 'hooks/Network/Users';
|
||||||
|
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||||
|
|
||||||
|
export type CreateUserFormValues = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
currentPassword: string;
|
||||||
|
note: string;
|
||||||
|
userRole: string;
|
||||||
|
emailValidation: boolean;
|
||||||
|
changePassword: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
formRef: React.Ref<FormikProps<CreateUserFormValues>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateUserForm = ({ isOpen, onClose, formRef }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [formKey, setFormKey] = useState(uuid());
|
||||||
|
const createUser = useCreateUser();
|
||||||
|
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
||||||
|
|
||||||
|
const CreateUserSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
||||||
|
name: Yup.string().required('Required'),
|
||||||
|
description: Yup.string(),
|
||||||
|
currentPassword: Yup.string()
|
||||||
|
.required(t('form.required'))
|
||||||
|
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern))
|
||||||
|
.default(''),
|
||||||
|
note: Yup.string(),
|
||||||
|
userRole: Yup.string(),
|
||||||
|
});
|
||||||
|
const CreateUserNonRootSchema = Yup.object().shape({
|
||||||
|
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
||||||
|
name: Yup.string().required('Required'),
|
||||||
|
description: Yup.string(),
|
||||||
|
currentPassword: Yup.string()
|
||||||
|
.required(t('form.required'))
|
||||||
|
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern))
|
||||||
|
.default(''),
|
||||||
|
note: Yup.string(),
|
||||||
|
userRole: Yup.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createParameters = ({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
email,
|
||||||
|
currentPassword,
|
||||||
|
note,
|
||||||
|
userRole,
|
||||||
|
emailValidation,
|
||||||
|
changePassword,
|
||||||
|
}: CreateUserFormValues) => {
|
||||||
|
if (userRole === 'root') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
currentPassword,
|
||||||
|
userRole,
|
||||||
|
description: description.length > 0 ? description : undefined,
|
||||||
|
notes: note.length > 0 ? [{ note }] : undefined,
|
||||||
|
emailValidation,
|
||||||
|
changePassword,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
currentPassword,
|
||||||
|
userRole,
|
||||||
|
description: description.length > 0 ? description : undefined,
|
||||||
|
notes: note.length > 0 ? [{ note }] : undefined,
|
||||||
|
emailValidation,
|
||||||
|
changePassword,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRole = () => {
|
||||||
|
if (user?.userRole === 'admin') return 'csr';
|
||||||
|
if (user) return user.userRole;
|
||||||
|
return 'csr';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormKey(uuid());
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
innerRef={formRef}
|
||||||
|
key={formKey}
|
||||||
|
initialValues={
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
email: '',
|
||||||
|
currentPassword: '',
|
||||||
|
note: '',
|
||||||
|
userRole: defaultRole(),
|
||||||
|
changePassword: true,
|
||||||
|
emailValidation: true,
|
||||||
|
} as CreateUserFormValues
|
||||||
|
}
|
||||||
|
validationSchema={user?.userRole === 'root' ? CreateUserSchema : CreateUserNonRootSchema}
|
||||||
|
onSubmit={(formData, { setSubmitting, resetForm }) =>
|
||||||
|
createUser.mutate(createParameters(formData), {
|
||||||
|
onSuccess: () => {
|
||||||
|
setSubmitting(false);
|
||||||
|
resetForm();
|
||||||
|
toast({
|
||||||
|
id: 'user-creation-success',
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('crud.success_create_obj', {
|
||||||
|
obj: t('user.title'),
|
||||||
|
}),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
if (axios.isAxiosError(e))
|
||||||
|
toast({
|
||||||
|
id: uuid(),
|
||||||
|
title: t('common.error'),
|
||||||
|
description: e?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||||
|
<StringField name="email" label={t('common.email')} isRequired />
|
||||||
|
<StringField name="name" label={t('common.name')} isRequired />
|
||||||
|
<SelectField
|
||||||
|
name="userRole"
|
||||||
|
label={t('user.role')}
|
||||||
|
options={[
|
||||||
|
{ value: 'accounting', label: 'Accounting' },
|
||||||
|
{ value: 'admin', label: 'Admin' },
|
||||||
|
{ value: 'csr', label: 'CSR' },
|
||||||
|
{ value: 'installer', label: 'Installer' },
|
||||||
|
{ value: 'noc', label: 'NOC' },
|
||||||
|
{ value: 'root', label: 'Root' },
|
||||||
|
{ value: 'system', label: 'System' },
|
||||||
|
]}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<StringField name="currentPassword" label={t('user.password')} isRequired hideButton />
|
||||||
|
<ToggleField name="changePassword" label={t('users.change_password')} />
|
||||||
|
<ToggleField name="emailValidation" label={t('users.email_validation')} />
|
||||||
|
<StringField name="description" label={t('common.description')} />
|
||||||
|
<StringField name="note" label={t('common.note')} />
|
||||||
|
</SimpleGrid>
|
||||||
|
<Flex justifyContent="center" alignItems="center" maxW="100%" mt={4} mb={6}>
|
||||||
|
<Box w="100%">
|
||||||
|
<Link href={passwordPolicyLink} isExternal>
|
||||||
|
{t('login.password_policy')}
|
||||||
|
<ExternalLinkIcon mx="2px" />
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateUserForm;
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { AddIcon } from '@chakra-ui/icons';
|
|
||||||
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import CreateUserForm from './Form';
|
|
||||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
|
||||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
|
||||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
|
||||||
import { ModalHeader } from 'components/Modals/GenericModal/ModalHeader';
|
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
import { useFormRef } from 'hooks/useFormRef';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
requirements: PropTypes.shape({
|
|
||||||
accessPolicy: PropTypes.string,
|
|
||||||
passwordPolicy: PropTypes.string,
|
|
||||||
}),
|
|
||||||
refreshUsers: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
requirements: {
|
|
||||||
accessPolicy: '',
|
|
||||||
passwordPolicy: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateUserModal = ({ requirements, refreshUsers }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { user } = useAuth();
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
|
||||||
const { form, formRef } = useFormRef();
|
|
||||||
const createUser = useMutation((newUser) =>
|
|
||||||
axiosSec.post(`user/0${newUser.emailValidation ? '?email_verification=true' : ''}`, newUser),
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
|
||||||
|
|
||||||
const closeCancelAndForm = () => {
|
|
||||||
closeConfirm();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
hidden={user?.userRole === 'CSR'}
|
|
||||||
alignItems="center"
|
|
||||||
colorScheme="blue"
|
|
||||||
rightIcon={<AddIcon />}
|
|
||||||
onClick={onOpen}
|
|
||||||
ml={2}
|
|
||||||
>
|
|
||||||
{t('crud.create')}
|
|
||||||
</Button>
|
|
||||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
|
||||||
<ModalHeader
|
|
||||||
title={t('crud.create_object', { obj: t('user.title') })}
|
|
||||||
right={
|
|
||||||
<>
|
|
||||||
<SaveButton
|
|
||||||
onClick={form.submitForm}
|
|
||||||
isLoading={form.isSubmitting}
|
|
||||||
isDisabled={!form.isValid || !form.dirty}
|
|
||||||
/>
|
|
||||||
<CloseButton ml={2} onClick={closeModal} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ModalBody>
|
|
||||||
<CreateUserForm
|
|
||||||
requirements={requirements}
|
|
||||||
createUser={createUser}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
refreshUsers={refreshUsers}
|
|
||||||
formRef={formRef}
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CreateUserModal.propTypes = propTypes;
|
|
||||||
CreateUserModal.defaultProps = defaultProps;
|
|
||||||
|
|
||||||
export default CreateUserModal;
|
|
||||||
57
src/pages/UsersPage/Table/CreateUserModal/index.tsx
Normal file
57
src/pages/UsersPage/Table/CreateUserModal/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
|
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||||
|
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||||
|
import { Modal } from '../../../../components/Modals/Modal';
|
||||||
|
import CreateUserForm, { CreateUserFormValues } from './Form';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
import { useFormRef } from 'hooks/useFormRef';
|
||||||
|
|
||||||
|
const CreateUserModal = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||||
|
const { form, formRef } = useFormRef<CreateUserFormValues>();
|
||||||
|
|
||||||
|
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||||
|
|
||||||
|
const closeCancelAndForm = () => {
|
||||||
|
closeConfirm();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
hidden={user?.userRole === 'csr'}
|
||||||
|
alignItems="center"
|
||||||
|
colorScheme="blue"
|
||||||
|
rightIcon={<AddIcon />}
|
||||||
|
onClick={onOpen}
|
||||||
|
ml={2}
|
||||||
|
>
|
||||||
|
{t('crud.create')}
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeModal}
|
||||||
|
title={t('crud.create_object', { obj: t('user.title') })}
|
||||||
|
topRightButtons={
|
||||||
|
<SaveButton
|
||||||
|
onClick={form.submitForm}
|
||||||
|
isLoading={form.isSubmitting}
|
||||||
|
isDisabled={!form.isValid || !form.dirty}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CreateUserForm isOpen={isOpen} onClose={onClose} formRef={formRef} />
|
||||||
|
</Modal>
|
||||||
|
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateUserModal;
|
||||||
@@ -1,64 +1,65 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
import { Box, Flex, Link, useToast, Tabs, TabList, TabPanels, TabPanel, Tab, SimpleGrid } from '@chakra-ui/react';
|
import { Box, Flex, Link, useToast, Tabs, TabList, TabPanels, TabPanel, Tab, SimpleGrid } from '@chakra-ui/react';
|
||||||
import { Formik, Form } from 'formik';
|
import axios from 'axios';
|
||||||
import PropTypes from 'prop-types';
|
import { Formik, Form, FormikProps } from 'formik';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { NotesField } from 'components/Form/Fields/NotesField';
|
import { NotesField } from '../../../../components/Form/Fields/NotesField';
|
||||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
import { SelectField } from '../../../../components/Form/Fields/SelectField';
|
||||||
import { StringField } from 'components/Form/Fields/StringField';
|
import { StringField } from '../../../../components/Form/Fields/StringField';
|
||||||
import { ToggleField } from 'components/Form/Fields/ToggleField';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { testObjectName, testRegex } from 'helpers/formTests';
|
import { testObjectName, testRegex } from 'helpers/formTests';
|
||||||
|
import { User, useUpdateUser } from 'hooks/Network/Users';
|
||||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||||
|
|
||||||
const UpdateUserSchema = (t, { passRegex }) =>
|
type Props = {
|
||||||
Yup.object().shape({
|
editing: boolean;
|
||||||
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
|
isOpen: boolean;
|
||||||
currentPassword: Yup.string()
|
onClose: () => void;
|
||||||
.notRequired()
|
selectedUser: User;
|
||||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex)),
|
formRef: React.Ref<FormikProps<User>>;
|
||||||
description: Yup.string(),
|
|
||||||
mfa: Yup.string(),
|
|
||||||
phoneNumber: Yup.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
editing: PropTypes.bool.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
updateUser: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
refreshUsers: PropTypes.func.isRequired,
|
|
||||||
userToUpdate: PropTypes.shape({
|
|
||||||
email: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
description: PropTypes.string.isRequired,
|
|
||||||
currentPassword: PropTypes.string.isRequired,
|
|
||||||
userRole: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
formRef: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, userToUpdate, formRef }) => {
|
const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [formKey, setFormKey] = useState(uuid());
|
const [formKey, setFormKey] = useState(uuid());
|
||||||
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
||||||
|
const updateUser = useUpdateUser();
|
||||||
|
|
||||||
|
const UpdateUserSchema = () =>
|
||||||
|
Yup.object().shape({
|
||||||
|
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
|
||||||
|
currentPassword: Yup.string()
|
||||||
|
.notRequired()
|
||||||
|
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern)),
|
||||||
|
description: Yup.string(),
|
||||||
|
mfa: Yup.string(),
|
||||||
|
phoneNumber: Yup.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const formIsDisabled = () => {
|
const formIsDisabled = () => {
|
||||||
if (!editing) return true;
|
if (!editing) return true;
|
||||||
if (user?.userRole === 'root') return false;
|
if (user?.userRole === 'root') return false;
|
||||||
if (user?.userRole === 'partner') return false;
|
if (user?.userRole === 'partner') return false;
|
||||||
if (user?.userRole === 'admin') {
|
if (user?.userRole === 'admin') {
|
||||||
if (userToUpdate.userRole === 'partner' || userToUpdate.userRole === 'admin') return true;
|
if (selectedUser.userRole === 'root' || selectedUser.userRole === 'partner' || selectedUser.userRole === 'admin')
|
||||||
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canEditRole = () => {
|
||||||
|
if (selectedUser.userRole === 'root') return false;
|
||||||
|
if (user?.userRole === 'root') return true;
|
||||||
|
if (user?.userRole === 'admin' && selectedUser.userRole !== 'admin') return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormKey(uuid());
|
setFormKey(uuid());
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@@ -68,18 +69,15 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
|||||||
innerRef={formRef}
|
innerRef={formRef}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
key={formKey}
|
key={formKey}
|
||||||
initialValues={userToUpdate}
|
initialValues={selectedUser}
|
||||||
validationSchema={UpdateUserSchema(t, { passRegex: passwordPattern })}
|
validationSchema={UpdateUserSchema}
|
||||||
onSubmit={(
|
onSubmit={({ name, description, currentPassword, userRole, notes }, { setSubmitting, resetForm }) =>
|
||||||
{ name, description, currentPassword, userRole, notes, changePassword },
|
|
||||||
{ setSubmitting, resetForm },
|
|
||||||
) =>
|
|
||||||
updateUser.mutateAsync(
|
updateUser.mutateAsync(
|
||||||
{
|
{
|
||||||
|
id: selectedUser.id,
|
||||||
name,
|
name,
|
||||||
currentPassword: currentPassword.length > 0 ? currentPassword : undefined,
|
currentPassword: currentPassword.length > 0 ? currentPassword : undefined,
|
||||||
userRole,
|
userRole,
|
||||||
changePassword,
|
|
||||||
description,
|
description,
|
||||||
notes: notes.filter((note) => note.isNew),
|
notes: notes.filter((note) => note.isNew),
|
||||||
},
|
},
|
||||||
@@ -98,23 +96,20 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
|||||||
isClosable: true,
|
isClosable: true,
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
});
|
});
|
||||||
refreshUsers();
|
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
if (axios.isAxiosError(e))
|
||||||
toast({
|
toast({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
description: t('crud.error_update_obj', {
|
description: e?.response?.data?.ErrorDescription,
|
||||||
obj: t('user.title'),
|
|
||||||
e: e?.response?.data?.ErrorDescription,
|
|
||||||
}),
|
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
});
|
});
|
||||||
setSubmitting(false);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -144,10 +139,9 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
|||||||
{ value: 'system', label: 'System' },
|
{ value: 'system', label: 'System' },
|
||||||
]}
|
]}
|
||||||
isRequired
|
isRequired
|
||||||
isDisabled
|
isDisabled={!canEditRole() || formIsDisabled()}
|
||||||
/>
|
/>
|
||||||
<StringField name="name" label={t('common.name')} isDisabled={formIsDisabled()} isRequired />
|
<StringField name="name" label={t('common.name')} isDisabled={formIsDisabled()} isRequired />
|
||||||
<ToggleField name="changePassword" label={t('users.change_password')} isDisabled={formIsDisabled()} />
|
|
||||||
<StringField
|
<StringField
|
||||||
name="currentPassword"
|
name="currentPassword"
|
||||||
label={t('user.password')}
|
label={t('user.password')}
|
||||||
@@ -176,6 +170,4 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UpdateUserForm.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default UpdateUserForm;
|
export default UpdateUserForm;
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalBody,
|
|
||||||
useToast,
|
|
||||||
Spinner,
|
|
||||||
Center,
|
|
||||||
useDisclosure,
|
|
||||||
useBoolean,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import UpdateUserForm from './Form';
|
|
||||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
|
||||||
import { EditButton } from 'components/Buttons/EditButton';
|
|
||||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
|
||||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
|
||||||
import { ModalHeader } from 'components/Modals/GenericModal/ModalHeader';
|
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
|
||||||
import { useGetUser } from 'hooks/Network/Users';
|
|
||||||
import { useFormRef } from 'hooks/useFormRef';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
userId: PropTypes.string,
|
|
||||||
requirements: PropTypes.shape({
|
|
||||||
accessPolicy: PropTypes.string,
|
|
||||||
passwordPolicy: PropTypes.string,
|
|
||||||
}),
|
|
||||||
refreshUsers: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
userId: '',
|
|
||||||
requirements: {
|
|
||||||
accessPolicy: '',
|
|
||||||
passwordPolicy: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditUserModal = ({ isOpen, onClose, userId, requirements, refreshUsers }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [editing, setEditing] = useBoolean();
|
|
||||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
|
||||||
const toast = useToast();
|
|
||||||
const { form, formRef } = useFormRef();
|
|
||||||
const canFetchUser = userId !== '' && isOpen;
|
|
||||||
const { data: user, isLoading } = useGetUser({ t, toast, id: userId, enabled: canFetchUser });
|
|
||||||
const createUser = useMutation((userInfo) => axiosSec.put(`user/${userId}`, userInfo));
|
|
||||||
|
|
||||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
|
||||||
|
|
||||||
const closeCancelAndForm = () => {
|
|
||||||
closeConfirm();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) setEditing.off();
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
|
||||||
<ModalHeader
|
|
||||||
title={t('crud.edit_obj', { obj: t('user.title') })}
|
|
||||||
right={
|
|
||||||
<>
|
|
||||||
<SaveButton
|
|
||||||
onClick={form.submitForm}
|
|
||||||
isLoading={form.isSubmitting}
|
|
||||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
|
||||||
/>
|
|
||||||
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
|
|
||||||
<CloseButton ml={2} onClick={closeModal} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ModalBody>
|
|
||||||
{!isLoading && user ? (
|
|
||||||
<UpdateUserForm
|
|
||||||
editing={editing}
|
|
||||||
userToUpdate={user}
|
|
||||||
requirements={requirements}
|
|
||||||
updateUser={createUser}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
refreshUsers={refreshUsers}
|
|
||||||
formRef={formRef}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center>
|
|
||||||
<Spinner />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EditUserModal.propTypes = propTypes;
|
|
||||||
EditUserModal.defaultProps = defaultProps;
|
|
||||||
|
|
||||||
export default EditUserModal;
|
|
||||||
68
src/pages/UsersPage/Table/EditUserModal/index.tsx
Normal file
68
src/pages/UsersPage/Table/EditUserModal/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { EditButton } from '../../../../components/Buttons/EditButton';
|
||||||
|
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||||
|
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||||
|
import { Modal } from '../../../../components/Modals/Modal';
|
||||||
|
import UpdateUserForm from './Form';
|
||||||
|
import { useGetUser, User } from 'hooks/Network/Users';
|
||||||
|
import { useFormRef } from 'hooks/useFormRef';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
userId?: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [editing, setEditing] = useBoolean();
|
||||||
|
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||||
|
const { form, formRef } = useFormRef<User>();
|
||||||
|
const canFetchUser = userId !== '' && isOpen;
|
||||||
|
const { data: user, isFetching } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
||||||
|
|
||||||
|
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||||
|
|
||||||
|
const closeCancelAndForm = () => {
|
||||||
|
closeConfirm();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) setEditing.off();
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeModal}
|
||||||
|
title={t('crud.edit_obj', { obj: t('user.title') })}
|
||||||
|
topRightButtons={
|
||||||
|
<>
|
||||||
|
<SaveButton
|
||||||
|
onClick={form.submitForm}
|
||||||
|
isLoading={form.isSubmitting}
|
||||||
|
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||||
|
/>
|
||||||
|
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!isFetching && user ? (
|
||||||
|
<UpdateUserForm editing={editing} selectedUser={user} isOpen={isOpen} onClose={onClose} formRef={formRef} />
|
||||||
|
) : (
|
||||||
|
<Center>
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUserModal;
|
||||||
@@ -17,37 +17,29 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import axios from 'axios';
|
||||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import ActionsDropdown from './ActionsDropdown';
|
import ActionsDropdown from './ActionsDropdown';
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
import { useDeleteUser, User } from 'hooks/Network/Users';
|
||||||
|
|
||||||
const deleteUserApi = async (userId) => axiosSec.delete(`/user/${userId}`).then(() => true);
|
type Props = {
|
||||||
|
user: User;
|
||||||
const propTypes = {
|
openEdit: (user: User) => void;
|
||||||
cell: PropTypes.shape({
|
refreshTable: () => void;
|
||||||
original: PropTypes.shape({
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
suspended: PropTypes.bool.isRequired,
|
|
||||||
waitingForEmailCheck: PropTypes.bool.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
refreshTable: PropTypes.func.isRequired,
|
|
||||||
openEdit: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
const UserActions = ({ user, openEdit, refreshTable }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const deleteUser = useMutation(() => deleteUserApi(user.id), {
|
const deleteUser = useDeleteUser();
|
||||||
|
|
||||||
|
const handleDeleteClick = () =>
|
||||||
|
deleteUser.mutate(user.id, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
onClose();
|
onClose();
|
||||||
refreshTable();
|
|
||||||
toast({
|
toast({
|
||||||
id: `user-delete-success${uuid()}`,
|
id: `user-delete-success${uuid()}`,
|
||||||
title: t('common.success'),
|
title: t('common.success'),
|
||||||
@@ -61,13 +53,11 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e))
|
||||||
toast({
|
toast({
|
||||||
id: 'user-delete-error',
|
id: 'user-delete-error',
|
||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
description: t('crud.error_delete_obj', {
|
description: e?.response?.data?.ErrorDescription,
|
||||||
obj: user.name,
|
|
||||||
e: e?.response?.data?.ErrorDescription,
|
|
||||||
}),
|
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@@ -76,8 +66,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDeleteClick = () => deleteUser.mutateAsync();
|
const handleEditClick = () => openEdit(user);
|
||||||
const handleEditClick = () => openEdit(user.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex>
|
<Flex>
|
||||||
@@ -85,7 +74,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
|||||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||||
<Box>
|
<Box>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -97,10 +86,10 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
|||||||
<PopoverFooter>
|
<PopoverFooter>
|
||||||
<Center>
|
<Center>
|
||||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||||
Cancel
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteUser.isLoading}>
|
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteUser.isLoading}>
|
||||||
Yes
|
{t('common.yes')}
|
||||||
</Button>
|
</Button>
|
||||||
</Center>
|
</Center>
|
||||||
</PopoverFooter>
|
</PopoverFooter>
|
||||||
@@ -114,6 +103,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
|||||||
/>
|
/>
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
aria-label={t('common.view_details')}
|
||||||
ml={2}
|
ml={2}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
icon={<MagnifyingGlass size={20} />}
|
icon={<MagnifyingGlass size={20} />}
|
||||||
@@ -125,6 +115,4 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UserActions.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default UserActions;
|
export default UserActions;
|
||||||
@@ -1,67 +1,55 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Avatar, Box, Button, Flex, Heading, useDisclosure, useToast } from '@chakra-ui/react';
|
import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||||
import { ArrowsClockwise } from 'phosphor-react';
|
import { ArrowsClockwise } from 'phosphor-react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
|
||||||
|
import { DataTable } from '../../../components/DataTables/DataTable';
|
||||||
|
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
|
||||||
import CreateUserModal from './CreateUserModal';
|
import CreateUserModal from './CreateUserModal';
|
||||||
import EditUserModal from './EditUserModal';
|
import EditUserModal from './EditUserModal';
|
||||||
import UserActions from './UserActions';
|
import UserActions from './UserActions';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
|
|
||||||
import { DataTable } from 'components/DataTables/DataTable';
|
|
||||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { useGetRequirements } from 'hooks/Network/Requirements';
|
|
||||||
import { useGetUsers } from 'hooks/Network/Users';
|
import { useGetUsers } from 'hooks/Network/Users';
|
||||||
|
import { Column } from 'models/Table';
|
||||||
|
import { User } from 'models/User';
|
||||||
|
|
||||||
const propTypes = {
|
const UserTable = () => {
|
||||||
title: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
title: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserTable = ({ title }) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [usersWithAvatars, setUsersWithAvatars] = useState([]);
|
|
||||||
const { data: requirements } = useGetRequirements();
|
|
||||||
const [editId, setEditId] = useState('');
|
const [editId, setEditId] = useState('');
|
||||||
const [hiddenColumns, setHiddenColumns] = useState([]);
|
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||||
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||||
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers({ t, toast, setUsersWithAvatars });
|
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
|
||||||
|
|
||||||
const openEditModal = (userId) => {
|
const openEditModal = (editUser: User) => {
|
||||||
setEditId(userId);
|
setEditId(editUser.id);
|
||||||
openEdit();
|
openEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const memoizedActions = useCallback(
|
const memoizedActions = useCallback(
|
||||||
(cell) => <UserActions cell={cell.row} refreshTable={refreshUsers} key={uuid()} openEdit={openEditModal} />,
|
(userActions: User) => (
|
||||||
|
<UserActions user={userActions} refreshTable={refreshUsers} key={uuid()} openEdit={openEditModal} />
|
||||||
|
),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const memoizedDate = useCallback((cell) => <FormattedDate date={cell.row.values.lastLogin} key={uuid()} />, []);
|
const memoizedDate = useCallback((date: number) => <FormattedDate date={date} key={uuid()} />, []);
|
||||||
|
|
||||||
const memoizedAvatar = useCallback(
|
const memoizedAvatar = useCallback((name: string, avatar: string) => <Avatar name={name} src={avatar} />, []);
|
||||||
(cell) => <Avatar name={cell.row.values.name} src={cell.row.original.avatar} />,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Columns array. This array contains your table headings and accessors which maps keys from data array
|
// Columns array. This array contains your table headings and accessors which maps keys from data array
|
||||||
const columns = React.useMemo(() => {
|
const columns = React.useMemo(() => {
|
||||||
const baseColumns = [
|
const baseColumns: Column<User>[] = [
|
||||||
{
|
{
|
||||||
id: 'avatar',
|
id: 'avatar',
|
||||||
Header: t('account.avatar'),
|
Header: t('account.avatar'),
|
||||||
Footer: '',
|
Footer: '',
|
||||||
accessor: 'avatar',
|
accessor: 'avatar',
|
||||||
customWidth: '32px',
|
customWidth: '32px',
|
||||||
Cell: ({ cell }) => memoizedAvatar(cell),
|
Cell: ({ cell }) => memoizedAvatar(cell.row.original.name, cell.row.original.avatar),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
alwaysShow: true,
|
alwaysShow: true,
|
||||||
},
|
},
|
||||||
@@ -97,7 +85,7 @@ const UserTable = ({ title }) => {
|
|||||||
Header: t('users.last_login'),
|
Header: t('users.last_login'),
|
||||||
Footer: '',
|
Footer: '',
|
||||||
accessor: 'lastLogin',
|
accessor: 'lastLogin',
|
||||||
Cell: ({ cell }) => memoizedDate(cell, 'lastLogin'),
|
Cell: ({ cell }) => memoizedDate(cell.row.original.lastLogin),
|
||||||
customMinWidth: '150px',
|
customMinWidth: '150px',
|
||||||
customWidth: '150px',
|
customWidth: '150px',
|
||||||
},
|
},
|
||||||
@@ -116,7 +104,7 @@ const UserTable = ({ title }) => {
|
|||||||
Footer: '',
|
Footer: '',
|
||||||
accessor: 'Id',
|
accessor: 'Id',
|
||||||
customWidth: '80px',
|
customWidth: '80px',
|
||||||
Cell: ({ cell }) => memoizedActions(cell),
|
Cell: ({ cell }) => memoizedActions(cell.row.original),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
alwaysShow: true,
|
alwaysShow: true,
|
||||||
});
|
});
|
||||||
@@ -124,30 +112,22 @@ const UserTable = ({ title }) => {
|
|||||||
return baseColumns;
|
return baseColumns;
|
||||||
}, [t, user]);
|
}, [t, user]);
|
||||||
|
|
||||||
const showUsers = () => {
|
|
||||||
if (usersWithAvatars.length > 0) return usersWithAvatars;
|
|
||||||
return users ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader mb="10px">
|
<CardHeader mb="10px">
|
||||||
<Box>
|
|
||||||
<Heading size="md">{title}</Heading>
|
|
||||||
</Box>
|
|
||||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||||
<Box ms="auto">
|
<Box ms="auto">
|
||||||
<ColumnPicker
|
<ColumnPicker
|
||||||
columns={columns}
|
columns={columns as Column<unknown>[]}
|
||||||
hiddenColumns={hiddenColumns}
|
hiddenColumns={hiddenColumns}
|
||||||
setHiddenColumns={setHiddenColumns}
|
setHiddenColumns={setHiddenColumns}
|
||||||
preference="provisioning.userTable.hiddenColumns"
|
preference="provisioning.userTable.hiddenColumns"
|
||||||
/>
|
/>
|
||||||
<CreateUserModal requirements={requirements} refreshUsers={refreshUsers} />
|
<CreateUserModal />
|
||||||
<Button
|
<Button
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
onClick={refreshUsers}
|
onClick={() => refreshUsers()}
|
||||||
rightIcon={<ArrowsClockwise />}
|
rightIcon={<ArrowsClockwise />}
|
||||||
ml={2}
|
ml={2}
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
@@ -160,27 +140,20 @@ const UserTable = ({ title }) => {
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<Box overflowX="auto" w="100%">
|
<Box overflowX="auto" w="100%">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns as Column<object>[]}
|
||||||
data={showUsers()}
|
data={users?.filter((curr) => curr.email !== user?.email) ?? []}
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
obj={t('users.title')}
|
obj={t('users.title')}
|
||||||
|
sortBy={[{ id: 'email', desc: false }]}
|
||||||
hiddenColumns={hiddenColumns}
|
hiddenColumns={hiddenColumns}
|
||||||
fullScreen
|
fullScreen
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<EditUserModal
|
<EditUserModal isOpen={editOpen} onClose={closeEdit} userId={editId} />
|
||||||
isOpen={editOpen}
|
|
||||||
onClose={closeEdit}
|
|
||||||
userId={editId}
|
|
||||||
requirements={requirements}
|
|
||||||
refreshUsers={refreshUsers}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UserTable.propTypes = propTypes;
|
|
||||||
UserTable.defaultProps = defaultProps;
|
|
||||||
export default UserTable;
|
export default UserTable;
|
||||||
Reference in New Issue
Block a user