diff --git a/package-lock.json b/package-lock.json index ed6d716..df8a416 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,13 @@ "@mui/icons-material": "^5.16.14", "@mui/lab": "5.0.0-alpha.175", "@mui/material": "^5.16.14", + "@mui/x-date-pickers": "^7.28.3", "@mui/x-tree-view": "^7.28.1", "@reduxjs/toolkit": "^2.5.1", "ag-grid-community": "^33.1.0", "ag-grid-react": "^33.1.0", "core-js": "^3.40.0", + "dayjs": "^1.11.13", "notistack": "^3.0.2", "oidc-client": "^1.11.5", "react": "^18.3.1", @@ -4082,6 +4084,71 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.28.3.tgz", + "integrity": "sha512-5umKB/DIMfDN+FAlzcrocix9PpoJDJ+5hMdlby8spTPObP4wCSN+wkEhk0vFC7qE9FAWXr4wjemaKvsNf41cCw==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta", + "@mui/x-internals": "7.28.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@mui/x-internals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.28.0.tgz", @@ -7152,6 +7219,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 894b946..5f358e8 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "@mui/icons-material": "^5.16.14", "@mui/lab": "5.0.0-alpha.175", "@mui/material": "^5.16.14", + "@mui/x-date-pickers": "^7.28.3", "@mui/x-tree-view": "^7.28.1", "@reduxjs/toolkit": "^2.5.1", "ag-grid-community": "^33.1.0", "ag-grid-react": "^33.1.0", "core-js": "^3.40.0", + "dayjs": "^1.11.13", "notistack": "^3.0.2", "oidc-client": "^1.11.5", "react": "^18.3.1", diff --git a/src/components/App/app-top-bar.tsx b/src/components/App/app-top-bar.tsx index 8e79691..f16cd65 100644 --- a/src/components/App/app-top-bar.tsx +++ b/src/components/App/app-top-bar.tsx @@ -15,8 +15,8 @@ import { useState, } from 'react'; import { capitalize, Tab, Tabs, useTheme } from '@mui/material'; -import { Groups, ManageAccounts, PeopleAlt } from '@mui/icons-material'; -import { fetchAppsMetadata, logout, Metadata, TopBar } from '@gridsuite/commons-ui'; +import { Groups, ManageAccounts, NotificationImportant, PeopleAlt } from '@mui/icons-material'; +import { fetchAppsMetadata, logout, Metadata, TopBar, useGlobalAnnouncement } from '@gridsuite/commons-ui'; import { useParameterState } from '../parameters'; import { APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../../utils/config-params'; import { NavLink, type To, useMatches, useNavigate } from 'react-router'; @@ -73,6 +73,20 @@ const tabs = new Map([ ))} />, ], + [ + MainPaths.banners, + } + label={} + href={`/${MainPaths.banners}`} + value={MainPaths.banners} + key={`tab-${MainPaths.banners}`} + iconPosition="start" + LinkComponent={forwardRef>((props, ref) => ( + + ))} + />, + ], ]); const AppTopBar: FunctionComponent = () => { @@ -96,6 +110,9 @@ const AppTopBar: FunctionComponent = () => { const [languageLocal, handleChangeLanguage] = useParameterState(PARAM_LANGUAGE); const [appsAndUrls, setAppsAndUrls] = useState([]); + + const announcementInfos = useGlobalAnnouncement(user); + useEffect(() => { if (user !== null) { fetchAppsMetadata().then((res) => { @@ -122,6 +139,7 @@ const AppTopBar: FunctionComponent = () => { onLanguageClick={handleChangeLanguage} language={languageLocal} developerMode={false} // TODO: set as optional in commons-ui + announcementInfos={announcementInfos} > > = (props, context) => { useDebugRender('app'); @@ -59,7 +63,7 @@ const App: FunctionComponent> = (props, context) => { [updateParams, snackError] ); - useNotificationsListener(NOTIFICATIONS_URL_KEYS.CONFIG, { listenerCallbackMessage: updateConfig }); + useNotificationsListener(NotificationsUrlKeys.CONFIG, { listenerCallbackMessage: updateConfig }); useEffect(() => { if (user !== null) { diff --git a/src/pages/banners/add-announcement-form.tsx b/src/pages/banners/add-announcement-form.tsx new file mode 100644 index 0000000..ba6a339 --- /dev/null +++ b/src/pages/banners/add-announcement-form.tsx @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2025, 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 { CustomFormProvider, SubmitButton, useSnackMessage } from '@gridsuite/commons-ui'; +import Grid from '@mui/material/Grid'; +import { FormattedMessage, useIntl } from 'react-intl'; +import React, { FunctionComponent, useCallback } from 'react'; +import { FormControl, InputLabel, MenuItem, Select, TextField } from '@mui/material'; +import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { UserAdminSrv, Announcement } from '../../services'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import yup from '../../utils/yup-config'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/en'; +import { useParameterState } from '../../components/parameters'; +import { PARAM_LANGUAGE } from '../../utils/config-params'; +import { getErrorMessage, handleAnnouncementCreationErrors } from '../../utils/error'; + +export const MESSAGE = 'message'; +export const START_DATE = 'startDate'; +export const END_DATE = 'endDate'; +export const SEVERITY = 'severity'; + +interface AddAnnouncementProps { + onAnnouncementCreated: () => void; +} + +const AddAnnouncementForm: FunctionComponent = ({ onAnnouncementCreated }) => { + const intl = useIntl(); + const [languageLocal] = useParameterState(PARAM_LANGUAGE); + const { snackError } = useSnackMessage(); + + const formSchema = yup + .object() + .shape({ + [MESSAGE]: yup.string().trim().required(), + [START_DATE]: yup.string().required(), + [END_DATE]: yup.string().required(), + [SEVERITY]: yup.string().required(), + }) + .required(); + + const formMethods = useForm({ + resolver: yupResolver(formSchema), + }); + + const { register, setValue, handleSubmit, formState } = formMethods; + + const onSubmit = useCallback( + (params: any) => { + let startDate = new Date(params.startDate).toISOString(); + let endDate = new Date(params.endDate).toISOString(); + const newAnnouncement = { + id: crypto.randomUUID(), + message: params.message, + startDate: startDate, + endDate: endDate, + severity: params.severity, + } as Announcement; + UserAdminSrv.addAnnouncement(newAnnouncement) + .then(() => onAnnouncementCreated()) + .catch((error) => { + let errorMessage = getErrorMessage(error) ?? ''; + if (!handleAnnouncementCreationErrors(errorMessage, snackError)) { + snackError({ + headerId: 'errCreateAnnouncement', + messageTxt: errorMessage, + }); + } + }); + }, + [onAnnouncementCreated, snackError] + ); + + return ( + + + + + + + + setValue('startDate', newValue?.toISOString() ?? '')} + /> + + + + + setValue('endDate', newValue?.toISOString() ?? '')} + /> + + + + + + + + + + + + + + + + + + ); +}; + +export default AddAnnouncementForm; diff --git a/src/pages/banners/announcements-page.tsx b/src/pages/banners/announcements-page.tsx new file mode 100644 index 0000000..9508b3f --- /dev/null +++ b/src/pages/banners/announcements-page.tsx @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025, 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 { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Grid } from '@mui/material'; +import { GridTableRef } from '../../components/Grid'; +import { Announcement, UserAdminSrv } from '../../services'; +import { ColDef, GetRowIdParams, ValueGetterParams } from 'ag-grid-community'; +import AddAnnouncementForm from './add-announcement-form'; +import { DateCellRenderer } from './date-cell-renderer'; +import AgGrid from '../../components/Grid/AgGrid'; +import { useSnackMessage } from '@gridsuite/commons-ui'; +import { CancelButtonCellRenderer } from './cancel-button-cell-renderer'; +import { UUID } from 'crypto'; + +const stylesLayout = { + columnContainer: { + maxHeight: '60px', + paddingLeft: '15px', + }, +}; + +const defaultColDef: ColDef = { + editable: false, + resizable: true, + minWidth: 50, + cellRenderer: 'agAnimateSlideCellRenderer', + rowDrag: false, + sortable: true, +}; + +function getRowId(params: GetRowIdParams): string { + return params.data.id; +} + +const AnnouncementsPage: FunctionComponent = () => { + const intl = useIntl(); + const gridRef = useRef>(null); + + const { snackError } = useSnackMessage(); + + const [data, setData] = useState(null); + + const loadDataAndSave = useCallback( + function loadDataAndSave(): Promise { + return UserAdminSrv.fetchAnnouncementList().then(setData, (error) => { + snackError({ + messageTxt: error.message, + headerId: 'table.error.retrieve', + }); + }); + }, + [snackError] + ); + + const convertSeverity = useCallback( + (severity: string) => { + if (severity === UserAdminSrv.AnnouncementSeverity.INFO) { + return intl.formatMessage({ id: 'banners.table.info' }); + } else if (severity === UserAdminSrv.AnnouncementSeverity.WARN) { + return intl.formatMessage({ id: 'banners.table.warn' }); + } else { + return ''; + } + }, + [intl] + ); + + const refreshGrid = useCallback(() => { + gridRef.current?.context?.refresh(); + }, []); + + const handleDeleteAnnouncement = useCallback( + (announcementId: UUID) => { + UserAdminSrv.deleteAnnouncement(announcementId).then(() => { + refreshGrid(); + }); + }, + [refreshGrid] + ); + + const columns = useMemo( + (): ColDef[] => [ + { + field: 'message', + cellDataType: 'text', + flex: 3, + lockVisible: true, + headerName: intl.formatMessage({ id: 'banners.table.message' }), + }, + { + field: 'startDate', + cellRenderer: DateCellRenderer, + flex: 3, + lockVisible: true, + headerName: intl.formatMessage({ id: 'banners.table.startDate' }), + }, + { + field: 'endDate', + cellRenderer: DateCellRenderer, + flex: 3, + lockVisible: true, + headerName: intl.formatMessage({ id: 'banners.table.endDate' }), + }, + { + field: 'severity', + cellDataType: 'text', + flex: 2, + lockVisible: true, + headerName: intl.formatMessage({ id: 'banners.table.severity' }), + valueGetter: (value: ValueGetterParams) => convertSeverity(value.data.severity), + }, + { + field: 'id', + cellRenderer: CancelButtonCellRenderer, + cellRendererParams: { + onClickHandler: handleDeleteAnnouncement, + }, + flex: 2, + lockVisible: true, + headerName: intl.formatMessage({ id: 'banners.table.cancel' }), + }, + ], + [intl, convertSeverity, handleDeleteAnnouncement] + ); + + return ( + <> + + + +

