From f83ece580c321f86d73ef4507d216700f73500fa Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 28 Mar 2024 15:16:49 +0100 Subject: [PATCH 1/8] Update local port to 3003 Signed-off-by: Florent MILLOT --- package.json | 2 +- public/idpSettings.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f1fe50c..0c46ebc 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "yup": "^1.2.0" }, "scripts": { - "start": "react-scripts start", + "start": "PORT=3003 react-scripts start", "build": "react-scripts build", "test": "react-scripts test --watchAll=false", "test:watch": "react-scripts test", diff --git a/public/idpSettings.json b/public/idpSettings.json index 80a89b2..97d79a0 100644 --- a/public/idpSettings.json +++ b/public/idpSettings.json @@ -1,8 +1,8 @@ { "authority": "http://172.17.0.1:9090/", - "client_id": "my-client-2", - "redirect_uri": "http://localhost:3000/sign-in-callback", - "post_logout_redirect_uri": "http://localhost:3000/logout-callback", - "silent_redirect_uri": "http://localhost:3000/silent-renew-callback", + "client_id": "gridadmin-local", + "redirect_uri": "http://localhost:3003/sign-in-callback", + "post_logout_redirect_uri": "http://localhost:3003/logout-callback", + "silent_redirect_uri": "http://localhost:3003/silent-renew-callback", "scope": "openid" } From 19b8738a1c24aa790bf35671a5b1cf1effe2398e Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 28 Mar 2024 17:40:01 +0100 Subject: [PATCH 2/8] Update redirect URIs to use port 3002 instead of 3003. Signed-off-by: Florent MILLOT --- public/idpSettings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/idpSettings.json b/public/idpSettings.json index 97d79a0..ad8a68c 100644 --- a/public/idpSettings.json +++ b/public/idpSettings.json @@ -1,8 +1,8 @@ { "authority": "http://172.17.0.1:9090/", "client_id": "gridadmin-local", - "redirect_uri": "http://localhost:3003/sign-in-callback", - "post_logout_redirect_uri": "http://localhost:3003/logout-callback", - "silent_redirect_uri": "http://localhost:3003/silent-renew-callback", + "redirect_uri": "http://localhost:3002/sign-in-callback", + "post_logout_redirect_uri": "http://localhost:3002/logout-callback", + "silent_redirect_uri": "http://localhost:3002/silent-renew-callback", "scope": "openid" } From 82167636373a64aa9e78912d48814fd18f2bec61 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 28 Mar 2024 17:45:28 +0100 Subject: [PATCH 3/8] Update start script port number to 3002 instead of 3003. Signed-off-by: Florent MILLOT --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c46ebc..9ca4cb1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "yup": "^1.2.0" }, "scripts": { - "start": "PORT=3003 react-scripts start", + "start": "PORT=3002 react-scripts start", "build": "react-scripts build", "test": "react-scripts test --watchAll=false", "test:watch": "react-scripts test", From c80678ec23c0ad752d47887a76495ee0196fd697 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Fri, 29 Mar 2024 16:43:56 +0100 Subject: [PATCH 4/8] Announcements Signed-off-by: Florent MILLOT --- package-lock.json | 22 ++-- package.json | 5 +- src/components/App/app-top-bar.tsx | 11 ++ src/pages/announcements/Announcements.tsx | 51 +++++++++ .../CreateAnnouncementDialog.tsx | 87 ++++++++++++++ src/pages/announcements/DurationInput.tsx | 73 ++++++++++++ .../announcements/ListItemAnnouncement.tsx | 77 +++++++++++++ src/pages/announcements/index.ts | 8 ++ src/pages/announcements/useAnnouncements.ts | 54 +++++++++ src/pages/announcements/utils.ts | 106 ++++++++++++++++++ src/routes/router.tsx | 25 ++--- src/services/user-admin.ts | 42 +++++++ src/translations/en.json | 13 ++- src/translations/fr.json | 13 ++- src/utils/yup-config.ts | 23 ++++ 15 files changed, 585 insertions(+), 25 deletions(-) create mode 100644 src/pages/announcements/Announcements.tsx create mode 100644 src/pages/announcements/CreateAnnouncementDialog.tsx create mode 100644 src/pages/announcements/DurationInput.tsx create mode 100644 src/pages/announcements/ListItemAnnouncement.tsx create mode 100644 src/pages/announcements/index.ts create mode 100644 src/pages/announcements/useAnnouncements.ts create mode 100644 src/pages/announcements/utils.ts create mode 100644 src/utils/yup-config.ts diff --git a/package-lock.json b/package-lock.json index c7e1459..8b96e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "prop-types": "^15.7.2", "react": "^18.0.0", "react-dom": "^18.0.0", - "react-hook-form": "^7.46.1", + "react-hook-form": "^7.51.1", "react-intl": "^6.0.0", "react-redux": "^8.0.0", "react-router-dom": "^6.0.0", @@ -39,10 +39,11 @@ "react-window": "^1.8.5", "reconnecting-websocket": "^4.4.0", "redux": "^4.0.5", + "tinyduration": "^3.3.0", "type-fest": "^4.11.1", "typeface-roboto": "^1.0.0", "typescript": "^5.1.3", - "yup": "^1.2.0" + "yup": "^1.4.0" }, "devDependencies": { "@types/eslint-config-prettier": "^6.11.3", @@ -14818,9 +14819,9 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, "node_modules/react-hook-form": { - "version": "7.46.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.46.1.tgz", - "integrity": "sha512-0GfI31LRTBd5tqbXMGXT1Rdsv3rnvy0FjEk8Gn9/4tp6+s77T7DPZuGEpBRXOauL+NhyGT5iaXzdIM2R6F/E+w==", + "version": "7.51.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.1.tgz", + "integrity": "sha512-ifnBjl+kW0ksINHd+8C/Gp6a4eZOdWyvRv0UBaByShwU8JbVx5hTcTWEcd5VdybvmPTATkVVXk9npXArHmo56w==", "engines": { "node": ">=12.22.0" }, @@ -16674,6 +16675,11 @@ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" }, + "node_modules/tinyduration": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/tinyduration/-/tinyduration-3.3.0.tgz", + "integrity": "sha512-sLR0iVUnnnyGEX/a3jhTA0QMK7UvakBqQJFLiibiuEYL6U1L85W+qApTZj6DcL1uoWQntYuL0gExoe9NU5B3PA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -18023,9 +18029,9 @@ } }, "node_modules/yup": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", - "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", diff --git a/package.json b/package.json index 9ca4cb1..ed0f62d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "prop-types": "^15.7.2", "react": "^18.0.0", "react-dom": "^18.0.0", - "react-hook-form": "^7.46.1", + "react-hook-form": "^7.51.1", "react-intl": "^6.0.0", "react-redux": "^8.0.0", "react-router-dom": "^6.0.0", @@ -44,10 +44,11 @@ "react-window": "^1.8.5", "reconnecting-websocket": "^4.4.0", "redux": "^4.0.5", + "tinyduration": "^3.3.0", "type-fest": "^4.11.1", "typeface-roboto": "^1.0.0", "typescript": "^5.1.3", - "yup": "^1.2.0" + "yup": "^1.4.0" }, "scripts": { "start": "PORT=3002 react-scripts start", diff --git a/src/components/App/app-top-bar.tsx b/src/components/App/app-top-bar.tsx index 56a95b9..414607d 100644 --- a/src/components/App/app-top-bar.tsx +++ b/src/components/App/app-top-bar.tsx @@ -14,6 +14,7 @@ import { useState, } from 'react'; import { capitalize, Tab, TabProps, Tabs, useTheme } from '@mui/material'; +import { Announcement } from '@mui/icons-material'; import { PeopleAlt } from '@mui/icons-material'; import { logout, TopBar } from '@gridsuite/commons-ui'; import { useParameterState } from '../parameters'; @@ -56,6 +57,16 @@ const tabs = new Map([ key={`tab-${MainPaths.users}`} />, ], + [ + MainPaths.announcements, + } + label={} + href={`/${MainPaths.announcements}`} + value={MainPaths.announcements} + key={`tab-${MainPaths.announcements}`} + />, + ], ]); const AppTopBar: FunctionComponent = () => { diff --git a/src/pages/announcements/Announcements.tsx b/src/pages/announcements/Announcements.tsx new file mode 100644 index 0000000..38c6179 --- /dev/null +++ b/src/pages/announcements/Announcements.tsx @@ -0,0 +1,51 @@ +/* + * Copyright (c) 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 { FunctionComponent, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { AppBar, Button, Grid, List, Toolbar } from '@mui/material'; +import { AddCircleOutline } from '@mui/icons-material'; +import { CreateAnnouncementDialog } from './CreateAnnouncementDialog'; +import { useAnnouncements } from './useAnnouncements'; +import { ListItemAnnouncement } from './ListItemAnnouncement'; + +const Announcements: FunctionComponent = () => { + const intl = useIntl(); + const [openDialog, setOpenDialog] = useState(false); + const announcements = useAnnouncements(); + + return ( + + + + + + + + + setOpenDialog(false)} + /> + + + {announcements.map((announcement) => ( + + ))} + + + + ); +}; +export default Announcements; diff --git a/src/pages/announcements/CreateAnnouncementDialog.tsx b/src/pages/announcements/CreateAnnouncementDialog.tsx new file mode 100644 index 0000000..be25fd2 --- /dev/null +++ b/src/pages/announcements/CreateAnnouncementDialog.tsx @@ -0,0 +1,87 @@ +/* + * Copyright (c) 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 { FunctionComponent, useCallback } from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, +} from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { FormProvider, useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { DurationInput } from './DurationInput'; +import { SubmitButton, TextInput } from '@gridsuite/commons-ui'; +import { + AnnouncementFormData, + emptyFormData, + formSchema, + fromFrontToBack, + MESSAGE, +} from './utils'; +import { UserAdminSrv } from '../../services'; + +interface CreateAnnouncementDialogProps { + open: boolean; + onClose: () => void; +} + +export const CreateAnnouncementDialog: FunctionComponent< + CreateAnnouncementDialogProps +> = (props) => { + const formMethods = useForm({ + defaultValues: emptyFormData, + //@ts-ignore because yup TS is broken + resolver: yupResolver(formSchema), + }); + + const { handleSubmit, reset } = formMethods; + + const onSubmit = useCallback( + (formData: AnnouncementFormData) => { + UserAdminSrv.createAnnouncement(fromFrontToBack(formData)); + reset(); + props.onClose(); + }, + [props, reset] + ); + + return ( + //@ts-ignore because RHF TS is broken + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/pages/announcements/DurationInput.tsx b/src/pages/announcements/DurationInput.tsx new file mode 100644 index 0000000..b194100 --- /dev/null +++ b/src/pages/announcements/DurationInput.tsx @@ -0,0 +1,73 @@ +/* + * Copyright (c) 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 { Grid, Typography } from '@mui/material'; +import { DAYS, DURATION, HOURS, MINUTES } from './utils'; +import { IntegerInput } from '@gridsuite/commons-ui'; +import { useIntl } from 'react-intl'; + +const centerStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}; + +const DaysAdornment = { + position: 'end', + text: 'd', +}; +const HoursAdornment = { + position: 'end', + text: 'h', +}; +const MinutesAdornment = { + position: 'end', + text: 'm', +}; + +export const DurationInput = () => { + const intl = useIntl(); + + return ( + + + + {intl.formatMessage({ + id: 'announcements.dialog.duration', + })} + + + + + + + : + + + + + + : + + + + + + ); +}; diff --git a/src/pages/announcements/ListItemAnnouncement.tsx b/src/pages/announcements/ListItemAnnouncement.tsx new file mode 100644 index 0000000..6bf8939 --- /dev/null +++ b/src/pages/announcements/ListItemAnnouncement.tsx @@ -0,0 +1,77 @@ +/* + * Copyright (c) 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 { + IconButton, + ListItem, + ListItemIcon, + ListItemText, +} from '@mui/material'; +import { FunctionComponent } from 'react'; +import { + Announcement, + DATE, + DAYS, + DURATION, + HOURS, + ID, + MESSAGE, + MINUTES, +} from './utils'; +import { Cancel, Message, ScheduleSend, Timelapse } from '@mui/icons-material'; +import { UserAdminSrv } from '../../services'; + +export const ListItemAnnouncement: FunctionComponent = ( + announcement +) => { + return ( + + UserAdminSrv.deleteAnnouncement(announcement[ID]) + } + edge="end" + aria-label="delete" + > + + + } + sx={{ minHeight: 100 }} + > + + + + + + + + + + + + + + ); +}; diff --git a/src/pages/announcements/index.ts b/src/pages/announcements/index.ts new file mode 100644 index 0000000..e53ae07 --- /dev/null +++ b/src/pages/announcements/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 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/. + */ + +export { default as Announcements } from './Announcements'; diff --git a/src/pages/announcements/useAnnouncements.ts b/src/pages/announcements/useAnnouncements.ts new file mode 100644 index 0000000..efd1e63 --- /dev/null +++ b/src/pages/announcements/useAnnouncements.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 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 { useEffect, useState } from 'react'; +import ReconnectingWebSocket from 'reconnecting-websocket'; +import { Announcement, fromBackToFront } from './utils'; +import { UserAdminSrv } from '../../services'; +import { getUrlWithToken, getWsBase } from '../../utils/api-ws'; + +const PREFIX_CONFIG_NOTIFICATION_WS = `${getWsBase()}/config-notification`; +const webSocketUrl = `${PREFIX_CONFIG_NOTIFICATION_WS}/global`; + +export function useAnnouncements() { + const [announcements, setAnnouncements] = useState([]); + + useEffect(() => { + function getAnnouncements() { + UserAdminSrv.getAnnouncements().then((announcements) => + setAnnouncements( + announcements.map((announcement) => + fromBackToFront(announcement) + ) + ) + ); + } + + const rws = new ReconnectingWebSocket(() => + getUrlWithToken(webSocketUrl) + ); + + rws.addEventListener('open', () => { + console.info('WebSocket for announcements is connected'); + // We retrieve the announcements at start + getAnnouncements(); + }); + + rws.addEventListener('message', (event) => { + // When new message, we just fetch back the latest list of announcements + getAnnouncements(); + }); + + rws.addEventListener('error', (event) => { + console.error('Unexpected announcements WebSocket error : ', event); + }); + + return () => rws.close(); + }, []); + + return announcements; +} diff --git a/src/pages/announcements/utils.ts b/src/pages/announcements/utils.ts new file mode 100644 index 0000000..3436282 --- /dev/null +++ b/src/pages/announcements/utils.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 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 yup from '../../utils/yup-config'; +import { Duration, parse, serialize } from 'tinyduration'; + +export const ID = 'id'; +export const DATE = 'creationDate'; +export const MESSAGE = 'message'; +export const DURATION = 'duration'; +export const DAYS = 'days'; +export const HOURS = 'hours'; +export const MINUTES = 'minutes'; + +export interface DurationFormData { + [DAYS]: number | null; + [HOURS]: number | null; + [MINUTES]: number | null; +} + +export interface AnnouncementFormData { + [MESSAGE]: string; + [DURATION]: DurationFormData; +} + +export const emptyFormData: AnnouncementFormData = { + [MESSAGE]: '', + [DURATION]: { + [DAYS]: null, + [HOURS]: null, + [MINUTES]: null, + }, +}; + +// const formSchema: ObjectSchema = yup +export const formSchema = yup + .object() + .shape({ + [MESSAGE]: yup.string().max(100).required(), + [DURATION]: yup + .object() + .shape( + { + [DAYS]: getDurationUnitSchema(HOURS, MINUTES), + [HOURS]: getDurationUnitSchema(DAYS, MINUTES), + [MINUTES]: getDurationUnitSchema(DAYS, HOURS), + }, + // to avoid cyclic dependencies + [ + [HOURS, MINUTES], + [DAYS, MINUTES], + [DAYS, HOURS], + ] + ) + .required(), + }) + .required(); + +function getDurationUnitSchema(otherUnitName1: string, otherUnitName2: string) { + return yup.number().when([otherUnitName1, otherUnitName2], { + is: (v1: number | null, v2: number | null) => + v1 === null && v2 === null, + then: (schema) => schema.required(), + otherwise: (schema) => schema.nullable(), + }); +} + +export interface AnnouncementServerData { + [ID]: string; + [DATE]: string; + [MESSAGE]: string; + [DURATION]: string; +} + +export function fromFrontToBack(formData: AnnouncementFormData) { + return { + [MESSAGE]: formData[MESSAGE], + [DURATION]: serialize(formData[DURATION] as Duration), // null values also works + }; +} + +export interface Announcement extends AnnouncementFormData { + [ID]: string; + [DATE]: string; +} + +export function fromBackToFront( + serverData: AnnouncementServerData +): Announcement { + // In server side, duration is stored in hours only, so we need to compute the days + const duration = parse(serverData[DURATION]); + if (duration[HOURS] && duration[HOURS] >= 24) { + duration[DAYS] = Math.trunc(duration[HOURS] / 24); + duration[HOURS] %= 24; + } + return { + [ID]: serverData[ID], + [DATE]: new Date(serverData[DATE]).toLocaleString(), + [MESSAGE]: serverData[MESSAGE], + [DURATION]: duration as DurationFormData, + }; +} diff --git a/src/routes/router.tsx b/src/routes/router.tsx index c6a6a7c..dc465e1 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -5,19 +5,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { - FunctionComponent, - PropsWithChildren, - useEffect, - useMemo, - useState, -} from 'react'; +import { FunctionComponent, PropsWithChildren, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { - AuthenticationRouter, - getPreLoginPath, - initializeAuthenticationProd, -} from '@gridsuite/commons-ui'; +import { AuthenticationRouter, getPreLoginPath, initializeAuthenticationProd } from '@gridsuite/commons-ui'; import { createBrowserRouter, Navigate, @@ -26,7 +16,7 @@ import { RouterProvider, useLocation, useMatch, - useNavigate, + useNavigate } from 'react-router-dom'; import { UserManager } from 'oidc-client'; import { useDispatch, useSelector } from 'react-redux'; @@ -38,9 +28,11 @@ import ErrorPage from './ErrorPage'; import { updateUserManagerDestructured } from '../redux/actions'; import HomePage from './HomePage'; import { getErrorMessage } from '../utils/error'; +import { Announcements } from '../pages/announcements'; export enum MainPaths { users = 'users', + announcements = 'announcements', } export function appRoutes(): RouteObject[] { @@ -60,6 +52,13 @@ export function appRoutes(): RouteObject[] { appBar_tab: MainPaths.users, }, }, + { + path: `/${MainPaths.announcements}`, + element: , + handle: { + appBar_tab: MainPaths.announcements, + }, + }, ], }, { diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index 05079d2..0773f56 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -8,6 +8,7 @@ import { backendFetch, backendFetchJson, getRestBase } from '../utils/api-rest'; import { extractUserSub, getToken, getUser } from '../utils/api'; import { User } from '../utils/auth'; +import { AnnouncementServerData, DURATION, MESSAGE } from '../pages/announcements/utils'; const USER_ADMIN_URL = `${getRestBase()}/user-admin/v1`; @@ -95,3 +96,44 @@ export function addUser(sub: string): Promise { throw reason; }); } + +export function getAnnouncements(): Promise { + console.debug('Getting list of announcements...'); + return backendFetchJson(`${USER_ADMIN_URL}/announcements`, { + method: 'get', + cache: 'default', + }).catch((reason) => { + console.error( + `Error while getting the list of announcements : ${reason}` + ); + throw reason; + }) as Promise; +} + +export function createAnnouncement(announcement: { + [MESSAGE]: string; + [DURATION]: string; +}) { + const body = JSON.stringify(announcement); + console.debug('Creating announcement...' + body); + return backendFetch(`${USER_ADMIN_URL}/announcements`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + }, + body, + }).catch((reason) => { + console.error(`Error while creating announcement : ${reason}`); + throw reason; + }); +} + +export function deleteAnnouncement(id: string) { + console.debug('Deleting announcement with ID ' + id); + return backendFetch(`${USER_ADMIN_URL}/announcements/${id}`, { + method: 'delete', + }).catch((reason) => { + console.error(`Error while deleting announcement : ${reason}`); + throw reason; + }); +} diff --git a/src/translations/en.json b/src/translations/en.json index ddc62de..7cfd649 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -10,6 +10,7 @@ "paramsRetrievingError": "An error occurred while retrieving the parameters", "appBar.tabs.users": "Users", + "appBar.tabs.announcement": "Announcements", "appBar.tabs.connections": "Connections", "table.noRows": "No data", @@ -34,5 +35,15 @@ "users.table.toolbar.add.label": "Add a user", "users.form.title": "Add a user", "users.form.content": "Please fill in new user data.", - "users.form.field.username.label": "User ID" + "users.form.field.username.label": "User ID", + + "announcements.add": "New", + "announcements.dialog.title": "Announcement", + "announcements.dialog.input": "Message to send", + "announcements.dialog.duration": "Duration : ", + "duration.days": "Days", + "duration.hours": "Hours", + "duration.minutes": "Minutes", + + "YupRequired": "Required" } diff --git a/src/translations/fr.json b/src/translations/fr.json index 8f67967..d67904e 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -10,6 +10,7 @@ "paramsRetrievingError": "Une erreur est survenue lors de la récupération des paramètres", "appBar.tabs.users": "Utilisateurs", + "appBar.tabs.announcement": "Annonces", "appBar.tabs.connections": "Connexions", "table.noRows": "No data", @@ -34,5 +35,15 @@ "users.table.toolbar.add.label": "Ajouter un utilisateur", "users.form.title": "Ajouter un utilisateur", "users.form.content": "Veuillez renseigner les informations de l'utilisateur.", - "users.form.field.username.label": "ID utilisateur" + "users.form.field.username.label": "ID utilisateur", + + "announcements.add": "Nouveau", + "announcements.dialog.title": "Annonce", + "announcements.dialog.input": "Message à envoyer", + "announcements.dialog.duration": "Durée : ", + "duration.days": "Jours", + "duration.hours": "Heures", + "duration.minutes": "Minutes", + + "YupRequired": "Requis" } diff --git a/src/utils/yup-config.ts b/src/utils/yup-config.ts new file mode 100644 index 0000000..b918c6a --- /dev/null +++ b/src/utils/yup-config.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 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 * as yup from 'yup'; + +yup.setLocale({ + mixed: { + required: 'YupRequired', + notType: ({ type }) => { + if (type === 'number') { + return 'YupNotTypeNumber'; + } else { + return 'YupNotTypeDefault'; + } + }, + }, +}); + +export default yup; From c4141fdbb78e284659c99f27abc4f38f1b301db3 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Fri, 29 Mar 2024 16:49:35 +0100 Subject: [PATCH 5/8] prettier Signed-off-by: Florent MILLOT --- src/routes/router.tsx | 16 +++++++++++++--- src/services/user-admin.ts | 6 +++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/routes/router.tsx b/src/routes/router.tsx index dc465e1..0570a67 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -5,9 +5,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { FunctionComponent, PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { + FunctionComponent, + PropsWithChildren, + useEffect, + useMemo, + useState, +} from 'react'; import { FormattedMessage } from 'react-intl'; -import { AuthenticationRouter, getPreLoginPath, initializeAuthenticationProd } from '@gridsuite/commons-ui'; +import { + AuthenticationRouter, + getPreLoginPath, + initializeAuthenticationProd, +} from '@gridsuite/commons-ui'; import { createBrowserRouter, Navigate, @@ -16,7 +26,7 @@ import { RouterProvider, useLocation, useMatch, - useNavigate + useNavigate, } from 'react-router-dom'; import { UserManager } from 'oidc-client'; import { useDispatch, useSelector } from 'react-redux'; diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index 0773f56..abddba1 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -8,7 +8,11 @@ import { backendFetch, backendFetchJson, getRestBase } from '../utils/api-rest'; import { extractUserSub, getToken, getUser } from '../utils/api'; import { User } from '../utils/auth'; -import { AnnouncementServerData, DURATION, MESSAGE } from '../pages/announcements/utils'; +import { + AnnouncementServerData, + DURATION, + MESSAGE, +} from '../pages/announcements/utils'; const USER_ADMIN_URL = `${getRestBase()}/user-admin/v1`; From f4604c218e102afc9b3ece59da5a20369c0749a8 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Tue, 2 Apr 2024 12:56:59 +0200 Subject: [PATCH 6/8] Add default color to AppBar in Announcements component. Signed-off-by: Florent MILLOT --- src/pages/announcements/Announcements.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/announcements/Announcements.tsx b/src/pages/announcements/Announcements.tsx index 38c6179..5ad7833 100644 --- a/src/pages/announcements/Announcements.tsx +++ b/src/pages/announcements/Announcements.tsx @@ -21,7 +21,7 @@ const Announcements: FunctionComponent = () => { return ( - + - - + + + { {announcements.map((announcement) => ( - + ))} diff --git a/src/pages/announcements/CreateAnnouncementDialog.tsx b/src/pages/announcements/CreateAnnouncementDialog.tsx index be25fd2..fb51ebf 100644 --- a/src/pages/announcements/CreateAnnouncementDialog.tsx +++ b/src/pages/announcements/CreateAnnouncementDialog.tsx @@ -17,7 +17,11 @@ import { FormattedMessage } from 'react-intl'; import { FormProvider, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { DurationInput } from './DurationInput'; -import { SubmitButton, TextInput } from '@gridsuite/commons-ui'; +import { + SubmitButton, + TextInput, + useSnackMessage, +} from '@gridsuite/commons-ui'; import { AnnouncementFormData, emptyFormData, @@ -35,6 +39,8 @@ interface CreateAnnouncementDialogProps { export const CreateAnnouncementDialog: FunctionComponent< CreateAnnouncementDialogProps > = (props) => { + const { snackError } = useSnackMessage(); + const formMethods = useForm({ defaultValues: emptyFormData, //@ts-ignore because yup TS is broken @@ -45,11 +51,17 @@ export const CreateAnnouncementDialog: FunctionComponent< const onSubmit = useCallback( (formData: AnnouncementFormData) => { - UserAdminSrv.createAnnouncement(fromFrontToBack(formData)); + UserAdminSrv.createAnnouncement(fromFrontToBack(formData)).catch( + (error) => + snackError({ + messageTxt: error.message, + headerId: 'announcements.error.add', + }) + ); reset(); props.onClose(); }, - [props, reset] + [props, reset, snackError] ); return ( diff --git a/src/pages/announcements/ListItemAnnouncement.tsx b/src/pages/announcements/ListItemAnnouncement.tsx index 6bf8939..e12ba04 100644 --- a/src/pages/announcements/ListItemAnnouncement.tsx +++ b/src/pages/announcements/ListItemAnnouncement.tsx @@ -24,17 +24,25 @@ import { } from './utils'; import { Cancel, Message, ScheduleSend, Timelapse } from '@mui/icons-material'; import { UserAdminSrv } from '../../services'; +import { useSnackMessage } from '@gridsuite/commons-ui'; export const ListItemAnnouncement: FunctionComponent = ( announcement ) => { + const { snackError } = useSnackMessage(); + return ( - UserAdminSrv.deleteAnnouncement(announcement[ID]) + UserAdminSrv.deleteAnnouncement(announcement[ID]).catch( + (error) => + snackError({ + messageTxt: error.message, + headerId: 'announcements.error.delete', + }) + ) } edge="end" aria-label="delete" diff --git a/src/pages/announcements/useAnnouncements.ts b/src/pages/announcements/useAnnouncements.ts index efd1e63..3cecdb2 100644 --- a/src/pages/announcements/useAnnouncements.ts +++ b/src/pages/announcements/useAnnouncements.ts @@ -10,22 +10,32 @@ import ReconnectingWebSocket from 'reconnecting-websocket'; import { Announcement, fromBackToFront } from './utils'; import { UserAdminSrv } from '../../services'; import { getUrlWithToken, getWsBase } from '../../utils/api-ws'; +import { useSnackMessage } from '@gridsuite/commons-ui'; const PREFIX_CONFIG_NOTIFICATION_WS = `${getWsBase()}/config-notification`; const webSocketUrl = `${PREFIX_CONFIG_NOTIFICATION_WS}/global`; export function useAnnouncements() { + const { snackError } = useSnackMessage(); + const [announcements, setAnnouncements] = useState([]); useEffect(() => { function getAnnouncements() { - UserAdminSrv.getAnnouncements().then((announcements) => - setAnnouncements( - announcements.map((announcement) => - fromBackToFront(announcement) + UserAdminSrv.getAnnouncements() + .then((announcements) => + setAnnouncements( + announcements.map((announcement) => + fromBackToFront(announcement) + ) ) ) - ); + .catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'announcements.error.get', + }) + ); } const rws = new ReconnectingWebSocket(() => @@ -48,7 +58,7 @@ export function useAnnouncements() { }); return () => rws.close(); - }, []); + }, [snackError]); return announcements; } diff --git a/src/pages/utils/CustomToolbar.tsx b/src/pages/utils/CustomToolbar.tsx new file mode 100644 index 0000000..511671a --- /dev/null +++ b/src/pages/utils/CustomToolbar.tsx @@ -0,0 +1,26 @@ +import { AppBar, Box, Toolbar } from '@mui/material'; +import { FunctionComponent, PropsWithChildren } from 'react'; + +export const CustomToolbar: FunctionComponent = (props) => { + return ( + + ({ + marginLeft: 1, + '& > *': { + // mui's button set it own margin on itself... + marginRight: `${theme.spacing(1)} !important`, + '&:last-child': { + marginRight: '0 !important', + }, + }, + })} + > + {props.children} + + + + ); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 7cfd649..fa440b5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -41,6 +41,9 @@ "announcements.dialog.title": "Announcement", "announcements.dialog.input": "Message to send", "announcements.dialog.duration": "Duration : ", + "announcements.error.get": "Error while getting the list of announcements", + "announcements.error.add": "Error while creating announcement", + "announcements.error.delete": "Error while deleting announcement", "duration.days": "Days", "duration.hours": "Hours", "duration.minutes": "Minutes", diff --git a/src/translations/fr.json b/src/translations/fr.json index d67904e..55eefd8 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -41,6 +41,9 @@ "announcements.dialog.title": "Annonce", "announcements.dialog.input": "Message à envoyer", "announcements.dialog.duration": "Durée : ", + "announcements.error.get": "Erreur lors de la récupération de la liste des annonces", + "announcements.error.add": "Erreur lors de la création d'une annonce", + "announcements.error.delete": "Erreur lors de la suppression d'une annonce", "duration.days": "Jours", "duration.hours": "Heures", "duration.minutes": "Minutes", From 82bcfc33f9db1337ae5055c254ea39f9138c41e4 Mon Sep 17 00:00:00 2001 From: Abdelsalem Date: Tue, 17 Sep 2024 13:28:38 +0200 Subject: [PATCH 8/8] update code to work with main Signed-off-by: Abdelsalem --- src/components/App/app-top-bar.tsx | 7 ++- src/components/Grid/GridTable.tsx | 8 +-- src/pages/announcements/Announcements.tsx | 10 +--- .../CreateAnnouncementDialog.tsx | 48 +++++------------- src/pages/announcements/DurationInput.tsx | 18 ++----- .../announcements/ListItemAnnouncement.tsx | 50 +++++-------------- src/pages/announcements/useAnnouncements.ts | 10 +--- src/pages/announcements/utils.ts | 7 +-- src/routes/router.tsx | 6 +-- src/services/user-admin.ts | 16 ++---- src/translations/en.json | 3 +- src/translations/fr.json | 2 +- 12 files changed, 48 insertions(+), 137 deletions(-) diff --git a/src/components/App/app-top-bar.tsx b/src/components/App/app-top-bar.tsx index 445c2dc..bf7f099 100644 --- a/src/components/App/app-top-bar.tsx +++ b/src/components/App/app-top-bar.tsx @@ -9,7 +9,6 @@ import { forwardRef, FunctionComponent, ReactElement, useEffect, useMemo, useSta import { capitalize, Tab, Tabs, useTheme } from '@mui/material'; import { ManageAccounts, PeopleAlt } from '@mui/icons-material'; import { Announcement } from '@mui/icons-material'; -import { PeopleAlt } from '@mui/icons-material'; import { logout, TopBar } from '@gridsuite/commons-ui'; import { useParameterState } from '../parameters'; import { APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../../utils/config-params'; @@ -55,12 +54,16 @@ const tabs = new Map([ ], [ MainPaths.announcements, - } label={} href={`/${MainPaths.announcements}`} value={MainPaths.announcements} key={`tab-${MainPaths.announcements}`} + iconPosition="start" + LinkComponent={forwardRef((props, ref) => ( + + ))} />, ], ]); diff --git a/src/components/Grid/GridTable.tsx b/src/components/Grid/GridTable.tsx index 3032e34..8b81cd9 100644 --- a/src/components/Grid/GridTable.tsx +++ b/src/components/Grid/GridTable.tsx @@ -18,13 +18,7 @@ import { useMemo, useState, } from 'react'; -import { - Button as MuiButton, - ButtonProps, - ButtonTypeMap, - ExtendButtonBaseTypeMap, - Grid, -} from '@mui/material'; +import { Button as MuiButton, ButtonProps, ButtonTypeMap, ExtendButtonBaseTypeMap, Grid } from '@mui/material'; import { OverridableComponent, OverridableTypeMap, OverrideProps } from '@mui/material/OverridableComponent'; import { Delete } from '@mui/icons-material'; import { AgGrid, AgGridRef } from './AgGrid'; diff --git a/src/pages/announcements/Announcements.tsx b/src/pages/announcements/Announcements.tsx index df6240d..07732f1 100644 --- a/src/pages/announcements/Announcements.tsx +++ b/src/pages/announcements/Announcements.tsx @@ -34,17 +34,11 @@ const Announcements: FunctionComponent = () => { - setOpenDialog(false)} - /> + setOpenDialog(false)} /> {announcements.map((announcement) => ( - + ))} diff --git a/src/pages/announcements/CreateAnnouncementDialog.tsx b/src/pages/announcements/CreateAnnouncementDialog.tsx index fb51ebf..d65747d 100644 --- a/src/pages/announcements/CreateAnnouncementDialog.tsx +++ b/src/pages/announcements/CreateAnnouncementDialog.tsx @@ -6,29 +6,13 @@ */ import { FunctionComponent, useCallback } from 'react'; -import { - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Grid, -} from '@mui/material'; +import { Dialog, DialogActions, DialogContent, DialogTitle, Grid } from '@mui/material'; import { FormattedMessage } from 'react-intl'; -import { FormProvider, useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { DurationInput } from './DurationInput'; -import { - SubmitButton, - TextInput, - useSnackMessage, -} from '@gridsuite/commons-ui'; -import { - AnnouncementFormData, - emptyFormData, - formSchema, - fromFrontToBack, - MESSAGE, -} from './utils'; +import { SubmitButton, TextInput, useSnackMessage, CustomFormProvider } from '@gridsuite/commons-ui'; +import { AnnouncementFormData, emptyFormData, formSchema, fromFrontToBack, MESSAGE } from './utils'; import { UserAdminSrv } from '../../services'; interface CreateAnnouncementDialogProps { @@ -36,9 +20,7 @@ interface CreateAnnouncementDialogProps { onClose: () => void; } -export const CreateAnnouncementDialog: FunctionComponent< - CreateAnnouncementDialogProps -> = (props) => { +export const CreateAnnouncementDialog: FunctionComponent = (props) => { const { snackError } = useSnackMessage(); const formMethods = useForm({ @@ -51,12 +33,11 @@ export const CreateAnnouncementDialog: FunctionComponent< const onSubmit = useCallback( (formData: AnnouncementFormData) => { - UserAdminSrv.createAnnouncement(fromFrontToBack(formData)).catch( - (error) => - snackError({ - messageTxt: error.message, - headerId: 'announcements.error.add', - }) + UserAdminSrv.createAnnouncement(fromFrontToBack(formData)).catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'announcements.error.add', + }) ); reset(); props.onClose(); @@ -66,7 +47,7 @@ export const CreateAnnouncementDialog: FunctionComponent< return ( //@ts-ignore because RHF TS is broken - + @@ -88,12 +69,9 @@ export const CreateAnnouncementDialog: FunctionComponent< - + - + ); }; diff --git a/src/pages/announcements/DurationInput.tsx b/src/pages/announcements/DurationInput.tsx index b194100..03bf050 100644 --- a/src/pages/announcements/DurationInput.tsx +++ b/src/pages/announcements/DurationInput.tsx @@ -42,31 +42,19 @@ export const DurationInput = () => { - + : - + : - + ); diff --git a/src/pages/announcements/ListItemAnnouncement.tsx b/src/pages/announcements/ListItemAnnouncement.tsx index e12ba04..191c60f 100644 --- a/src/pages/announcements/ListItemAnnouncement.tsx +++ b/src/pages/announcements/ListItemAnnouncement.tsx @@ -5,30 +5,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { - IconButton, - ListItem, - ListItemIcon, - ListItemText, -} from '@mui/material'; +import { IconButton, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { FunctionComponent } from 'react'; -import { - Announcement, - DATE, - DAYS, - DURATION, - HOURS, - ID, - MESSAGE, - MINUTES, -} from './utils'; +import { Announcement, DATE, DAYS, DURATION, HOURS, ID, MESSAGE, MINUTES } from './utils'; import { Cancel, Message, ScheduleSend, Timelapse } from '@mui/icons-material'; import { UserAdminSrv } from '../../services'; import { useSnackMessage } from '@gridsuite/commons-ui'; -export const ListItemAnnouncement: FunctionComponent = ( - announcement -) => { +export const ListItemAnnouncement: FunctionComponent = (announcement) => { const { snackError } = useSnackMessage(); return ( @@ -36,12 +20,11 @@ export const ListItemAnnouncement: FunctionComponent = ( secondaryAction={ - UserAdminSrv.deleteAnnouncement(announcement[ID]).catch( - (error) => - snackError({ - messageTxt: error.message, - headerId: 'announcements.error.delete', - }) + UserAdminSrv.deleteAnnouncement(announcement[ID]).catch((error) => + snackError({ + messageTxt: error.message, + headerId: 'announcements.error.delete', + }) ) } edge="end" @@ -55,10 +38,7 @@ export const ListItemAnnouncement: FunctionComponent = ( - + @@ -69,15 +49,9 @@ export const ListItemAnnouncement: FunctionComponent = ( diff --git a/src/pages/announcements/useAnnouncements.ts b/src/pages/announcements/useAnnouncements.ts index 3cecdb2..1ef2a91 100644 --- a/src/pages/announcements/useAnnouncements.ts +++ b/src/pages/announcements/useAnnouncements.ts @@ -24,11 +24,7 @@ export function useAnnouncements() { function getAnnouncements() { UserAdminSrv.getAnnouncements() .then((announcements) => - setAnnouncements( - announcements.map((announcement) => - fromBackToFront(announcement) - ) - ) + setAnnouncements(announcements.map((announcement) => fromBackToFront(announcement))) ) .catch((error) => snackError({ @@ -38,9 +34,7 @@ export function useAnnouncements() { ); } - const rws = new ReconnectingWebSocket(() => - getUrlWithToken(webSocketUrl) - ); + const rws = new ReconnectingWebSocket(() => getUrlWithToken(webSocketUrl)); rws.addEventListener('open', () => { console.info('WebSocket for announcements is connected'); diff --git a/src/pages/announcements/utils.ts b/src/pages/announcements/utils.ts index 3436282..a77aa8d 100644 --- a/src/pages/announcements/utils.ts +++ b/src/pages/announcements/utils.ts @@ -62,8 +62,7 @@ export const formSchema = yup function getDurationUnitSchema(otherUnitName1: string, otherUnitName2: string) { return yup.number().when([otherUnitName1, otherUnitName2], { - is: (v1: number | null, v2: number | null) => - v1 === null && v2 === null, + is: (v1: number | null, v2: number | null) => v1 === null && v2 === null, then: (schema) => schema.required(), otherwise: (schema) => schema.nullable(), }); @@ -88,9 +87,7 @@ export interface Announcement extends AnnouncementFormData { [DATE]: string; } -export function fromBackToFront( - serverData: AnnouncementServerData -): Announcement { +export function fromBackToFront(serverData: AnnouncementServerData): Announcement { // In server side, duration is stored in hours only, so we need to compute the days const duration = parse(serverData[DURATION]); if (duration[HOURS] && duration[HOURS] >= 24) { diff --git a/src/routes/router.tsx b/src/routes/router.tsx index cf9cdb6..b4eec13 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -33,7 +33,7 @@ import { Announcements } from '../pages/announcements'; export enum MainPaths { users = 'users', profiles = 'profiles', - annoncements = 'annoucements', + announcements = 'announcements', } export function appRoutes(): RouteObject[] { @@ -59,8 +59,8 @@ export function appRoutes(): RouteObject[] { handle: { appBar_tab: MainPaths.announcements, }, - }, - { + }, + { path: `/${MainPaths.profiles}`, element: , handle: { diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts index 6fd809d..e3afb91 100644 --- a/src/services/user-admin.ts +++ b/src/services/user-admin.ts @@ -8,12 +8,7 @@ import { User } from 'oidc-client'; import { backendFetch, backendFetchJson, getRestBase } from '../utils/api-rest'; import { extractUserSub, getToken, getUser } from '../utils/api'; -import { User } from '../utils/auth'; -import { - AnnouncementServerData, - DURATION, - MESSAGE, -} from '../pages/announcements/utils'; +import { AnnouncementServerData, DURATION, MESSAGE } from '../pages/announcements/utils'; import { UUID } from 'crypto'; const USER_ADMIN_URL = `${getRestBase()}/user-admin/v1`; @@ -213,17 +208,12 @@ export function getAnnouncements(): Promise { method: 'get', cache: 'default', }).catch((reason) => { - console.error( - `Error while getting the list of announcements : ${reason}` - ); + console.error(`Error while getting the list of announcements : ${reason}`); throw reason; }) as Promise; } -export function createAnnouncement(announcement: { - [MESSAGE]: string; - [DURATION]: string; -}) { +export function createAnnouncement(announcement: { [MESSAGE]: string; [DURATION]: string }) { const body = JSON.stringify(announcement); console.debug('Creating announcement...' + body); return backendFetch(`${USER_ADMIN_URL}/announcements`, { diff --git a/src/translations/en.json b/src/translations/en.json index ac6b9fc..9daf091 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -45,7 +45,6 @@ "users.form.content": "Please fill in new user data.", "users.form.field.username.label": "User ID", - "announcements.add": "New", "announcements.dialog.title": "Announcement", "announcements.dialog.input": "Message to send", @@ -57,7 +56,7 @@ "duration.hours": "Hours", "duration.minutes": "Minutes", - "YupRequired": "Required" + "YupRequired": "Required", "users.form.delete.dialog.title": "Delete a user", "users.form.delete.multiple.dialog.message": "{itemsCount} users will be deleted.", diff --git a/src/translations/fr.json b/src/translations/fr.json index 133b4c1..b71c5d2 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -57,7 +57,7 @@ "duration.hours": "Heures", "duration.minutes": "Minutes", - "YupRequired": "Requis" + "YupRequired": "Requis", "users.form.delete.dialog.title": "Supprimer utilisateur", "users.form.delete.dialog.message": "{itemName} va être supprimé.",