From e35b6331f9a1347cfd08d2f92472c6ef1d0de15d Mon Sep 17 00:00:00 2001 From: Tristan Chuine Date: Mon, 12 Aug 2024 20:15:22 +0200 Subject: [PATCH] deduplicate some functions & types --- package-lock.json | 35 +-- package.json | 1 + .../menus/content-contextual-menu.jsx | 3 +- .../menus/directory-tree-contextual-menu.jsx | 2 +- src/components/toolbars/content-toolbar.jsx | 2 +- src/hooks/useDeferredFetch.ts | 160 ++++++++++ src/hooks/useMultipleDeferredFetch.ts | 184 ++++++++++++ src/utils/UIconstants.js | 37 --- src/utils/UIconstants.ts | 38 +++ src/utils/custom-hooks.js | 284 ------------------ ...otificationType.js => notificationType.ts} | 10 +- 11 files changed, 410 insertions(+), 346 deletions(-) create mode 100644 src/hooks/useDeferredFetch.ts create mode 100644 src/hooks/useMultipleDeferredFetch.ts delete mode 100644 src/utils/UIconstants.js create mode 100644 src/utils/UIconstants.ts delete mode 100644 src/utils/custom-hooks.js rename src/utils/{notificationType.js => notificationType.ts} (62%) diff --git a/package-lock.json b/package-lock.json index 9a21787ac..ebb37db17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-window": "^1.8.10", "reconnecting-websocket": "^4.4.0", "redux": "^5.0.1", + "type-fest": "^4.24.0", "typeface-roboto": "^1.1.13", "uuid": "^9.0.1", "yup": "^1.4.0" @@ -2957,7 +2958,7 @@ "node_modules/@gridsuite/commons-ui": { "version": "0.63.4", "resolved": "file:../commons-ui/gridsuite-commons-ui-0.63.4.tgz", - "integrity": "sha512-Nl20uDnxThs6Qqm522JEZjKuxd9sQXBc3CEJeeLj3CWG6wEiak4YxlDUVWlsZStsdRB6pY/dF5XsamNJebehSA==", + "integrity": "sha512-QChFJVffZ60q1JIbfvNcAOIN/bX/yN35CgJLvzYF8Ppg2NI6G8vu4khmxyvuQ6QFBnv8sqZqw+eMNU0MjjUQAQ==", "license": "MPL-2.0", "dependencies": { "@react-querybuilder/dnd": "^7.2.0", @@ -3004,17 +3005,6 @@ "yup": "^1.4.0" } }, - "node_modules/@gridsuite/commons-ui/node_modules/type-fest": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", - "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@hookform/resolvers": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", @@ -5835,6 +5825,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -14561,12 +14563,11 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.24.0.tgz", + "integrity": "sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index bddfc354a..5f10092cc 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-window": "^1.8.10", "reconnecting-websocket": "^4.4.0", "redux": "^5.0.1", + "type-fest": "^4.24.0", "typeface-roboto": "^1.1.13", "uuid": "^9.0.1", "yup": "^1.4.0" diff --git a/src/components/menus/content-contextual-menu.jsx b/src/components/menus/content-contextual-menu.jsx index 5f5ab0a44..8246cd796 100644 --- a/src/components/menus/content-contextual-menu.jsx +++ b/src/components/menus/content-contextual-menu.jsx @@ -30,7 +30,8 @@ import { DialogsId } from '../../utils/UIconstants'; import { ContingencyListType, FilterType } from '../../utils/elementType'; import { ElementType, FilterCreationDialog, PARAM_LANGUAGE, useSnackMessage } from '@gridsuite/commons-ui'; import CommonContextualMenu from './common-contextual-menu'; -import { useDeferredFetch, useMultipleDeferredFetch } from '../../utils/custom-hooks'; +import useDeferredFetch from '../../hooks/useDeferredFetch'; +import useMultipleDeferredFetch from '../../hooks/useMultipleDeferredFetch'; import MoveDialog from '../dialogs/move-dialog'; import { useDownloadUtils } from '../utils/caseUtils'; import ExportCaseDialog from '../dialogs/export-case-dialog'; diff --git a/src/components/menus/directory-tree-contextual-menu.jsx b/src/components/menus/directory-tree-contextual-menu.jsx index c809cdb1c..a4255a119 100644 --- a/src/components/menus/directory-tree-contextual-menu.jsx +++ b/src/components/menus/directory-tree-contextual-menu.jsx @@ -23,7 +23,7 @@ import RenameDialog from '../dialogs/rename-dialog'; import DeleteDialog from '../dialogs/delete-dialog'; import { DialogsId } from '../../utils/UIconstants'; import CommonContextualMenu from './common-contextual-menu'; -import { useDeferredFetch } from '../../utils/custom-hooks'; +import useDeferredFetch from '../../hooks/useDeferredFetch'; import { ElementType, FilterCreationDialog, PARAM_LANGUAGE, useSnackMessage } from '@gridsuite/commons-ui'; import ContingencyListCreationDialog from '../dialogs/contingency-list/creation/contingency-list-creation-dialog'; import CreateCaseDialog from '../dialogs/create-case-dialog/create-case-dialog'; diff --git a/src/components/toolbars/content-toolbar.jsx b/src/components/toolbars/content-toolbar.jsx index 516b26ca2..ed1a15e66 100644 --- a/src/components/toolbars/content-toolbar.jsx +++ b/src/components/toolbars/content-toolbar.jsx @@ -17,7 +17,7 @@ import { } from '@mui/icons-material'; import DeleteDialog from '../dialogs/delete-dialog'; import CommonToolbar from './common-toolbar'; -import { useMultipleDeferredFetch } from '../../utils/custom-hooks'; +import useMultipleDeferredFetch from '../../hooks/useMultipleDeferredFetch'; import { ElementType, useSnackMessage } from '@gridsuite/commons-ui'; import MoveDialog from '../dialogs/move-dialog'; import { useDownloadUtils } from '../utils/caseUtils'; diff --git a/src/hooks/useDeferredFetch.ts b/src/hooks/useDeferredFetch.ts new file mode 100644 index 000000000..44b3d57c5 --- /dev/null +++ b/src/hooks/useDeferredFetch.ts @@ -0,0 +1,160 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useReducer } from 'react'; +import { FetchStatus, FetchStatusType } from '@gridsuite/commons-ui'; + +export enum ActionType { + START = 'START', + ERROR = 'ERROR', + SUCCESS = 'SUCCESS', + ADD_ERROR = 'ADD_ERROR', // Use by multipleDeferredFetch when one request respond with error + ADD_SUCCESS = 'ADD_SUCCESS', // Use by multipleDeferredFetch when one request respond with success +} + +type FetchAction = + | { + type: ActionType.START; + } + | { + type: ActionType.ERROR; + payload: unknown; + } + | { + type: ActionType.SUCCESS; + payload: unknown; + } + | { + type: ActionType.ADD_ERROR; + } + | { + type: ActionType.ADD_SUCCESS; + }; + +type FetchState = { + status: FetchStatusType; + data: unknown; + errorMessage: unknown; +}; + +const initialState: FetchState = { + status: FetchStatus.IDLE, + errorMessage: '', + data: null, +}; + +/** + * This custom hook manage a fetch workflow and return a unique callback to defer process execution when needed. + * It also returns a unique state which contains fetch status, results and error message if it failed. + * @param {function} fetchFunction the fetch function to call + * @param {Object} params Params of the fetch function. WARNING: Must respect order here + * @param {function} onSuccess callback to call on request success + * @param {function} errorToString callback to translate HTTPCode to string error messages + * @param {function} onError callback to call if request failed + * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response + * @returns {function} fetchCallback The callback to call to execute the request. + * It accepts params as argument which must follow fetch function params. + * @returns {state} state complete state of the request + * {Enum} state.status Status of the request + * {String} state.errorMessage error message of the request + * {Object} state.data The JSON results of the request (see hasResult) + */ +export default function useDeferredFetch( + fetchFunction: (...args: TArgs) => Promise, + onSuccess: ((data: unknown | null, args: TArgs) => void) | undefined, + errorToString: ((status: unknown) => string) | undefined = undefined, + onError: ((errorMessage: unknown | null, paramsOnError: TArgs) => void) | undefined = undefined, + hasResult: boolean = true +): [(...args: TArgs) => void, FetchState] { + const [state, dispatch] = useReducer((lastState: FetchState, action: FetchAction) => { + switch (action.type) { + case ActionType.START: + return { ...initialState, status: FetchStatus.FETCHING }; + case ActionType.SUCCESS: + return { + ...initialState, + status: FetchStatus.FETCH_SUCCESS, + data: action.payload, + }; + case ActionType.ERROR: + return { + ...initialState, + status: FetchStatus.FETCH_ERROR, + errorMessage: action.payload, + }; + default: + return lastState; + } + }, initialState); + + const handleError = useCallback( + (error: any, paramsOnError: TArgs) => { + const defaultErrorMessage = error.message; + let errorMessage = defaultErrorMessage; + if (error && errorToString) { + const providedErrorMessage = errorToString(error.status); + if (providedErrorMessage && providedErrorMessage !== '') { + errorMessage = providedErrorMessage; + } + } + dispatch({ + type: ActionType.ERROR, + payload: errorMessage, + }); + if (onError) { + onError(errorMessage, paramsOnError); + } + }, + [errorToString, onError] + ); + + const fetchData = useCallback( + async (...args: TArgs) => { + dispatch({ type: ActionType.START }); + try { + // Params resolution + const response = await fetchFunction(...args); + + if (hasResult) { + dispatch({ + type: ActionType.SUCCESS, + payload: response, + }); + if (onSuccess) { + onSuccess(response, args); + } + } else { + dispatch({ + type: ActionType.SUCCESS, + payload: null, + }); + if (onSuccess) { + onSuccess(null, args); + } + } + } catch (error: any) { + if (!error.status) { + // an http error + handleError(null, args); + throw error; + } else { + handleError(error, args); + } + } + }, + [fetchFunction, onSuccess, handleError, hasResult] + ); + + const fetchCallback = useCallback( + (...args: TArgs) => { + fetchData(...args); + }, + [fetchData] + ); + + return [fetchCallback, state]; +} diff --git a/src/hooks/useMultipleDeferredFetch.ts b/src/hooks/useMultipleDeferredFetch.ts new file mode 100644 index 000000000..ee19d06f0 --- /dev/null +++ b/src/hooks/useMultipleDeferredFetch.ts @@ -0,0 +1,184 @@ +/* + * Copyright © 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useEffect, useReducer, useState } from 'react'; +import { UnknownArray } from 'type-fest'; +import { FetchStatus, FetchStatusType } from '@gridsuite/commons-ui'; +import useDeferredFetch, { ActionType } from './useDeferredFetch'; + +type MultipleFetchAction = + | { + type: ActionType.START; + } + | { + type: ActionType.ERROR; + } + | { + type: ActionType.SUCCESS; + } + | { + type: ActionType.ADD_ERROR; + payload: unknown; + context: TArgs; + } + | { + type: ActionType.ADD_SUCCESS; + payload: unknown; + context: TArgs; + }; + +type MultipleFetchState = { + public: { + status: FetchStatusType; + data: UnknownArray; + errorMessage: UnknownArray; + paramsOnError: TArgs[]; + paramsOnSuccess: TArgs[]; + }; + counter: number; +}; + +const initialState: MultipleFetchState = { + public: { + status: FetchStatus.IDLE, + errorMessage: [], + paramsOnError: [], + data: [], + paramsOnSuccess: [], + }, + counter: 0, +}; + +/** + * This custom hook manage multiple fetchs workflows and return a unique callback to defer process execution when needed. + * It also return a unique state which concatenate all fetch results independently. + * @param {function} fetchFunction the fetch function to call for each request + * @param {function} onSuccess callback to call on all request success + * @param {function} errorToString callback to translate HTTPCode to string error messages + * @param {function} onError callback to call if one or more requests failed + * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response + * @returns {function} fetchCallback The callback to call to execute the requests collection. + * It accepts params array as arguments which define the number of fetch to execute. + * @returns {state} state complete states of the requests collection + * {Enum} state.status Status of the requests set + * {Array} state.errorMessage error message of the requests set + * {Array} state.paramsOnError The parameters used when requests set have failed + * {Array} state.data The results array of each request (see hasResult) + */ +export default function useMultipleDeferredFetch( + fetchFunction: (...args: TArgs) => Promise, + onSuccess: ((data: UnknownArray) => void) | undefined, + errorToString: ((status: unknown) => string) | undefined = undefined, + onError: ((errorMessage: UnknownArray, paramList: TArgs, paramsOnError: TArgs[]) => void) | undefined = undefined, + hasResult = true +) { + const [state, dispatch] = useReducer((lastState: MultipleFetchState, action: MultipleFetchAction) => { + switch (action.type) { + case ActionType.START: + return { + ...initialState, + public: { + ...initialState.public, + status: FetchStatus.FETCHING, + }, + }; + case ActionType.ADD_SUCCESS: + return { + public: { + ...lastState.public, + data: lastState.public.data.concat([action.payload]), + paramsOnSuccess: lastState.public.paramsOnSuccess.concat([action.context]), + }, + counter: lastState.counter + 1, + }; + case ActionType.ADD_ERROR: + return { + public: { + ...lastState.public, + errorMessage: lastState.public.errorMessage.concat([action.payload]), + paramsOnError: lastState.public.paramsOnError.concat([action.context]), + }, + counter: lastState.counter + 1, + }; + case ActionType.SUCCESS: + return { + ...lastState, + public: { + ...lastState.public, + status: FetchStatus.FETCH_SUCCESS, + }, + counter: 0, + }; + case ActionType.ERROR: + return { + ...lastState, + public: { + ...lastState.public, + status: FetchStatus.FETCH_ERROR, + }, + counter: 0, + }; + default: + return lastState; + } + }, initialState); + + const [paramList, setParamList] = useState([] as unknown as TArgs); + + const onInstanceSuccess = useCallback((data: unknown, paramsOnSuccess: TArgs) => { + dispatch({ + type: ActionType.ADD_SUCCESS, + payload: data, + context: paramsOnSuccess, + }); + }, []); + + const onInstanceError = useCallback((errorMessage: unknown, paramsOnError: TArgs) => { + // counter now stored in reducer to avoid counter and state being updated not simultaneously, + // causing useEffect to be triggered once for each change, which would cause an expected behaviour + dispatch({ + type: ActionType.ADD_ERROR, + payload: errorMessage, + context: paramsOnError, + }); + }, []); + + const [fetchCB] = useDeferredFetch(fetchFunction, onInstanceSuccess, errorToString, onInstanceError, hasResult); + + const fetchCallback = useCallback( + (cbParamsList: TArgs) => { + dispatch({ type: ActionType.START }); + setParamList(cbParamsList); + for (let params of cbParamsList) { + fetchCB(...params); + } + }, + [fetchCB] + ); + + useEffect(() => { + if (paramList.length !== 0 && paramList.length === state.counter) { + if (state.public.errorMessage.length > 0) { + dispatch({ + type: ActionType.ERROR, + }); + if (onError) { + onError(state.public.errorMessage, paramList, state.public.paramsOnError); + } + } else { + dispatch({ + type: ActionType.SUCCESS, + }); + if (onSuccess) { + onSuccess(state.public.data); + } + } + } + }, [paramList, onError, onSuccess, state]); + + return [fetchCallback]; +} diff --git a/src/utils/UIconstants.js b/src/utils/UIconstants.js deleted file mode 100644 index a1d4efdfa..000000000 --- a/src/utils/UIconstants.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -export const HORIZONTAL_SHIFT = 16; -export const VERTICAL_SHIFT = -4; -export const MOUSE_EVENT_RIGHT_BUTTON = 2; - -export const DialogsId = { - RENAME: 'rename', - DELETE: 'delete', - MOVE: 'move', - EXPORT: 'export', - ADD_NEW_STUDY_FROM_CASE: 'create_study_from_case', - ADD_NEW_STUDY: 'create_study', - SCRIPT: 'script', - REPLACE_FILTER_BY_SCRIPT_CONTINGENCY: 'replace_filter_by_script_contingency', - COPY_FILTER_TO_SCRIPT_CONTINGENCY: 'copy_filter_to_script_contingency', - REPLACE_FILTER_BY_SCRIPT: 'replace_filter_by_script', - COPY_FILTER_TO_SCRIPT: 'copy_filter_to_script', - CONVERT_TO_EXPLICIT_NAMING_FILTER: 'convert_to_explicit_naming_filter', - GENERIC_FILTER: 'generic_filter', - ADD_ROOT_DIRECTORY: 'add_root_directory', - ADD_DIRECTORY: 'add_directory', - ADD_NEW_CONTINGENCY_LIST: 'add_new_contingency_list', - ADD_NEW_FILTER: 'add_new_filter', - ADD_NEW_CASE: 'add_new_case', - RENAME_DIRECTORY: 'rename_directory', - DELETE_DIRECTORY: 'delete_directory', - NONE: 'none', -}; -export const HTTP_UNPROCESSABLE_ENTITY_STATUS = 422; -export const HTTP_CONNECTION_FAILED_MESSAGE = 'failed: Connection refused'; -export const HTTP_MAX_ELEMENTS_EXCEEDED_MESSAGE = 'MAX_ELEMENTS_EXCEEDED'; diff --git a/src/utils/UIconstants.ts b/src/utils/UIconstants.ts new file mode 100644 index 000000000..40f83f6fd --- /dev/null +++ b/src/utils/UIconstants.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +export const HORIZONTAL_SHIFT = 16; +export const VERTICAL_SHIFT = -4; +export const MOUSE_EVENT_RIGHT_BUTTON = 2; + +export enum DialogsId { + RENAME = 'rename', + DELETE = 'delete', + MOVE = 'move', + EXPORT = 'export', + ADD_NEW_STUDY_FROM_CASE = 'create_study_from_case', + ADD_NEW_STUDY = 'create_study', + SCRIPT = 'script', + REPLACE_FILTER_BY_SCRIPT_CONTINGENCY = 'replace_filter_by_script_contingency', + COPY_FILTER_TO_SCRIPT_CONTINGENCY = 'copy_filter_to_script_contingency', + REPLACE_FILTER_BY_SCRIPT = 'replace_filter_by_script', + COPY_FILTER_TO_SCRIPT = 'copy_filter_to_script', + CONVERT_TO_EXPLICIT_NAMING_FILTER = 'convert_to_explicit_naming_filter', + GENERIC_FILTER = 'generic_filter', + ADD_ROOT_DIRECTORY = 'add_root_directory', + ADD_DIRECTORY = 'add_directory', + ADD_NEW_CONTINGENCY_LIST = 'add_new_contingency_list', + ADD_NEW_FILTER = 'add_new_filter', + ADD_NEW_CASE = 'add_new_case', + RENAME_DIRECTORY = 'rename_directory', + DELETE_DIRECTORY = 'delete_directory', + NONE = 'none', +} + +export const HTTP_UNPROCESSABLE_ENTITY_STATUS = 422; +export const HTTP_CONNECTION_FAILED_MESSAGE = 'failed: Connection refused'; +export const HTTP_MAX_ELEMENTS_EXCEEDED_MESSAGE = 'MAX_ELEMENTS_EXCEEDED'; diff --git a/src/utils/custom-hooks.js b/src/utils/custom-hooks.js deleted file mode 100644 index edeff35ea..000000000 --- a/src/utils/custom-hooks.js +++ /dev/null @@ -1,284 +0,0 @@ -/** - * Copyright (c) 2022, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { useEffect, useCallback, useReducer, useState } from 'react'; - -export const FetchStatus = { - IDLE: 'IDLE', - FETCHING: 'FETCHING', - FETCH_SUCCESS: 'FETCH_SUCCESS', - FETCH_ERROR: 'FETCH_ERROR', -}; - -export const ActionType = { - START: 'START', - ERROR: 'ERROR', - SUCCESS: 'SUCCESS', - ADD_ERROR: 'ADD_ERROR', // Use by multipleDeferredFetch when one request respond with error - ADD_SUCCESS: 'ADD_SUCCESS', // Use by multipleDeferredFetch when one request respond with success -}; - -/** - * This custom hook manage a fetch workflow and return a unique callback to defer process execution when needed. - * It also returns a unique state which contains fetch status, results and error message if it failed. - * @param {function} fetchFunction the fetch function to call - * @param {Object} params Params of the fetch function. WARNING: Must respect order here - * @param {function} onSuccess callback to call on request success - * @param {function} errorToString callback to translate HTTPCode to string error messages - * @param {function} onError callback to call if request failed - * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response - * @returns {function} fetchCallback The callback to call to execute the request. - * It accepts params as argument which must follow fetch function params. - * @returns {state} state complete state of the request - * {Enum} state.status Status of the request - * {String} state.errorMessage error message of the request - * {Object} state.data The JSON results of the request (see hasResult) - */ -export const useDeferredFetch = ( - fetchFunction, - onSuccess, - errorToString = undefined, - onError = undefined, - hasResult = true -) => { - const initialState = { - status: FetchStatus.IDLE, - errorMessage: '', - data: null, - }; - - const [state, dispatch] = useReducer((lastState, action) => { - switch (action.type) { - case ActionType.START: - return { ...initialState, status: FetchStatus.FETCHING }; - case ActionType.SUCCESS: - return { - ...initialState, - status: FetchStatus.FETCH_SUCCESS, - data: action.payload, - }; - case ActionType.ERROR: - return { - ...initialState, - status: FetchStatus.FETCH_ERROR, - errorMessage: action.payload, - }; - default: - return lastState; - } - }, initialState); - - const handleError = useCallback( - (error, paramsOnError) => { - const defaultErrorMessage = error.message; - let errorMessage = defaultErrorMessage; - if (error && errorToString) { - const providedErrorMessage = errorToString(error.status); - if (providedErrorMessage && providedErrorMessage !== '') { - errorMessage = providedErrorMessage; - } - } - dispatch({ - type: ActionType.ERROR, - payload: errorMessage, - }); - if (onError) { - onError(errorMessage, paramsOnError); - } - }, - [errorToString, onError] - ); - - const fetchData = useCallback( - async (...args) => { - dispatch({ type: ActionType.START }); - try { - // Params resolution - const response = await fetchFunction.apply(null, args); - - if (hasResult) { - const data = response; - dispatch({ - type: ActionType.SUCCESS, - payload: data, - }); - if (onSuccess) { - onSuccess(data, args); - } - } else { - dispatch({ - type: ActionType.SUCCESS, - }); - if (onSuccess) { - onSuccess(null, args); - } - } - } catch (error) { - if (!error.status) { - // an http error - handleError(null, args); - throw error; - } else { - handleError(error, args); - } - } - }, - [fetchFunction, onSuccess, handleError, hasResult] - ); - - const fetchCallback = useCallback( - (...args) => { - fetchData(...args); - }, - [fetchData] - ); - - return [fetchCallback, state]; -}; - -/////////////////////////////////////////////////////////////////: - -/** - * This custom hook manage multiple fetchs workflows and return a unique callback to defer process execution when needed. - * It also return a unique state which concatenate all fetch results independently. - * @param {function} fetchFunction the fetch function to call for each request - * @param {function} onSuccess callback to call on all request success - * @param {function} errorToString callback to translate HTTPCode to string error messages - * @param {function} onError callback to call if one or more requests failed - * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response - * @returns {function} fetchCallback The callback to call to execute the requests collection. - * It accepts params array as arguments which define the number of fetch to execute. - * @returns {state} state complete states of the requests collection - * {Enum} state.status Status of the requests set - * {Array} state.errorMessage error message of the requests set - * {Array} state.paramsOnError The parameters used when requests set have failed - * {Array} state.data The results array of each request (see hasResult) - */ -export const useMultipleDeferredFetch = ( - fetchFunction, - onSuccess, - errorToString = undefined, - onError = undefined, - hasResult = true -) => { - const initialState = { - public: { - status: FetchStatus.IDLE, - errorMessage: [], - paramsOnError: [], - data: [], - paramsOnSuccess: [], - }, - counter: 0, - }; - - const [state, dispatch] = useReducer((lastState, action) => { - switch (action.type) { - case ActionType.START: - return { - ...initialState, - public: { - ...initialState.public, - status: FetchStatus.FETCHING, - }, - }; - case ActionType.ADD_SUCCESS: - return { - public: { - ...lastState.public, - data: lastState.public.data.concat([action.payload]), - paramsOnSuccess: lastState.public.paramsOnSuccess.concat([action.context]), - }, - counter: lastState.counter + 1, - }; - case ActionType.ADD_ERROR: - return { - public: { - ...lastState.public, - errorMessage: lastState.public.errorMessage.concat([action.payload]), - paramsOnError: lastState.public.paramsOnError.concat([action.context]), - }, - counter: lastState.counter + 1, - }; - case ActionType.SUCCESS: - return { - ...lastState, - public: { - ...lastState.public, - status: FetchStatus.FETCH_SUCCESS, - }, - counter: 0, - }; - case ActionType.ERROR: - return { - ...lastState, - public: { - ...lastState.public, - status: FetchStatus.FETCH_ERROR, - }, - counter: 0, - }; - default: - return lastState; - } - }, initialState); - - const [paramList, setParamList] = useState([]); - - const onInstanceSuccess = useCallback((data, paramsOnSuccess) => { - dispatch({ - type: ActionType.ADD_SUCCESS, - payload: data, - context: paramsOnSuccess, - }); - }, []); - - const onInstanceError = useCallback((errorMessage, paramsOnError) => { - // counter now stored in reducer to avoid counter and state being updated not simultenaously, - // causing useEffect to be triggered once for each change, which would cause an expected behaviour - dispatch({ - type: ActionType.ADD_ERROR, - payload: errorMessage, - context: paramsOnError, - }); - }, []); - - const [fetchCB] = useDeferredFetch(fetchFunction, onInstanceSuccess, errorToString, onInstanceError, hasResult); - - const fetchCallback = useCallback( - (cbParamsList) => { - dispatch({ type: ActionType.START }); - setParamList(cbParamsList); - for (let params of cbParamsList) { - fetchCB(...params); - } - }, - [fetchCB] - ); - - useEffect(() => { - if (paramList.length !== 0 && paramList.length === state.counter) { - if (state.public.errorMessage.length > 0) { - dispatch({ - type: ActionType.ERROR, - }); - if (onError) { - onError(state.public.errorMessage, paramList, state.public.paramsOnError); - } - } else { - dispatch({ - type: ActionType.SUCCESS, - }); - if (onSuccess) { - onSuccess(state.public.data); - } - } - } - }, [paramList, onError, onSuccess, state]); - - return [fetchCallback]; -}; diff --git a/src/utils/notificationType.js b/src/utils/notificationType.ts similarity index 62% rename from src/utils/notificationType.js rename to src/utils/notificationType.ts index 243e770e3..695693fbd 100644 --- a/src/utils/notificationType.js +++ b/src/utils/notificationType.ts @@ -5,8 +5,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export const notificationType = { - DELETE_DIRECTORY: 'DELETE_DIRECTORY', - ADD_DIRECTORY: 'ADD_DIRECTORY', - UPDATE_DIRECTORY: 'UPDATE_DIRECTORY', -}; +export enum notificationType { + DELETE_DIRECTORY = 'DELETE_DIRECTORY', + ADD_DIRECTORY = 'ADD_DIRECTORY', + UPDATE_DIRECTORY = 'UPDATE_DIRECTORY', +}