mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-11-02 03:27:56 +00:00
Merge pull request #168 from stephb9959/main
[WIFI-12224] User table cache fixes
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.9.0(1)",
|
||||
"version": "2.9.0(3)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.9.0(1)",
|
||||
"version": "2.9.0(3)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.9.0(1)",
|
||||
"version": "2.9.0(3)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
"certificate_expires_in": "Zertifikat läuft ab in",
|
||||
"certificate_expiry": "Zert. Läuft ab in",
|
||||
"connected": "In Verbindung gebracht",
|
||||
"crash_logs": "Absturzprotokolle",
|
||||
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
|
||||
"create_success": " Geräte erfolgreich erstellt",
|
||||
"current_firmware": "Aktuelle Firmware",
|
||||
@@ -615,6 +616,7 @@
|
||||
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
|
||||
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
|
||||
"logs_one": "Log",
|
||||
"new_devices": "Neue Geräte",
|
||||
"no_model_image": "Kein Modellbild gefunden",
|
||||
"not_connected": "Nicht verbunden",
|
||||
@@ -799,6 +801,7 @@
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"sign_in": "Einloggen",
|
||||
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
|
||||
"suspended_error": "Konto gesperrt, wenden Sie sich bitte an Ihren Administrator",
|
||||
"verification": "Bestätigen Sie Ihre Anmeldung",
|
||||
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
|
||||
"welcome_back": "Willkommen zurück!",
|
||||
@@ -1055,9 +1058,11 @@
|
||||
"previous_page": "Vorherige Seite"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "E-Mail nicht validiert",
|
||||
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
|
||||
"password": "Passwort",
|
||||
"role": "Rolle",
|
||||
"suspended": "Suspendiert",
|
||||
"title": "Nutzer"
|
||||
},
|
||||
"users": {
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
"certificate_expires_in": "Certificate Expiry",
|
||||
"certificate_expiry": "Cert. Expires In",
|
||||
"connected": "Connected",
|
||||
"crash_logs": "Crash Logs",
|
||||
"create_errors": "errors while trying to create devices",
|
||||
"create_success": " devices successfully created",
|
||||
"current_firmware": "Current Firmware",
|
||||
@@ -615,6 +616,7 @@
|
||||
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
|
||||
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
|
||||
"logs_one": "Log",
|
||||
"new_devices": "new devices",
|
||||
"no_model_image": "No Model Image Found",
|
||||
"not_connected": "Not Connected",
|
||||
@@ -799,6 +801,7 @@
|
||||
"reset_password": "Reset Password",
|
||||
"sign_in": "Sign In",
|
||||
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
|
||||
"suspended_error": "Suspended account, please contact your administrator",
|
||||
"verification": "Verify your login",
|
||||
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
|
||||
"welcome_back": "Welcome Back!",
|
||||
@@ -1055,9 +1058,11 @@
|
||||
"previous_page": "Previous Page"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "email not validated",
|
||||
"error_fetching": "Error fetching user information: {{e}}",
|
||||
"password": "Password",
|
||||
"role": "Role",
|
||||
"suspended": "suspended",
|
||||
"title": "User"
|
||||
},
|
||||
"users": {
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
"certificate_expires_in": "El certificado caduca en",
|
||||
"certificate_expiry": "Cert. Expira en",
|
||||
"connected": "Conectado",
|
||||
"crash_logs": "Registros de fallas",
|
||||
"create_errors": "errores al intentar crear dispositivos",
|
||||
"create_success": " dispositivos creados con éxito",
|
||||
"current_firmware": "Firmware actual",
|
||||
@@ -615,6 +616,7 @@
|
||||
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
|
||||
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
|
||||
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
|
||||
"logs_one": "Iniciar sesión",
|
||||
"new_devices": "Nuevos dispositivos",
|
||||
"no_model_image": "No se encontró ninguna imagen de modelo",
|
||||
"not_connected": "No conectado",
|
||||
@@ -799,6 +801,7 @@
|
||||
"reset_password": "Restablecer la contraseña",
|
||||
"sign_in": "Registrarse",
|
||||
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
|
||||
"suspended_error": "Cuenta suspendida, comuníquese con su administrador",
|
||||
"verification": "Verifica tu inicio de sesión",
|
||||
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
|
||||
"welcome_back": "¡Dar una buena acogida!",
|
||||
@@ -1055,9 +1058,11 @@
|
||||
"previous_page": "Página anterior"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "correo electrónico no validado",
|
||||
"error_fetching": "Error al obtener la información del usuario: {{e}}",
|
||||
"password": "Contraseña",
|
||||
"role": "papel",
|
||||
"suspended": "Suspendido",
|
||||
"title": "Usuario"
|
||||
},
|
||||
"users": {
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
"certificate_expires_in": "Le certificat expire dans",
|
||||
"certificate_expiry": "Cert. Expire dans",
|
||||
"connected": "Connecté",
|
||||
"crash_logs": "Journaux des plantages",
|
||||
"create_errors": "erreurs lors de la tentative de création d'appareils",
|
||||
"create_success": " appareils créés avec succès",
|
||||
"current_firmware": "Firmware actuel",
|
||||
@@ -615,6 +616,7 @@
|
||||
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
|
||||
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
|
||||
"logs_one": "Bûche",
|
||||
"new_devices": "nouveaux appareils",
|
||||
"no_model_image": "Aucune image de modèle trouvée",
|
||||
"not_connected": "Pas connecté",
|
||||
@@ -799,6 +801,7 @@
|
||||
"reset_password": "Réinitialiser le mot de passe",
|
||||
"sign_in": "se connecter",
|
||||
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
|
||||
"suspended_error": "Compte suspendu, veuillez contacter votre administrateur",
|
||||
"verification": "Vérifiez votre connexion",
|
||||
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
|
||||
"welcome_back": "Nous saluons le retour!",
|
||||
@@ -1055,9 +1058,11 @@
|
||||
"previous_page": "Page précédente"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "Mail non valide",
|
||||
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
|
||||
"password": "Mot de passe",
|
||||
"role": "Rôle",
|
||||
"suspended": "Suspendu",
|
||||
"title": "Utilisateur"
|
||||
},
|
||||
"users": {
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
"certificate_expires_in": "Certificado expira em",
|
||||
"certificate_expiry": "Certificado expira em",
|
||||
"connected": "Conectado",
|
||||
"crash_logs": "Registros de falhas",
|
||||
"create_errors": "erros ao tentar criar dispositivos",
|
||||
"create_success": " dispositivos criados com sucesso",
|
||||
"current_firmware": "Firmware atual",
|
||||
@@ -615,6 +616,7 @@
|
||||
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
|
||||
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
|
||||
"logs_one": "Registro",
|
||||
"new_devices": "novos dispositivos",
|
||||
"no_model_image": "Nenhuma imagem de modelo encontrada",
|
||||
"not_connected": "Não conectado",
|
||||
@@ -799,6 +801,7 @@
|
||||
"reset_password": "Redefinir senha",
|
||||
"sign_in": "assinar em",
|
||||
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
|
||||
"suspended_error": "Conta suspensa, entre em contato com seu administrador",
|
||||
"verification": "Verifique seu login",
|
||||
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
|
||||
"welcome_back": "Bem vindo de volta!",
|
||||
@@ -1055,9 +1058,11 @@
|
||||
"previous_page": "Página anterior"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "e-mail não validado",
|
||||
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
|
||||
"password": "Senha",
|
||||
"role": "Função",
|
||||
"suspended": "Suspenso",
|
||||
"title": "Do utilizador"
|
||||
},
|
||||
"users": {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ModalProps = {
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
topRightButtons?: React.ReactNode;
|
||||
tags?: React.ReactNode;
|
||||
options?: {
|
||||
modalSize?: 'sm' | 'md' | 'lg';
|
||||
maxWidth?: LayoutProps['maxWidth'];
|
||||
@@ -15,8 +16,7 @@ export type ModalProps = {
|
||||
children: React.ReactElement;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => {
|
||||
const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => {
|
||||
const maxWidth = React.useMemo(() => {
|
||||
if (options?.maxWidth) return options.maxWidth;
|
||||
if (options?.modalSize === 'sm') return undefined;
|
||||
@@ -33,6 +33,7 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }:
|
||||
<ModalContent maxWidth={maxWidth}>
|
||||
<ModalHeader
|
||||
title={title}
|
||||
left={tags}
|
||||
right={
|
||||
<HStack spacing={2}>
|
||||
{topRightButtons}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
right: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const ModalHeader = ({ title, right }) => (
|
||||
<Header>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||
{title}
|
||||
<Spacer />
|
||||
{right}
|
||||
</Flex>
|
||||
</Header>
|
||||
);
|
||||
|
||||
ModalHeader.propTypes = propTypes;
|
||||
|
||||
export default ModalHeader;
|
||||
23
src/components/Modals/ModalHeader/index.tsx
Normal file
23
src/components/Modals/ModalHeader/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
||||
|
||||
interface ModalHeaderProps {
|
||||
title: string;
|
||||
left?: React.ReactNode;
|
||||
right: React.ReactNode;
|
||||
}
|
||||
|
||||
const ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => (
|
||||
<Header>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||
{title}
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left ?? null}
|
||||
</HStack>
|
||||
<Spacer />
|
||||
{right}
|
||||
</Flex>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export default ModalHeader;
|
||||
@@ -87,6 +87,12 @@ const ConfigurationsTable = ({ select, actions }) => {
|
||||
data={configurations ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('configurations.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -99,6 +99,12 @@ const ContactTable = ({ actions, select, ignoredColumns, refreshId, disabledIds
|
||||
data={venues ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('contacts.other')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -78,6 +78,12 @@ const EntityTable = ({ actions, select }) => {
|
||||
data={entities ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('entities.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -193,6 +193,12 @@ const InventoryTable = ({
|
||||
isLoading={isFetchingCount || isFetchingTags}
|
||||
isManual={!isManual}
|
||||
obj={t('devices.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'serialNumber',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
count={count || 0}
|
||||
setPageInfo={setPageInfo}
|
||||
minHeight={minHeight ?? '200px'}
|
||||
@@ -207,6 +213,12 @@ const InventoryTable = ({
|
||||
isLoading={isFetchingCount || isFetchingTags}
|
||||
isManual={!isManual}
|
||||
obj={t('devices.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'serialNumber',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
count={count || 0}
|
||||
setPageInfo={setPageInfo}
|
||||
minHeight={minHeight ?? '200px'}
|
||||
|
||||
@@ -105,6 +105,12 @@ const LocationTable = ({ actions, select, refreshId, ignoredColumns, disabledIds
|
||||
data={venues ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('locations.other')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -93,6 +93,12 @@ const ResourcesTable = ({ select, actions, refreshId }) => {
|
||||
data={resources ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('resources.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -119,6 +119,12 @@ const SubscriberDeviceTable = ({
|
||||
data={subscriberDevices ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('devices.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'serialNumber',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight={minHeight ?? '200px'}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -78,6 +78,12 @@ const VenueTable = ({ actions, select }) => {
|
||||
data={venues ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('venues.title')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
minHeight="200px"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { ContactObj } from 'models/Contact';
|
||||
import { PageInfo } from 'models/Table';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
@@ -102,32 +103,47 @@ export const useGetSelectContacts = ({ select }: { select: string[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getContactsBatch = async (limit: number, offset: number) =>
|
||||
axiosProv
|
||||
.get(`contact?withExtendedInfo=true&limit=${limit}&offset=${offset}`)
|
||||
.then(({ data }) => data.contacts as ContactObj[]);
|
||||
|
||||
const getAllContacts = async () => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: ContactObj[] = [];
|
||||
let lastResponse: ContactObj[] = [];
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
lastResponse = await getContactsBatch(limit, offset);
|
||||
data = data.concat(lastResponse);
|
||||
offset += 500;
|
||||
} while (lastResponse.length === 500);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAllContacts = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-all-contacts'],
|
||||
() => axiosProv.get(`contact?withExtendedInfo=true&limit=500&offset=0`).then(({ data }) => data.contacts),
|
||||
{
|
||||
staleTime: 1000 * 1000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('contacts-fetching-error'))
|
||||
toast({
|
||||
id: 'contacts-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('contacts.other'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['get-all-contacts'], () => getAllContacts(), {
|
||||
staleTime: 60 * 1000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('contacts-fetching-error'))
|
||||
toast({
|
||||
id: 'contacts-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('contacts.other'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetContact = ({ enabled, id }: { enabled: boolean; id: string }) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useDefaultPage from 'hooks/useDefaultPage';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Entity } from 'models/Entity';
|
||||
import { axiosProv, axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
@@ -32,35 +32,47 @@ export const useGetEntityTree = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getEntitiesBatch = async (limit: number, offset: number) =>
|
||||
axiosProv
|
||||
.get(`entity?withExtendedInfo=true&offset=${offset}&limit=${limit}`)
|
||||
.then(({ data }: { data: { entities: Entity[] } }) => data.entities);
|
||||
|
||||
const getAllEntities = async () => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: Entity[] = [];
|
||||
let lastResponse: Entity[] = [];
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
lastResponse = await getEntitiesBatch(limit, offset);
|
||||
data = data.concat(lastResponse);
|
||||
offset += limit;
|
||||
} while (lastResponse.length === limit);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetEntities = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-entities'],
|
||||
() =>
|
||||
axiosProv
|
||||
.get('entity?withExtendedInfo=true&offset=0&limit=500')
|
||||
.then(({ data }: { data: { entities: Entity[] } }) => data.entities),
|
||||
{
|
||||
staleTime: 30000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('entities-fetching-error'))
|
||||
toast({
|
||||
id: 'entities-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('entities.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['get-entities'], () => getAllEntities(), {
|
||||
staleTime: 30000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('entities-fetching-error'))
|
||||
toast({
|
||||
id: 'entities-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('entities.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSelectEntities = ({ select }: { select: string[] }) => {
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Firmware } from 'models/Firmware';
|
||||
import { axiosFms, axiosGw } from 'utils/axiosInstances';
|
||||
|
||||
const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offset: number) =>
|
||||
axiosFms
|
||||
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
|
||||
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
|
||||
|
||||
const getAllAvailableFirmware = async (deviceType: string) => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: { firmwares: Firmware[] } = { firmwares: [] };
|
||||
let lastResponse: { firmwares: Firmware[] } = { firmwares: [] };
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
lastResponse = await getAvailableFirmwareBatch(deviceType, limit, offset);
|
||||
data = {
|
||||
firmwares: [...data.firmwares, ...lastResponse.firmwares],
|
||||
};
|
||||
offset += 500;
|
||||
} while (lastResponse.firmwares.length === 500);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-device-profile'],
|
||||
() =>
|
||||
axiosFms
|
||||
.get(`firmwares?deviceType=${deviceType}&limit=10000&offset=0`)
|
||||
.then(({ data }: { data: { firmwares: Firmware[] } }) => data),
|
||||
{
|
||||
enabled: deviceType !== '',
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('firmware-fetching-error'))
|
||||
toast({
|
||||
id: 'firmware-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
obj: t('analytics.firmware'),
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['firmware'], () => getAllAvailableFirmware(deviceType), {
|
||||
enabled: deviceType !== '',
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('firmware-fetching-error'))
|
||||
toast({
|
||||
id: 'firmware-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
obj: t('analytics.firmware'),
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumber: string; onClose: () => void }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { PageInfo } from 'models/Table';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
@@ -107,35 +107,53 @@ export const useGetSelectLocations = ({ select, enabled = true }: { select: stri
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetAllLocations = ({ venueId }: { venueId: string[] }) => {
|
||||
const getLocationsBatch = async (limit: number, offset: number, venueId?: string) =>
|
||||
axiosProv
|
||||
.get(
|
||||
`${
|
||||
venueId
|
||||
? `venue?locationsForVenue=${venueId}&limit=${limit}&offset=${offset}`
|
||||
: `location?withExtendedInfo=true&limit=${limit}&offset=${offset}`
|
||||
}`,
|
||||
)
|
||||
.then(({ data }) => data.locations as Location[]);
|
||||
|
||||
const getAllLocations = async (venueId?: string) => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: Location[] = [];
|
||||
let lastResponse: Location[] = [];
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
lastResponse = await getLocationsBatch(limit, offset, venueId);
|
||||
data = data.concat(lastResponse);
|
||||
offset += limit;
|
||||
} while (lastResponse.length === limit && venueId === undefined);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAllLocations = ({ venueId }: { venueId?: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-all-locations', venueId],
|
||||
() =>
|
||||
axiosProv
|
||||
.get(`${venueId ? `venue?locationsForVenue=${venueId}` : 'location?withExtendedInfo=true&limit=500&offset=0'}`)
|
||||
.then(({ data }) => data.locations),
|
||||
{
|
||||
staleTime: 1000 * 1000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('locations-fetching-error'))
|
||||
toast({
|
||||
id: 'locations-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('locations.other'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['get-all-locations', venueId], () => getAllLocations(venueId), {
|
||||
staleTime: 1000 * 60,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('locations-fetching-error'))
|
||||
toast({
|
||||
id: 'locations-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('locations.other'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetLocation = ({ enabled, id }: { enabled: boolean; id: string }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { PageInfo } from 'models/Table';
|
||||
import { VariableBlock } from 'models/VariableBlock';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
@@ -33,31 +33,44 @@ export const useGetResourcesCount = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getResourceBatch = async (limit: number, offset: number) =>
|
||||
axiosProv.get(`variable?limit=${limit}&offset=${offset}`).then(({ data }) => data.variableBlocks as unknown[]);
|
||||
|
||||
const getAllResources = async () => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: unknown[] = [];
|
||||
let lastResponse: unknown[] = [];
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
lastResponse = await getResourceBatch(limit, offset);
|
||||
data = data.concat(lastResponse);
|
||||
offset += limit;
|
||||
} while (lastResponse.length === limit);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAllResources = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-all-resources'],
|
||||
() => axiosProv.get(`variable?limit=500`).then(({ data }) => data.variableBlocks),
|
||||
{
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('resource-fetching-error'))
|
||||
toast({
|
||||
id: 'resource-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('resources.configuration_resource'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['get-all-resources'], () => getAllResources(), {
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('resource-fetching-error'))
|
||||
toast({
|
||||
id: 'resource-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('resources.configuration_resource'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetResources = ({
|
||||
|
||||
@@ -1,16 +1,81 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User } from 'models/User';
|
||||
import { AtLeast } from 'models/General';
|
||||
import { Note } from 'models/Note';
|
||||
import { axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
const getAvatarPromises = (userList: User[]) => {
|
||||
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[], queryClient: QueryClient) => {
|
||||
const promises = userList.map(async (user) => {
|
||||
if (user.avatar !== '' && user.avatar !== '0') {
|
||||
return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
// If the avatar is already in the cache, return it
|
||||
const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]);
|
||||
if (cachedAvatar) return cachedAvatar;
|
||||
|
||||
return axiosSec
|
||||
.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => {
|
||||
queryClient.setQueryData(['avatar', user.id, user.avatar], response);
|
||||
return response;
|
||||
})
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
});
|
||||
@@ -18,28 +83,57 @@ const getAvatarPromises = (userList: User[]) => {
|
||||
return promises;
|
||||
};
|
||||
|
||||
export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (users: unknown) => void }) => {
|
||||
const getBatchUsers = async (offset: number, limit: number) => {
|
||||
const users = await axiosSec
|
||||
.get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`)
|
||||
.then(({ data }) => data.users as User[]);
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
const getAllUsers = async () => {
|
||||
let users: User[] = [];
|
||||
let offset = 0;
|
||||
const limit = 500;
|
||||
let lastResponseLength = 0;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await getBatchUsers(offset, limit);
|
||||
users = [...users, ...response];
|
||||
offset += limit;
|
||||
lastResponseLength = response.length;
|
||||
} while (lastResponseLength === limit);
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
const getUsers = async (queryClient: QueryClient) => {
|
||||
const users = await getAllUsers();
|
||||
|
||||
const avatars = await Promise.allSettled(getAvatarPromises(users, queryClient)).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();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
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(queryClient), {
|
||||
staleTime: 30 * 1000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('users-fetching-error'))
|
||||
toast({
|
||||
@@ -62,24 +156,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(
|
||||
['users', 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 }) => {
|
||||
@@ -114,13 +212,80 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useSuspendUser = ({ id }: { id: string }) =>
|
||||
useMutation((isSuspended: boolean) =>
|
||||
axiosSec.put(`user/${id}`, {
|
||||
suspended: isSuspended,
|
||||
}),
|
||||
);
|
||||
export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}));
|
||||
export const useSuspendUser = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
export const useResetPassword = ({ id }: { id: string }) =>
|
||||
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
||||
return useMutation(
|
||||
(isSuspended: boolean) =>
|
||||
axiosSec.put(`user/${id}`, {
|
||||
suspended: isSuspended,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useResetMfa = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetPassword = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import useDefaultPage from 'hooks/useDefaultPage';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { Venue } from 'models/Venue';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
const getVenuesBatch = async (limit: number, offset: number) =>
|
||||
axiosProv
|
||||
.get(`venue?withExtendedInfo=true&offset=${offset}&limit=${limit}`)
|
||||
.then(({ data }) => data.venues as Venue[]);
|
||||
|
||||
const getAllVenues = async () => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: Venue[] = [];
|
||||
let lastResponse: Venue[] = [];
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
lastResponse = await getVenuesBatch(limit, offset);
|
||||
data = data.concat(lastResponse);
|
||||
offset += limit;
|
||||
} while (lastResponse.length === limit);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetVenues = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-venues'],
|
||||
() => axiosProv.get('venue?withExtendedInfo=true&offset=0&limit=500').then(({ data }) => data.venues),
|
||||
{
|
||||
staleTime: 30000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('venues-fetching-error'))
|
||||
toast({
|
||||
id: 'venues-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('venues.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['get-venues'], () => getAllVenues(), {
|
||||
staleTime: 30000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('venues-fetching-error'))
|
||||
toast({
|
||||
id: 'venues-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('venues.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSelectVenues = ({ select }: { select: string[] }) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Ref, useCallback, useMemo, useState } from 'react';
|
||||
import { FormikProps } from 'formik';
|
||||
import { FormType } from 'models/Form';
|
||||
|
||||
const useFormRef = () => {
|
||||
const useFormRef = <Type = Record<string, unknown>>() => {
|
||||
const [form, setForm] = useState<FormType>({
|
||||
submitForm: () => {},
|
||||
isSubmitting: false,
|
||||
@@ -10,7 +10,7 @@ const useFormRef = () => {
|
||||
dirty: false,
|
||||
});
|
||||
const formRef = useCallback(
|
||||
(node: FormikProps<Record<string, unknown>> | undefined) => {
|
||||
(node: FormikProps<Type>) => {
|
||||
if (
|
||||
node &&
|
||||
(form.submitForm !== node.submitForm ||
|
||||
@@ -22,7 +22,7 @@ const useFormRef = () => {
|
||||
}
|
||||
},
|
||||
[form],
|
||||
) as Ref<FormikProps<Record<string, unknown>>> | undefined;
|
||||
) as Ref<FormikProps<Type>>;
|
||||
|
||||
const toReturn = useMemo(() => ({ form, formRef }), [form]);
|
||||
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
import { Note } from './Note';
|
||||
|
||||
export type CreateContactObj = {
|
||||
name: string;
|
||||
description?: string;
|
||||
notes?: Note[];
|
||||
id?: string;
|
||||
type:
|
||||
| 'SUBSCRIBER'
|
||||
| 'USER'
|
||||
| 'INSTALLER'
|
||||
| 'CSR'
|
||||
| 'MANAGER'
|
||||
| 'BUSINESSOWNER'
|
||||
| 'TECHNICIAN'
|
||||
| 'CORPORATE'
|
||||
| 'UNKNOWN';
|
||||
title?: string;
|
||||
salutation?: string;
|
||||
firstname: string;
|
||||
lastname?: string;
|
||||
initials?: string;
|
||||
visual?: string;
|
||||
phones: string[];
|
||||
mobiles: string[];
|
||||
primaryEmail: string;
|
||||
secondaryEmail?: string;
|
||||
accessPIN: string;
|
||||
inUse?: string[];
|
||||
entity: string;
|
||||
};
|
||||
|
||||
export type ContactObj = {
|
||||
name: string;
|
||||
description: string;
|
||||
notes: Note[];
|
||||
id: string;
|
||||
created: number;
|
||||
modified: number;
|
||||
type:
|
||||
| 'SUBSCRIBER'
|
||||
| 'USER'
|
||||
| 'INSTALLER'
|
||||
| 'CSR'
|
||||
| 'MANAGER'
|
||||
| 'BUSINESSOWNER'
|
||||
| 'TECHNICIAN'
|
||||
| 'CORPORATE'
|
||||
| 'UNKNOWN';
|
||||
title: string;
|
||||
salutation: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
initials: string;
|
||||
visual: string;
|
||||
phones: string[];
|
||||
mobiles: string[];
|
||||
primaryEmail: string;
|
||||
secondaryEmail: string;
|
||||
accessPIN: string;
|
||||
inUse: string[];
|
||||
entity: string;
|
||||
};
|
||||
|
||||
export interface Contact {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
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>;
|
||||
@@ -36,11 +36,7 @@ export interface _LoginFormProps {
|
||||
setActiveForm: React.Dispatch<React.SetStateAction<LoginFormProps>>;
|
||||
}
|
||||
|
||||
const _LoginForm = (
|
||||
{
|
||||
setActiveForm
|
||||
}: _LoginFormProps
|
||||
) => {
|
||||
const _LoginForm: React.FC<_LoginFormProps> = ({ setActiveForm }) => {
|
||||
const { t } = useTranslation();
|
||||
const { setToken } = useAuth();
|
||||
const { accessPolicyLink, passwordPolicyLink } = useApiRequirements();
|
||||
@@ -50,9 +46,12 @@ const _LoginForm = (
|
||||
const forgotPassword = () => setActiveForm({ form: 'forgot-password' });
|
||||
|
||||
const displayError = useMemo(() => {
|
||||
const loginError = error as AxiosError;
|
||||
const loginError: AxiosError = error as AxiosError;
|
||||
|
||||
if (loginError?.response?.data?.ErrorCode === 4) return t('login.waiting_for_email_verification');
|
||||
if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification');
|
||||
if (loginError?.response?.data?.ErrorCode === 15) {
|
||||
return t('login.suspended_error');
|
||||
}
|
||||
return t('login.invalid_credentials');
|
||||
}, [t, error]);
|
||||
|
||||
|
||||
@@ -87,6 +87,12 @@ const ServiceClassTable = ({ operatorId, refreshId, actions }) => {
|
||||
columns={columns}
|
||||
data={serviceClasses ?? []}
|
||||
obj={t('service.other')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
isLoading={isFetching}
|
||||
minHeight="200px"
|
||||
/>
|
||||
|
||||
@@ -111,6 +111,12 @@ const OperatorsTable = () => {
|
||||
isManual
|
||||
hiddenColumns={hiddenColumns}
|
||||
obj={t('operator.other')}
|
||||
sortBy={[
|
||||
{
|
||||
id: 'name',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
count={count || 0}
|
||||
// @ts-ignore
|
||||
setPageInfo={setPageInfo}
|
||||
|
||||
@@ -72,6 +72,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
<SystemLoggingButton endpoint={endpoint} token={token} />
|
||||
<Button
|
||||
mt={1}
|
||||
ml={2}
|
||||
minWidth="112px"
|
||||
colorScheme="gray"
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
|
||||
@@ -5,22 +5,18 @@ import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useSendUserEmailValidation, useSuspendUser, useResetMfa, useResetPassword } from 'hooks/Network/Users';
|
||||
import useMutationResult from 'hooks/useMutationResult';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
isSuspended: boolean;
|
||||
isWaitingForCheck: boolean;
|
||||
refresh: () => void;
|
||||
isDisabled?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const UserActions = (
|
||||
{
|
||||
id,
|
||||
isSuspended,
|
||||
isWaitingForCheck,
|
||||
refresh
|
||||
}: Props
|
||||
) => {
|
||||
const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh });
|
||||
@@ -38,7 +34,7 @@ const UserActions = (
|
||||
onSuccess();
|
||||
},
|
||||
onError: (e) => {
|
||||
onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
const handleResetMfaClick = () =>
|
||||
@@ -55,7 +51,7 @@ const UserActions = (
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,7 +69,7 @@ const UserActions = (
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -82,9 +78,16 @@ const UserActions = (
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} />
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
icon={<Wrench size={20} />}
|
||||
size={size}
|
||||
ml={2}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList>
|
||||
<MenuList fontSize="md">
|
||||
<MenuItem onClick={handleSuspendClick}>
|
||||
{isSuspended ? t('users.reactivate_user') : t('users.suspend')}
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,73 +1,83 @@
|
||||
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 NotesTable from 'components/CustomFields/NotesTable';
|
||||
import * as Yup from 'yup';
|
||||
import { NotesField } from 'components/FormFields/NotesField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
import ToggleField from 'components/FormFields/ToggleField';
|
||||
import { UpdateUserSchema } from 'constants/formSchemas';
|
||||
import { testObjectName, testRegex } from 'constants/formTests';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { User, useUpdateUser } from 'hooks/Network/Users';
|
||||
import useApiRequirements from 'hooks/useApiRequirements';
|
||||
|
||||
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]);
|
||||
}, [isOpen, editing]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
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),
|
||||
},
|
||||
@@ -86,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',
|
||||
});
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -132,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')}
|
||||
@@ -147,8 +153,7 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
</Form>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{' '}
|
||||
<NotesTable name="notes" isDisabled={!editing} />
|
||||
<NotesField isDisabled={!editing} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
@@ -165,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 ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useGetUser } from 'hooks/Network/Users';
|
||||
import useFormRef from 'hooks/useFormRef';
|
||||
import { axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
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>
|
||||
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
EditUserModal.propTypes = propTypes;
|
||||
EditUserModal.defaultProps = defaultProps;
|
||||
|
||||
export default EditUserModal;
|
||||
100
src/pages/UsersPage/Table/EditUserModal/index.tsx
Normal file
100
src/pages/UsersPage/Table/EditUserModal/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ActionsDropdown from '../ActionsDropdown';
|
||||
import UpdateUserForm from './Form';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import ToggleEditButton from 'components/Buttons/ToggleEditButton';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { User, useGetUser } 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 queryClient = useQueryClient();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const { form, formRef } = useFormRef<User>();
|
||||
const canFetchUser = userId !== '' && isOpen;
|
||||
const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
closeConfirm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
refetch();
|
||||
queryClient.invalidateQueries(['users']);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditing.off();
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
title={user?.name ?? t('crud.edit_obj', { obj: t('user.title') })}
|
||||
tags={
|
||||
<>
|
||||
{user?.suspended ? (
|
||||
<Tag colorScheme="yellow" size="lg">
|
||||
{t('user.suspended')}
|
||||
</Tag>
|
||||
) : null}
|
||||
{user?.waitingForEmailCheck ? (
|
||||
<Tag colorScheme="blue" size="lg">
|
||||
{t('user.email_not_validated')}
|
||||
</Tag>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
topRightButtons={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
hidden={!editing}
|
||||
/>
|
||||
<ToggleEditButton ml={2} isEditing={editing} toggleEdit={setEditing.toggle} isDirty={form.dirty} />
|
||||
{user ? (
|
||||
<ActionsDropdown
|
||||
id={user?.id}
|
||||
isSuspended={user?.suspended}
|
||||
isWaitingForCheck={user?.waitingForEmailCheck}
|
||||
refresh={refresh}
|
||||
size="md"
|
||||
isDisabled={editing}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!isFetching && user ? (
|
||||
<UpdateUserForm editing={editing} selectedUser={user} isOpen={isOpen} onClose={onClose} formRef={formRef} />
|
||||
) : (
|
||||
<Center>
|
||||
<Spinner />
|
||||
</Center>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmCloseAlert isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUserModal;
|
||||
@@ -164,6 +164,7 @@ const UserTable = ({ title }) => {
|
||||
data={showUsers()}
|
||||
isLoading={isFetching}
|
||||
obj={t('users.title')}
|
||||
sortBy={[{ id: 'email', desc: false }]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
fullScreen
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user