From 0f94f82c7b0baf23865f6b335996d6c9d58da7d1 Mon Sep 17 00:00:00 2001 From: Loup Theron Date: Fri, 27 Sep 2024 16:04:12 +0200 Subject: [PATCH 1/4] Add useInterval hook to listen for changes --- frontend/src/api/constants.ts | 1 - .../LogbookPriorNotificationForm/index.tsx | 23 +++++++ .../ManualPriorNotificationForm/index.tsx | 24 ++++++- .../PriorNotification/hooks/useInterval.ts | 25 +++++++ .../PriorNotification/priorNotificationApi.ts | 22 ++---- .../useCases/refreshPriorNotification.ts | 68 +++++++++++++++++++ 6 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 frontend/src/features/PriorNotification/hooks/useInterval.ts create mode 100644 frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts diff --git a/frontend/src/api/constants.ts b/frontend/src/api/constants.ts index 85b0fe785f..fcbb0a2cf2 100644 --- a/frontend/src/api/constants.ts +++ b/frontend/src/api/constants.ts @@ -35,7 +35,6 @@ export enum HttpStatusCode { } export enum RtkCacheTagType { - PriorNotification = 'PriorNotification', PriorNotificationDocuments = 'PriorNotificationDocuments', PriorNotificationTypes = 'PriorNotificationTypes', PriorNotifications = 'PriorNotifications', diff --git a/frontend/src/features/PriorNotification/components/LogbookPriorNotificationForm/index.tsx b/frontend/src/features/PriorNotification/components/LogbookPriorNotificationForm/index.tsx index 35300e07c1..809374c6d5 100644 --- a/frontend/src/features/PriorNotification/components/LogbookPriorNotificationForm/index.tsx +++ b/frontend/src/features/PriorNotification/components/LogbookPriorNotificationForm/index.tsx @@ -1,3 +1,6 @@ +import { useInterval } from '@features/PriorNotification/hooks/useInterval' +import { PriorNotification } from '@features/PriorNotification/PriorNotification.types' +import { refreshPriorNotification } from '@features/PriorNotification/useCases/refreshPriorNotification' import { verifyAndSendPriorNotification } from '@features/PriorNotification/useCases/verifyAndSendPriorNotification' import { getPriorNotificationIdentifier } from '@features/PriorNotification/utils' import { useMainAppDispatch } from '@hooks/useMainAppDispatch' @@ -24,6 +27,26 @@ export function LogbookPriorNotificationForm() { state => state.priorNotification.openedPriorNotificationDetail ) + const isBeingSent = + openedPriorNotificationDetail?.state === PriorNotification.State.PENDING_SEND || + openedPriorNotificationDetail?.state === PriorNotification.State.PENDING_AUTO_SEND + + useInterval( + () => { + assertNotNullish(openedPriorNotificationDetail) + + dispatch( + refreshPriorNotification( + getPriorNotificationIdentifier(openedPriorNotificationDetail), + openedPriorNotificationDetail.fingerprint, + openedPriorNotificationDetail.isManuallyCreated + ) + ) + }, + isBeingSent, + 5000 + ) + const displayedErrorKey = displayedError ? DisplayedErrorKey.SIDE_WINDOW_PRIOR_NOTIFICATION_FORM_ERROR : undefined const verifyAndSend = async () => { diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/index.tsx b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/index.tsx index 49cc01fd35..24d8123401 100644 --- a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/index.tsx +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/index.tsx @@ -1,4 +1,6 @@ import { ErrorWall } from '@components/ErrorWall' +import { useInterval } from '@features/PriorNotification/hooks/useInterval' +import { refreshPriorNotification } from '@features/PriorNotification/useCases/refreshPriorNotification' import { verifyAndSendPriorNotification } from '@features/PriorNotification/useCases/verifyAndSendPriorNotification' import { getPriorNotificationIdentifier } from '@features/PriorNotification/utils' import { useMainAppDispatch } from '@hooks/useMainAppDispatch' @@ -14,11 +16,11 @@ import { LoadingSpinnerWall } from 'ui/LoadingSpinnerWall' import { FORM_VALIDATION_SCHEMA } from './constants' import { Content } from './Content' import { SideWindowCard } from '../../../../components/SideWindowCard' +import { PriorNotification } from '../../PriorNotification.types' import { priorNotificationActions } from '../../slice' import { createOrUpdateManualPriorNotification } from '../../useCases/createOrUpdateManualPriorNotification' import type { ManualPriorNotificationFormValues } from './types' -import type { PriorNotification } from '../../PriorNotification.types' export function ManualPriorNotificationForm() { const dispatch = useMainAppDispatch() @@ -33,6 +35,26 @@ export function ManualPriorNotificationForm() { state => state.priorNotification.openedPriorNotificationDetail ) + const isBeingSent = + openedPriorNotificationDetail?.state === PriorNotification.State.PENDING_SEND || + openedPriorNotificationDetail?.state === PriorNotification.State.PENDING_AUTO_SEND + + useInterval( + () => { + assertNotNullish(openedPriorNotificationDetail) + + dispatch( + refreshPriorNotification( + getPriorNotificationIdentifier(openedPriorNotificationDetail), + openedPriorNotificationDetail.fingerprint, + openedPriorNotificationDetail.isManuallyCreated + ) + ) + }, + isBeingSent, + 5000 + ) + const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false) const [isLoading, setIsLoading] = useState(false) diff --git a/frontend/src/features/PriorNotification/hooks/useInterval.ts b/frontend/src/features/PriorNotification/hooks/useInterval.ts new file mode 100644 index 0000000000..49fce67e51 --- /dev/null +++ b/frontend/src/features/PriorNotification/hooks/useInterval.ts @@ -0,0 +1,25 @@ +import { useEffect, useRef } from 'react' + +export function useInterval(callback: () => void, condition: boolean, delay: number): void { + const savedCallback = useRef<() => void>() + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + if (!condition) { + return undefined + } + + function tick() { + if (savedCallback.current) { + savedCallback.current() + } + } + + const intervalId = setInterval(tick, delay) + + return () => clearInterval(intervalId) + }, [condition, delay]) +} diff --git a/frontend/src/features/PriorNotification/priorNotificationApi.ts b/frontend/src/features/PriorNotification/priorNotificationApi.ts index 5223d833ce..2c0b06419e 100644 --- a/frontend/src/features/PriorNotification/priorNotificationApi.ts +++ b/frontend/src/features/PriorNotification/priorNotificationApi.ts @@ -83,7 +83,7 @@ export const priorNotificationApi = monitorfishApi.injectEndpoints({ isManuallyCreated: boolean } >({ - providesTags: (_, __, { reportId }) => [{ id: reportId, type: RtkCacheTagType.PriorNotification }], + forceRefetch: () => true, query: ({ isManuallyCreated, operationDate, reportId }) => getUrlOrPathWithQueryParams(`/prior_notifications/${reportId}`, { isManuallyCreated, operationDate }), transformErrorResponse: response => new FrontendApiError(GET_PRIOR_NOTIFICATION_DETAIL_ERROR_MESSAGE, response) @@ -152,10 +152,7 @@ export const priorNotificationApi = monitorfishApi.injectEndpoints({ isManuallyCreated: boolean } >({ - invalidatesTags: (_, __, { reportId }) => [ - { type: RtkCacheTagType.PriorNotifications }, - { id: reportId, type: RtkCacheTagType.PriorNotification } - ], + invalidatesTags: () => [{ type: RtkCacheTagType.PriorNotifications }], query: ({ isManuallyCreated, operationDate, reportId }) => ({ method: 'PUT', url: getUrlOrPathWithQueryParams(`/prior_notifications/${reportId}/invalidate`, { @@ -174,10 +171,7 @@ export const priorNotificationApi = monitorfishApi.injectEndpoints({ reportId: string } >({ - invalidatesTags: (_, __, { reportId }) => [ - { type: RtkCacheTagType.PriorNotifications }, - { id: reportId, type: RtkCacheTagType.PriorNotification } - ], + invalidatesTags: () => [{ type: RtkCacheTagType.PriorNotifications }], query: ({ data, operationDate, reportId }) => ({ body: data, method: 'PUT', @@ -193,10 +187,7 @@ export const priorNotificationApi = monitorfishApi.injectEndpoints({ reportId: string } >({ - invalidatesTags: (_, __, { reportId }) => [ - { type: RtkCacheTagType.PriorNotifications }, - { id: reportId, type: RtkCacheTagType.PriorNotification } - ], + invalidatesTags: () => [{ type: RtkCacheTagType.PriorNotifications }], query: ({ data, reportId }) => ({ body: data, method: 'PUT', @@ -211,10 +202,7 @@ export const priorNotificationApi = monitorfishApi.injectEndpoints({ isManuallyCreated: boolean } >({ - invalidatesTags: (_, __, { reportId }) => [ - { type: RtkCacheTagType.PriorNotifications }, - { id: reportId, type: RtkCacheTagType.PriorNotification } - ], + invalidatesTags: () => [{ type: RtkCacheTagType.PriorNotifications }], query: ({ isManuallyCreated, operationDate, reportId }) => ({ method: 'POST', url: getUrlOrPathWithQueryParams(`/prior_notifications/${reportId}/verify_and_send`, { diff --git a/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts b/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts new file mode 100644 index 0000000000..be0f9d64bd --- /dev/null +++ b/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts @@ -0,0 +1,68 @@ +import { RtkCacheTagType } from '@api/constants' +import { addMainWindowBanner } from '@features/SideWindow/useCases/addMainWindowBanner' +import { DisplayedErrorKey } from '@libs/DisplayedError/constants' +import { FrontendApiError } from '@libs/FrontendApiError' +import { Level } from '@mtes-mct/monitor-ui' +import { handleThunkError } from '@utils/handleThunkError' +import { displayOrLogError } from 'domain/use_cases/error/displayOrLogError' + +import { priorNotificationApi } from '../priorNotificationApi' +import { priorNotificationActions } from '../slice' + +import type { PriorNotification } from '../PriorNotification.types' +import type { MainAppThunk } from '@store' + +export const refreshPriorNotification = + ( + identifier: PriorNotification.Identifier, + fingerprint: string, + isManuallyCreated: boolean + ): MainAppThunk> => + async dispatch => { + try { + const logbookPriorNotification = await dispatch( + priorNotificationApi.endpoints.getPriorNotificationDetail.initiate({ + ...identifier, + isManuallyCreated + }) + ).unwrap() + + // Update prior notification list if prior notification fingerprint has changed + if (logbookPriorNotification.fingerprint !== fingerprint) { + dispatch(priorNotificationApi.util.invalidateTags([RtkCacheTagType.PriorNotifications])) + } + + // Close card and display a warning banner if prior notification has been deleted (in the meantime) + if (logbookPriorNotification.logbookMessage.isDeleted) { + dispatch(priorNotificationActions.closePriorNotificationCardAndForm()) + dispatch( + addMainWindowBanner({ + children: 'Ce préavis a été supprimé (entre temps).', + closingDelay: 5000, + isClosable: true, + level: Level.WARNING, + withAutomaticClosing: true + }) + ) + + return + } + + dispatch(priorNotificationActions.setOpenedPriorNotificationDetail(logbookPriorNotification)) + } catch (err) { + if (err instanceof FrontendApiError) { + dispatch( + displayOrLogError( + err, + () => refreshPriorNotification(identifier, fingerprint, isManuallyCreated), + true, + DisplayedErrorKey.SIDE_WINDOW_PRIOR_NOTIFICATION_FORM_ERROR + ) + ) + + return + } + + handleThunkError(err) + } + } From b59b1061cfa826d7d4171e5ba493e7205c99d239 Mon Sep 17 00:00:00 2001 From: Loup Theron Date: Mon, 30 Sep 2024 09:05:52 +0200 Subject: [PATCH 2/4] Reset interval --- .../src/features/PriorNotification/hooks/useInterval.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/PriorNotification/hooks/useInterval.ts b/frontend/src/features/PriorNotification/hooks/useInterval.ts index 49fce67e51..171ab39de1 100644 --- a/frontend/src/features/PriorNotification/hooks/useInterval.ts +++ b/frontend/src/features/PriorNotification/hooks/useInterval.ts @@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react' export function useInterval(callback: () => void, condition: boolean, delay: number): void { const savedCallback = useRef<() => void>() + const intervalId = useRef() useEffect(() => { savedCallback.current = callback @@ -9,7 +10,7 @@ export function useInterval(callback: () => void, condition: boolean, delay: num useEffect(() => { if (!condition) { - return undefined + return () => clearInterval(intervalId.current) } function tick() { @@ -18,8 +19,8 @@ export function useInterval(callback: () => void, condition: boolean, delay: num } } - const intervalId = setInterval(tick, delay) + intervalId.current = setInterval(tick, delay) - return () => clearInterval(intervalId) + return () => clearInterval(intervalId.current) }, [condition, delay]) } From 6ea1d7898e5d9e169cee406b5cd13d83ed77997f Mon Sep 17 00:00:00 2001 From: Loup Theron Date: Mon, 30 Sep 2024 14:31:24 +0200 Subject: [PATCH 3/4] Use window to specify window context --- frontend/src/features/PriorNotification/hooks/useInterval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/PriorNotification/hooks/useInterval.ts b/frontend/src/features/PriorNotification/hooks/useInterval.ts index 171ab39de1..c467212add 100644 --- a/frontend/src/features/PriorNotification/hooks/useInterval.ts +++ b/frontend/src/features/PriorNotification/hooks/useInterval.ts @@ -19,7 +19,7 @@ export function useInterval(callback: () => void, condition: boolean, delay: num } } - intervalId.current = setInterval(tick, delay) + intervalId.current = window.setInterval(tick, delay) return () => clearInterval(intervalId.current) }, [condition, delay]) From 27a3c54eb226a21ec817c700929d0833d2b8f8d8 Mon Sep 17 00:00:00 2001 From: Loup Theron Date: Mon, 30 Sep 2024 14:37:13 +0200 Subject: [PATCH 4/4] Rename use-case --- .../useCases/openLogbookPriorNotificationForm.ts | 4 ++-- .../PriorNotification/useCases/openPriorNotificationCard.ts | 4 ++-- .../PriorNotification/useCases/refreshPriorNotification.ts | 4 ++-- .../{addMainWindowBanner.ts => addSideWindowBanner.ts} | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename frontend/src/features/SideWindow/useCases/{addMainWindowBanner.ts => addSideWindowBanner.ts} (84%) diff --git a/frontend/src/features/PriorNotification/useCases/openLogbookPriorNotificationForm.ts b/frontend/src/features/PriorNotification/useCases/openLogbookPriorNotificationForm.ts index 556f78cae8..afa430b25f 100644 --- a/frontend/src/features/PriorNotification/useCases/openLogbookPriorNotificationForm.ts +++ b/frontend/src/features/PriorNotification/useCases/openLogbookPriorNotificationForm.ts @@ -1,5 +1,5 @@ import { RtkCacheTagType } from '@api/constants' -import { addMainWindowBanner } from '@features/SideWindow/useCases/addMainWindowBanner' +import { addSideWindowBanner } from '@features/SideWindow/useCases/addSideWindowBanner' import { customSentry, CustomSentryMeasurementName } from '@libs/customSentry' import { DisplayedErrorKey } from '@libs/DisplayedError/constants' import { FrontendApiError } from '@libs/FrontendApiError' @@ -54,7 +54,7 @@ export const openLogbookPriorNotificationForm = if (logbookPriorNotification.logbookMessage.isDeleted) { dispatch(priorNotificationActions.closePriorNotificationCardAndForm()) dispatch( - addMainWindowBanner({ + addSideWindowBanner({ children: 'Ce préavis a été supprimé (entre temps).', closingDelay: 5000, isClosable: true, diff --git a/frontend/src/features/PriorNotification/useCases/openPriorNotificationCard.ts b/frontend/src/features/PriorNotification/useCases/openPriorNotificationCard.ts index c5b0fb9212..01c5d827e6 100644 --- a/frontend/src/features/PriorNotification/useCases/openPriorNotificationCard.ts +++ b/frontend/src/features/PriorNotification/useCases/openPriorNotificationCard.ts @@ -1,5 +1,5 @@ import { RtkCacheTagType } from '@api/constants' -import { addMainWindowBanner } from '@features/SideWindow/useCases/addMainWindowBanner' +import { addSideWindowBanner } from '@features/SideWindow/useCases/addSideWindowBanner' import { DisplayedErrorKey } from '@libs/DisplayedError/constants' import { FrontendApiError } from '@libs/FrontendApiError' import { Level } from '@mtes-mct/monitor-ui' @@ -43,7 +43,7 @@ export const openPriorNotificationCard = if (priorNotificationDetail.logbookMessage.isDeleted) { dispatch(priorNotificationActions.closePriorNotificationCardAndForm()) dispatch( - addMainWindowBanner({ + addSideWindowBanner({ children: 'Ce préavis a été supprimé (entre temps).', closingDelay: 5000, isClosable: true, diff --git a/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts b/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts index be0f9d64bd..8757923764 100644 --- a/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts +++ b/frontend/src/features/PriorNotification/useCases/refreshPriorNotification.ts @@ -1,5 +1,5 @@ import { RtkCacheTagType } from '@api/constants' -import { addMainWindowBanner } from '@features/SideWindow/useCases/addMainWindowBanner' +import { addSideWindowBanner } from '@features/SideWindow/useCases/addSideWindowBanner' import { DisplayedErrorKey } from '@libs/DisplayedError/constants' import { FrontendApiError } from '@libs/FrontendApiError' import { Level } from '@mtes-mct/monitor-ui' @@ -36,7 +36,7 @@ export const refreshPriorNotification = if (logbookPriorNotification.logbookMessage.isDeleted) { dispatch(priorNotificationActions.closePriorNotificationCardAndForm()) dispatch( - addMainWindowBanner({ + addSideWindowBanner({ children: 'Ce préavis a été supprimé (entre temps).', closingDelay: 5000, isClosable: true, diff --git a/frontend/src/features/SideWindow/useCases/addMainWindowBanner.ts b/frontend/src/features/SideWindow/useCases/addSideWindowBanner.ts similarity index 84% rename from frontend/src/features/SideWindow/useCases/addMainWindowBanner.ts rename to frontend/src/features/SideWindow/useCases/addSideWindowBanner.ts index 94001107fa..49e9f4f02d 100644 --- a/frontend/src/features/SideWindow/useCases/addMainWindowBanner.ts +++ b/frontend/src/features/SideWindow/useCases/addSideWindowBanner.ts @@ -1,15 +1,15 @@ import { bannerStackAdapter, sideWindowActions } from '../slice' import type { SideWindow } from '../SideWindow.types' -import type { MainAppThunk } from '@store/index' +import type { MainAppThunk } from '@store' /** - * Add a banner to the main window. + * Add a banner to the side window. * * @param props Component props of the `` to add. * @returns ID of the added banner (used to remove it if needed). */ -export const addMainWindowBanner = +export const addSideWindowBanner = (props: SideWindow.BannerStackItemProps): MainAppThunk => (dispatch, getState) => { const { bannerStack } = getState().sideWindow