+ +

+
+ + + +
+ + + +

+ +

+
+ + + + ref={gridRef} + rowData={data} + alwaysShowVerticalScroll={true} + onGridReady={loadDataAndSave} + columnDefs={columns} + defaultColDef={defaultColDef} + gridId="table-banners" + getRowId={getRowId} + context={useMemo( + () => ({ + refresh: loadDataAndSave, + }), + [loadDataAndSave] + )} + /> + + +
+
+ + ); +}; +export default AnnouncementsPage; diff --git a/src/pages/banners/cancel-button-cell-renderer.tsx b/src/pages/banners/cancel-button-cell-renderer.tsx new file mode 100644 index 0000000..bb7773f --- /dev/null +++ b/src/pages/banners/cancel-button-cell-renderer.tsx @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025, 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 { Box, IconButton } from '@mui/material'; +import { DisabledByDefault } from '@mui/icons-material'; +import { UUID } from 'crypto'; + +export type CancelButtonCellRendererProps = { value: UUID; onClickHandler: Function }; + +export function CancelButtonCellRenderer({ value, onClickHandler }: Readonly) { + return ( + + { + onClickHandler(value); + }} + > + + + + ); +} diff --git a/src/pages/banners/date-cell-renderer.tsx b/src/pages/banners/date-cell-renderer.tsx new file mode 100644 index 0000000..80d7021 --- /dev/null +++ b/src/pages/banners/date-cell-renderer.tsx @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025, 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 { useIntl } from 'react-intl'; +import { Box, Tooltip } from '@mui/material'; + +export type DateCellRendererProps = { value: string }; + +export function DateCellRenderer({ value }: Readonly) { + const intl = useIntl(); + + const dateValue = new Date(value); + if (!Number.isNaN(dateValue.getDate())) { + const time = new Intl.DateTimeFormat(intl.locale, { + timeStyle: 'medium', + hour12: false, + }).format(dateValue); + const displayedDate = + intl.locale === 'en' ? dateValue.toISOString().substring(0, 10) : dateValue.toLocaleDateString(intl.locale); + + const cellText = displayedDate + ' ' + time; + + const fullDate = new Intl.DateTimeFormat(intl.locale, { + dateStyle: 'long', + timeStyle: 'long', + hour12: false, + }).format(dateValue); + + return ( + + + {cellText} + + + ); + } +} diff --git a/src/pages/banners/index.ts b/src/pages/banners/index.ts new file mode 100644 index 0000000..79d51d8 --- /dev/null +++ b/src/pages/banners/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2025, 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 { default as Banners } from './announcements-page'; diff --git a/src/pages/index.ts b/src/pages/index.ts index bdfb9db..fd8439e 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -8,3 +8,4 @@ export * from './users'; export * from './profiles'; export * from './groups'; +export * from './banners'; diff --git a/src/routes/utils.tsx b/src/routes/utils.tsx index db8a25b..ef2f555 100644 --- a/src/routes/utils.tsx +++ b/src/routes/utils.tsx @@ -11,11 +11,13 @@ import ErrorPage from './ErrorPage'; import HomePage from './HomePage'; import { getPreLoginPath } from '@gridsuite/commons-ui'; import { FormattedMessage } from 'react-intl'; +import { Banners } from '../pages/banners'; export enum MainPaths { users = 'users', profiles = 'profiles', groups = 'groups', + banners = 'banners', } export function appRoutes(): RouteObject[] { @@ -49,6 +51,13 @@ export function appRoutes(): RouteObject[] { appBar_tab: MainPaths.groups, }, }, + { + path: `/${MainPaths.banners}`, + element: , + handle: { + appBar_tab: MainPaths.banners, + }, + }, ], }, { diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index fcd2636..c325065 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -284,3 +284,59 @@ export function addGroup(group: string): Promise { throw reason; }); } + +export enum AnnouncementSeverity { + INFO = 'INFO', + WARN = 'WARN', +} + +export function sanitizeString(val: string | null | undefined) { + const trimedValue = val?.trim(); + return trimedValue === '' ? null : trimedValue; +} + +export type Announcement = { + id: UUID; + startDate: string; + endDate: string; + message: string; + severity: string; +}; + +export function addAnnouncement(announcement: Announcement): Promise { + console.debug(`Creating announcement ...`); + return backendFetch( + `${USER_ADMIN_URL}/announcements?startDate=${announcement.startDate}&endDate=${announcement.endDate}&severity=${announcement.severity}`, + { + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: sanitizeString(announcement.message), + } + ) + .then(() => undefined) + .catch((reason) => { + console.error(`Error while creating announcement : ${reason}`); + throw reason; + }); +} + +export function fetchAnnouncementList(): Promise { + console.debug(`Fetching announcement ...`); + return backendFetchJson(`${USER_ADMIN_URL}/announcements`, { method: 'get' }).catch((reason) => { + console.error(`Error while fetching announcement : ${reason}`); + throw reason; + }) as Promise; +} + +export function deleteAnnouncement(announcementId: UUID): Promise { + console.debug(`Deleting announcement ...`); + return backendFetch(`${USER_ADMIN_URL}/announcements/${announcementId}`, { method: 'delete' }) + .then(() => undefined) + .catch((reason) => { + console.error(`Error while deleting announcement : ${reason}`); + throw reason; + }); +} diff --git a/src/translations/en.json b/src/translations/en.json index bd1a00e..7ca7ba9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -16,6 +16,7 @@ "appBar.tabs.profiles": "Profiles", "appBar.tabs.groups": "Groups", "appBar.tabs.connections": "Connections", + "appBar.tabs.warningBanner": "Warning banner", "table.noRows": "No data", "table.error.retrieve": "Error while retrieving data", @@ -105,5 +106,20 @@ "groups.form.field.group.label": "Group ID", "linked.path.display.noLink": "no configuration selected.", - "linked.path.display.invalidLink": "invalid configurations link." + "linked.path.display.invalidLink": "invalid configurations link.", + + "banners.programNewMessage": "New message scheduling", + "banners.programmedMessage": "Programmed messages", + "banners.table.message": "Message", + "banners.table.startDate": "Start date", + "banners.table.endDate": "Stop date", + "banners.table.severity": "Severity", + "banners.table.cancel": "Cancel", + "banners.table.info": "Info", + "banners.table.warn": "Warning", + "banners.form.message": "Warning message", + "errCreateAnnouncement": "Error while creating announcement: ", + "noOverlapAllowedErr": "The date overlaps with another announcement date", + "noSameDateErr": "The announcement start and end date must be different", + "startDateAfterEndDateErr": "The start date cannot be after the end date" } diff --git a/src/translations/fr.json b/src/translations/fr.json index 80ca69f..a1b46dd 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -16,6 +16,7 @@ "appBar.tabs.profiles": "Profils", "appBar.tabs.groups": "Groupes", "appBar.tabs.connections": "Connexions", + "appBar.tabs.warningBanner": "Bandeau d'avertissement", "table.noRows": "No data", "table.id": "ID", @@ -106,5 +107,20 @@ "groups.form.field.group.label": "ID groupe", "linked.path.display.noLink": "pas de configuration selectionnée.", - "linked.path.display.invalidLink": "lien vers configurations invalide." + "linked.path.display.invalidLink": "lien vers configurations invalide.", + + "banners.programNewMessage": "Programmer un nouveau message", + "banners.programmedMessage": "Messages programmés", + "banners.table.message": "Message", + "banners.table.startDate": "Date de début", + "banners.table.endDate": "Date de fin", + "banners.table.severity": "Sévérité", + "banners.table.cancel": "Annulation", + "banners.table.info": "Info", + "banners.table.warn": "Attention", + "banners.form.message": "Message d'avertissement", + "errCreateAnnouncement": "Erreur lors de la création de l'annonce : ", + "noOverlapAllowedErr": "La date d'annonce chevauche une autre date d'annonce", + "noSameDateErr": "La date de début et de fin d'annonce doivent être différentes", + "startDateAfterEndDateErr": "La date de début d'annonce ne peut pas être après la date de fin" } diff --git a/src/utils/api-ws.ts b/src/utils/api-ws.ts index 84b37a9..1689a76 100644 --- a/src/utils/api-ws.ts +++ b/src/utils/api-ws.ts @@ -9,10 +9,7 @@ import { getToken } from './api'; export type * from './api'; -export function getWsBase(): string { - // We use the `baseURI` (from `` in index.html) to build the URL, which is corrected by httpd/nginx - return document.baseURI.replace(/^http(s?):\/\//, 'ws$1://').replace(/\/+$/, '') + import.meta.env.VITE_WS_GATEWAY; -} +export const getWsBase = () => document.baseURI.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://'); export function getUrlWithToken(baseUrl: string): string { const querySymbol = baseUrl.includes('?') ? '&' : '?'; diff --git a/src/utils/error.ts b/src/utils/error.ts index af9d81c..d9e0537 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -24,3 +24,30 @@ export function getErrorMessage(error: unknown): string | null { return JSON.stringify(error ?? undefined) ?? null; } } + +const OVERLAPPING_ANNOUNCEMENTS = 'OVERLAPPING_ANNOUNCEMENTS'; +const SAME_START_END_DATE = 'SAME_START_END_DATE'; +const START_DATE_AFTER_END_DATE = 'START_DATE_AFTER_END_DATE'; + +export function handleAnnouncementCreationErrors(error: string, snackError: Function): boolean { + if (error.includes(OVERLAPPING_ANNOUNCEMENTS)) { + snackError({ + headerId: 'errCreateAnnouncement', + messageId: 'noOverlapAllowedErr', + }); + return true; + } else if (error.includes(SAME_START_END_DATE)) { + snackError({ + headerId: 'errCreateAnnouncement', + messageId: 'noSameDateErr', + }); + return true; + } else if (error.includes(START_DATE_AFTER_END_DATE)) { + snackError({ + headerId: 'errCreateAnnouncement', + messageId: 'startDateAfterEndDateErr', + }); + return true; + } + return false; +} diff --git a/src/utils/notifications-provider.ts b/src/utils/notifications-provider.ts index 6f43da9..8abcfd0 100644 --- a/src/utils/notifications-provider.ts +++ b/src/utils/notifications-provider.ts @@ -10,12 +10,7 @@ import { useSelector } from 'react-redux'; import type { AppState } from '../redux/reducer'; import { getUrlWithToken, getWsBase } from './api-ws'; import { APP_NAME } from './config-params'; - -export enum NOTIFICATIONS_URL_KEYS { - CONFIG = 'CONFIG', -} - -export const PREFIX_CONFIG_NOTIFICATION_WS = '/config-notification'; +import { NotificationsUrlKeys, PREFIX_CONFIG_NOTIFICATION_WS } from '@gridsuite/commons-ui'; export function useNotificationsUrlGenerator() { // The websocket API doesn't allow relative urls @@ -27,14 +22,17 @@ export function useNotificationsUrlGenerator() { return useMemo( () => ({ - [NOTIFICATIONS_URL_KEYS.CONFIG]: tokenId + [NotificationsUrlKeys.CONFIG]: tokenId ? getUrlWithToken( `${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/notify?${new URLSearchParams({ appName: APP_NAME, })}` ) : undefined, - }) satisfies Record, + [NotificationsUrlKeys.GLOBAL_CONFIG]: tokenId + ? getUrlWithToken(`${wsBase}${PREFIX_CONFIG_NOTIFICATION_WS}/global`) + : undefined, + }) satisfies Partial>, [wsBase, tokenId] ); }