mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-30 01:42:19 +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",
|
||||
"version": "2.8.0(31)",
|
||||
"version": "2.8.0(32)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.8.0(31)",
|
||||
"version": "2.8.0(32)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.8.0(31)",
|
||||
"version": "2.8.0(32)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { User } from '../../models/User';
|
||||
import { UserRole } from './Users';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Note } from 'models/Note';
|
||||
@@ -141,6 +142,7 @@ export const useUpdateAccount = ({ user }: { user?: User }) => {
|
||||
};
|
||||
mobiles?: { number: string }[];
|
||||
};
|
||||
userRole?: UserRole;
|
||||
notes?: Note[];
|
||||
}) => axiosSec.put(`user/${user?.id ?? userInfo?.id}`, userInfo),
|
||||
{
|
||||
|
||||
@@ -1,9 +1,62 @@
|
||||
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 { axiosSec } from 'constants/axiosInstances';
|
||||
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 promises = userList.map(async (user) => {
|
||||
@@ -18,28 +71,30 @@ const getAvatarPromises = (userList: User[]) => {
|
||||
return promises;
|
||||
};
|
||||
|
||||
export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (users: unknown) => void }) => {
|
||||
const getUsers = async () => {
|
||||
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
|
||||
|
||||
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
||||
results.map((response) => {
|
||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||
const base64 = btoa(
|
||||
// @ts-ignore
|
||||
new Uint8Array(response.value.data).reduce((respData, byte) => respData + String.fromCharCode(byte), ''),
|
||||
);
|
||||
return `data:;base64,${base64}`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
);
|
||||
|
||||
return users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] })) as User[];
|
||||
};
|
||||
|
||||
export const useGetUsers = () => {
|
||||
const { t } = useTranslation();
|
||||
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) =>
|
||||
results.map((response) => {
|
||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||
const base64 = btoa(
|
||||
// @ts-ignore
|
||||
new Uint8Array(response.value.data).reduce((respData, byte) => respData + String.fromCharCode(byte), ''),
|
||||
);
|
||||
return `data:;base64,${base64}`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
);
|
||||
|
||||
const newUsers = users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] }));
|
||||
setUsersWithAvatars(newUsers);
|
||||
},
|
||||
return useQuery(['users'], getUsers, {
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('users-fetching-error'))
|
||||
toast({
|
||||
@@ -62,24 +117,28 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['get-user', id], () => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data), {
|
||||
enabled,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('user-fetching-error'))
|
||||
toast({
|
||||
id: 'user-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('users.one'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
return useQuery(
|
||||
['get-user', id],
|
||||
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
||||
{
|
||||
enabled,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('user-fetching-error'))
|
||||
toast({
|
||||
id: 'user-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('users.one'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
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 }) =>
|
||||
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 { 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>({
|
||||
submitForm: () => {},
|
||||
isSubmitting: false,
|
||||
@@ -10,7 +11,7 @@ export const useFormRef = () => {
|
||||
dirty: false,
|
||||
});
|
||||
const formRef = useCallback(
|
||||
(node: FormikProps<Record<string, unknown>> | undefined) => {
|
||||
(node: FormikProps<Type>) => {
|
||||
if (
|
||||
node &&
|
||||
(form.submitForm !== node.submitForm ||
|
||||
@@ -22,7 +23,7 @@ export const useFormRef = () => {
|
||||
}
|
||||
},
|
||||
[form],
|
||||
) as Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
) as Ref<FormikProps<Type>>;
|
||||
|
||||
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';
|
||||
|
||||
|
||||
export type UserRole =
|
||||
| 'root'
|
||||
| 'admin'
|
||||
@@ -11,13 +12,31 @@ export type UserRole =
|
||||
| 'noc'
|
||||
| 'accounting';
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
export type User = {
|
||||
avatar: string;
|
||||
blackListed: boolean;
|
||||
creationDate: number;
|
||||
currentLoginURI: string;
|
||||
currentPassword: string;
|
||||
description: string;
|
||||
currentPassword?: string;
|
||||
id: 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;
|
||||
@@ -27,6 +46,9 @@ export interface User {
|
||||
};
|
||||
mobiles: { number: string }[];
|
||||
};
|
||||
suspended: boolean;
|
||||
notes: Note[];
|
||||
}
|
||||
validated: boolean;
|
||||
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 { 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 { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import DeleteProfileButton from './DeleteButton';
|
||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { testRegex } from 'helpers/formTests';
|
||||
import { useUpdateAccount } from 'hooks/Network/Account';
|
||||
import { UserRole } from 'hooks/Network/Users';
|
||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||
import { useFormModal } from 'hooks/useFormModal';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
@@ -70,13 +74,16 @@ const GeneralInformationProfile = () => {
|
||||
<CardHeader mb={2}>
|
||||
<Heading size="md">{t('profile.your_profile')}</Heading>
|
||||
<Spacer />
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} ml={2} />
|
||||
<HStack>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} />
|
||||
<DeleteProfileButton isDisabled={isEditing} />
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody display="block">
|
||||
{!user ? (
|
||||
@@ -89,6 +96,7 @@ const GeneralInformationProfile = () => {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
newPassword?: string;
|
||||
userRole: UserRole;
|
||||
}>
|
||||
key={formKey}
|
||||
initialValues={
|
||||
@@ -97,11 +105,13 @@ const GeneralInformationProfile = () => {
|
||||
description: user?.description ?? '',
|
||||
firstName: user?.name.split(' ')[0] ?? '',
|
||||
lastName: user?.name.split(' ')[1] ?? '',
|
||||
userRole: user?.userRole,
|
||||
} as {
|
||||
description: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
newPassword?: string;
|
||||
userRole: UserRole;
|
||||
}
|
||||
}
|
||||
innerRef={
|
||||
@@ -111,17 +121,19 @@ const GeneralInformationProfile = () => {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
newPassword?: string;
|
||||
userRole: UserRole;
|
||||
}>
|
||||
>
|
||||
}
|
||||
validationSchema={FormSchema(t, { passRegex: passwordPattern })}
|
||||
onSubmit={async ({ description, firstName, lastName, newPassword }, { setSubmitting }) => {
|
||||
onSubmit={async ({ description, firstName, lastName, newPassword, userRole }, { setSubmitting }) => {
|
||||
await updateUser.mutateAsync(
|
||||
{
|
||||
id: user?.id,
|
||||
description,
|
||||
name: `${firstName} ${lastName}`,
|
||||
currentPassword: newPassword,
|
||||
userRole: user?.userRole === 'root' ? userRole : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -139,13 +151,45 @@ const GeneralInformationProfile = () => {
|
||||
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 }) => (
|
||||
<Form>
|
||||
<StringField name="email" label={t('common.email')} isDisabled />
|
||||
<Flex>
|
||||
<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}>
|
||||
<StringField
|
||||
name="firstName"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { Wrench } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { AxiosError } from 'models/Axios';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -32,7 +32,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
onSuccess();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
const handleResetMfaClick = () =>
|
||||
@@ -49,7 +49,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
});
|
||||
},
|
||||
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) => {
|
||||
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 { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, Link, useToast, Tabs, TabList, TabPanels, TabPanel, Tab, SimpleGrid } from '@chakra-ui/react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
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 { NotesField } from 'components/Form/Fields/NotesField';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { ToggleField } from 'components/Form/Fields/ToggleField';
|
||||
import { NotesField } from '../../../../components/Form/Fields/NotesField';
|
||||
import { SelectField } from '../../../../components/Form/Fields/SelectField';
|
||||
import { StringField } from '../../../../components/Form/Fields/StringField';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { testObjectName, testRegex } from 'helpers/formTests';
|
||||
import { User, useUpdateUser } from 'hooks/Network/Users';
|
||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||
|
||||
const UpdateUserSchema = (t, { passRegex }) =>
|
||||
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, passRegex)),
|
||||
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,
|
||||
type Props = {
|
||||
editing: boolean;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedUser: User;
|
||||
formRef: React.Ref<FormikProps<User>>;
|
||||
};
|
||||
|
||||
const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, userToUpdate, formRef }) => {
|
||||
const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [formKey, setFormKey] = useState(uuid());
|
||||
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 = () => {
|
||||
if (!editing) return true;
|
||||
if (user?.userRole === 'root') return false;
|
||||
if (user?.userRole === 'partner') return false;
|
||||
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 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(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isOpen]);
|
||||
@@ -68,18 +69,15 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
innerRef={formRef}
|
||||
enableReinitialize
|
||||
key={formKey}
|
||||
initialValues={userToUpdate}
|
||||
validationSchema={UpdateUserSchema(t, { passRegex: passwordPattern })}
|
||||
onSubmit={(
|
||||
{ name, description, currentPassword, userRole, notes, changePassword },
|
||||
{ setSubmitting, resetForm },
|
||||
) =>
|
||||
initialValues={selectedUser}
|
||||
validationSchema={UpdateUserSchema}
|
||||
onSubmit={({ name, description, currentPassword, userRole, notes }, { setSubmitting, resetForm }) =>
|
||||
updateUser.mutateAsync(
|
||||
{
|
||||
id: selectedUser.id,
|
||||
name,
|
||||
currentPassword: currentPassword.length > 0 ? currentPassword : undefined,
|
||||
userRole,
|
||||
changePassword,
|
||||
description,
|
||||
notes: notes.filter((note) => note.isNew),
|
||||
},
|
||||
@@ -98,23 +96,20 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
refreshUsers();
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_update_obj', {
|
||||
obj: t('user.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
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',
|
||||
});
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -144,10 +139,9 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
{ value: 'system', label: 'System' },
|
||||
]}
|
||||
isRequired
|
||||
isDisabled
|
||||
isDisabled={!canEditRole() || formIsDisabled()}
|
||||
/>
|
||||
<StringField name="name" label={t('common.name')} isDisabled={formIsDisabled()} isRequired />
|
||||
<ToggleField name="changePassword" label={t('users.change_password')} isDisabled={formIsDisabled()} />
|
||||
<StringField
|
||||
name="currentPassword"
|
||||
label={t('user.password')}
|
||||
@@ -176,6 +170,4 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
);
|
||||
};
|
||||
|
||||
UpdateUserForm.propTypes = propTypes;
|
||||
|
||||
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,67 +17,56 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
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);
|
||||
|
||||
const propTypes = {
|
||||
cell: PropTypes.shape({
|
||||
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,
|
||||
type Props = {
|
||||
user: User;
|
||||
openEdit: (user: User) => void;
|
||||
refreshTable: () => void;
|
||||
};
|
||||
|
||||
const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
const UserActions = ({ user, openEdit, refreshTable }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const deleteUser = useMutation(() => deleteUserApi(user.id), {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
refreshTable();
|
||||
toast({
|
||||
id: `user-delete-success${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_delete_obj', {
|
||||
obj: user.name,
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: 'user-delete-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_delete_obj', {
|
||||
obj: user.name,
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
const deleteUser = useDeleteUser();
|
||||
|
||||
const handleDeleteClick = () => deleteUser.mutateAsync();
|
||||
const handleEditClick = () => openEdit(user.id);
|
||||
const handleDeleteClick = () =>
|
||||
deleteUser.mutate(user.id, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
toast({
|
||||
id: `user-delete-success${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_delete_obj', {
|
||||
obj: user.name,
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e))
|
||||
toast({
|
||||
id: 'user-delete-error',
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleEditClick = () => openEdit(user);
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
@@ -85,7 +74,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<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>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
@@ -97,10 +86,10 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteUser.isLoading}>
|
||||
Yes
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
@@ -114,6 +103,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
/>
|
||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
ml={2}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
@@ -125,6 +115,4 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
);
|
||||
};
|
||||
|
||||
UserActions.propTypes = propTypes;
|
||||
|
||||
export default UserActions;
|
||||
@@ -1,67 +1,55 @@
|
||||
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 PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 EditUserModal from './EditUserModal';
|
||||
import UserActions from './UserActions';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
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 { useGetRequirements } from 'hooks/Network/Requirements';
|
||||
import { useGetUsers } from 'hooks/Network/Users';
|
||||
import { Column } from 'models/Table';
|
||||
import { User } from 'models/User';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
title: null,
|
||||
};
|
||||
|
||||
const UserTable = ({ title }) => {
|
||||
const UserTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [usersWithAvatars, setUsersWithAvatars] = useState([]);
|
||||
const { data: requirements } = useGetRequirements();
|
||||
const [editId, setEditId] = useState('');
|
||||
const [hiddenColumns, setHiddenColumns] = useState([]);
|
||||
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||
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) => {
|
||||
setEditId(userId);
|
||||
const openEditModal = (editUser: User) => {
|
||||
setEditId(editUser.id);
|
||||
openEdit();
|
||||
};
|
||||
|
||||
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(
|
||||
(cell) => <Avatar name={cell.row.values.name} src={cell.row.original.avatar} />,
|
||||
[],
|
||||
);
|
||||
const memoizedAvatar = useCallback((name: string, avatar: string) => <Avatar name={name} src={avatar} />, []);
|
||||
|
||||
// Columns array. This array contains your table headings and accessors which maps keys from data array
|
||||
const columns = React.useMemo(() => {
|
||||
const baseColumns = [
|
||||
const baseColumns: Column<User>[] = [
|
||||
{
|
||||
id: 'avatar',
|
||||
Header: t('account.avatar'),
|
||||
Footer: '',
|
||||
accessor: 'avatar',
|
||||
customWidth: '32px',
|
||||
Cell: ({ cell }) => memoizedAvatar(cell),
|
||||
Cell: ({ cell }) => memoizedAvatar(cell.row.original.name, cell.row.original.avatar),
|
||||
disableSortBy: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
@@ -97,7 +85,7 @@ const UserTable = ({ title }) => {
|
||||
Header: t('users.last_login'),
|
||||
Footer: '',
|
||||
accessor: 'lastLogin',
|
||||
Cell: ({ cell }) => memoizedDate(cell, 'lastLogin'),
|
||||
Cell: ({ cell }) => memoizedDate(cell.row.original.lastLogin),
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
@@ -116,7 +104,7 @@ const UserTable = ({ title }) => {
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
customWidth: '80px',
|
||||
Cell: ({ cell }) => memoizedActions(cell),
|
||||
Cell: ({ cell }) => memoizedActions(cell.row.original),
|
||||
disableSortBy: true,
|
||||
alwaysShow: true,
|
||||
});
|
||||
@@ -124,30 +112,22 @@ const UserTable = ({ title }) => {
|
||||
return baseColumns;
|
||||
}, [t, user]);
|
||||
|
||||
const showUsers = () => {
|
||||
if (usersWithAvatars.length > 0) return usersWithAvatars;
|
||||
return users ?? [];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader mb="10px">
|
||||
<Box>
|
||||
<Heading size="md">{title}</Heading>
|
||||
</Box>
|
||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||
<Box ms="auto">
|
||||
<ColumnPicker
|
||||
columns={columns}
|
||||
columns={columns as Column<unknown>[]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="provisioning.userTable.hiddenColumns"
|
||||
/>
|
||||
<CreateUserModal requirements={requirements} refreshUsers={refreshUsers} />
|
||||
<CreateUserModal />
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={refreshUsers}
|
||||
onClick={() => refreshUsers()}
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
ml={2}
|
||||
isLoading={isFetching}
|
||||
@@ -160,27 +140,20 @@ const UserTable = ({ title }) => {
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={showUsers()}
|
||||
columns={columns as Column<object>[]}
|
||||
data={users?.filter((curr) => curr.email !== user?.email) ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('users.title')}
|
||||
sortBy={[{ id: 'email', desc: false }]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
fullScreen
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<EditUserModal
|
||||
isOpen={editOpen}
|
||||
onClose={closeEdit}
|
||||
userId={editId}
|
||||
requirements={requirements}
|
||||
refreshUsers={refreshUsers}
|
||||
/>
|
||||
<EditUserModal isOpen={editOpen} onClose={closeEdit} userId={editId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UserTable.propTypes = propTypes;
|
||||
UserTable.defaultProps = defaultProps;
|
||||
export default UserTable;
|
||||
Reference in New Issue
Block a user