diff --git a/package-lock.json b/package-lock.json index 5e8045d..4bf8577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@gridsuite/commons-ui": "0.101.0", + "@gridsuite/commons-ui": "file:../commons-ui/gridsuite-commons-ui-0.102.0.tgz", "@hookform/resolvers": "^4.0.0", "@mui/icons-material": "^5.16.14", "@mui/lab": "5.0.0-alpha.175", @@ -3074,9 +3074,9 @@ } }, "node_modules/@gridsuite/commons-ui": { - "version": "0.101.0", - "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.101.0.tgz", - "integrity": "sha512-1nDH39fi2BpWbpT/1xlheG/Oy/nK6+qvQRBJ/v9whuUrdqE0GWpFYTTkjk+mqRiJBxMSmsEyNC1QvbUqbXyPRQ==", + "version": "0.102.0", + "resolved": "file:../commons-ui/gridsuite-commons-ui-0.102.0.tgz", + "integrity": "sha512-MjT4hNRzkQs6T3NrmXjunVNEjZAPQntAwvWa35eGHGhsxTCj6ORVDxQviX68GelKTpP5IaZr11wbz9U6LdZU7A==", "license": "MPL-2.0", "dependencies": { "@ag-grid-community/locale": "^33.1.0", @@ -3098,7 +3098,8 @@ "react-querybuilder": "^8.2.0", "reconnecting-websocket": "^4.4.0", "type-fest": "^4.34.1", - "uuid": "^11.0.5" + "uuid": "^11.0.5", + "yup-locales": "^1.2.28" }, "engines": { "node": ">=22", @@ -16065,6 +16066,12 @@ "type-fest": "^2.19.0" } }, + "node_modules/yup-locales": { + "version": "1.2.28", + "resolved": "https://registry.npmjs.org/yup-locales/-/yup-locales-1.2.28.tgz", + "integrity": "sha512-q2p5XDVThFXTLOOV1DgMbpMOfrxfZTjYSkS1WRWco6YZJOeHrbuM3ISytrLaYa8TiX/YPtE9tuQmMiUyTBZDIw==", + "license": "MIT" + }, "node_modules/yup/node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", diff --git a/package.json b/package.json index 3ed8524..923abaf 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@gridsuite/commons-ui": "0.101.0", + "@gridsuite/commons-ui": "file:../commons-ui/gridsuite-commons-ui-0.102.0.tgz", "@hookform/resolvers": "^4.0.0", "@mui/icons-material": "^5.16.14", "@mui/lab": "5.0.0-alpha.175", diff --git a/src/components/App/app-wrapper.tsx b/src/components/App/app-wrapper.tsx index 9dc16ae..c624401 100644 --- a/src/components/App/app-wrapper.tsx +++ b/src/components/App/app-wrapper.tsx @@ -18,6 +18,8 @@ import { CardErrorBoundary, cardErrorBoundaryEn, cardErrorBoundaryFr, + filterEn, + filterFr, GsLangUser, GsTheme, LANG_ENGLISH, @@ -115,16 +117,18 @@ const getMuiTheme = (theme: GsTheme, locale: GsLangUser): Theme => { const messages: Record = { en: { - ...messages_en, + ...filterEn, ...loginEn, ...topBarEn, ...cardErrorBoundaryEn, + ...messages_en, }, fr: { - ...messages_fr, + ...filterFr, ...loginFr, ...topBarFr, ...cardErrorBoundaryFr, + ...messages_fr, }, }; diff --git a/src/components/App/app.tsx b/src/components/App/app.tsx index 86746c1..85ff732 100644 --- a/src/components/App/app.tsx +++ b/src/components/App/app.tsx @@ -14,18 +14,20 @@ import { NotificationsUrlKeys, useNotificationsListener, useSnackMessage, + useYupIntl, } from '@gridsuite/commons-ui'; import { selectComputedLanguage, selectLanguage, selectTheme } from '../../redux/actions'; -import { AppState } from '../../redux/reducer'; -import { ConfigParameters, ConfigSrv } from '../../services'; +import type { AppState } from '../../redux/reducer'; +import type { AppDispatch } from '../../redux/store'; +import { type ConfigParameters, ConfigSrv } from '../../services'; import { APP_NAME, COMMON_APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../../utils/config-params'; import { getComputedLanguage } from '../../utils/language'; import AppTopBar from './app-top-bar'; import { useDebugRender } from '../../utils/hooks'; -import { AppDispatch } from '../../redux/store'; export default function App({ children }: Readonly>) { useDebugRender('app'); + useYupIntl(); const { snackError } = useSnackMessage(); const dispatch = useDispatch(); const user = useSelector((state: AppState) => state.user); diff --git a/src/pages/announcements/add-announcement-form.tsx b/src/pages/announcements/add-announcement-form.tsx index d7bd256..f858d82 100644 --- a/src/pages/announcements/add-announcement-form.tsx +++ b/src/pages/announcements/add-announcement-form.tsx @@ -10,8 +10,8 @@ import { Grid } from '@mui/material'; import { type DateOrTimeView } from '@mui/x-date-pickers'; import { useIntl } from 'react-intl'; import { SubmitButton, useSnackMessage } from '@gridsuite/commons-ui'; -import yup from '../../utils/yup-config'; -import { type InferType } from 'yup'; +import type { InferType } from 'yup'; +import * as yup from 'yup'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { FormContainer, SelectElement, TextareaAutosizeElement } from 'react-hook-form-mui'; @@ -28,42 +28,53 @@ export type AddAnnouncementFormProps = { onAnnouncementCreated?: () => void; }; -const formSchema = yup - .object() - .shape({ - [MESSAGE]: yup.string().nullable().trim().min(1).required(), - [START_DATE]: yup.string().nullable().datetime().required(), - [END_DATE]: yup - .string() - .nullable() - .datetime() - .required() - .when(START_DATE, (startDate, schema) => - schema.test( - 'is-after-start', - 'End date must be after start date', - (endDate) => !startDate || !endDate || new Date(endDate) > new Date(startDate as unknown as string) - ) - ), - [SEVERITY]: yup - .string() - .nullable() - .oneOf(Object.values(UserAdminSrv.AnnouncementSeverity)) - .required(), - }) - .required(); -type FormSchema = InferType; - -const datetimePickerTransform: NonNullable['transform']> = { - input: (value) => (value ? new Date(value) : null), - output: (value) => value?.toISOString() ?? '', -}; const pickerView = ['year', 'month', 'day', 'hours', 'minutes'] as const satisfies readonly DateOrTimeView[]; export default function AddAnnouncementForm({ onAnnouncementCreated }: Readonly) { const intl = useIntl(); const { snackError } = useSnackMessage(); + const formSchema = useMemo( + () => + yup + .object() + .shape({ + [MESSAGE]: yup.string().nullable().trim().min(1).required(), + [START_DATE]: yup.string().nullable().datetime().required(), + [END_DATE]: yup + .string() + .nullable() + .datetime() + .required() + .when(START_DATE, (startDate, schema) => + schema.test( + 'is-after-start', + intl.formatMessage({ id: 'announcements.form.errForm.startDateAfterEndDateErr' }), + (endDate) => + !startDate || + !endDate || + new Date(endDate) > new Date(startDate as unknown as string) + ) + ), + [SEVERITY]: yup + .string() + .nullable() + .oneOf(Object.values(UserAdminSrv.AnnouncementSeverity)) + .required(), + }) + .required(), + [intl] + ); + + type FormSchema = InferType; + const datetimePickerTransform = useMemo['transform']>>( + () => ({ + input: (value) => (value ? new Date(value) : null), + output: (value) => value?.toISOString() ?? '', + }), + [] + ); + const formContext = useForm({ resolver: yupResolver(formSchema), defaultValues: { diff --git a/src/pages/groups/modification/group-modification-dialog.tsx b/src/pages/groups/modification/group-modification-dialog.tsx index 89fd2ab..0e7cb7d 100644 --- a/src/pages/groups/modification/group-modification-dialog.tsx +++ b/src/pages/groups/modification/group-modification-dialog.tsx @@ -5,17 +5,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import GroupModificationForm, { - GROUP_NAME, - GroupModificationFormType, - GroupModificationSchema, - SELECTED_USERS, -} from './group-modification-form'; +import GroupModificationForm, { GROUP_NAME, SELECTED_USERS } from './group-modification-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; import { GroupInfos, UserAdminSrv, UserInfos } from '../../../services'; +import type { InferType } from 'yup'; +import * as yup from 'yup'; +import { useIntl } from 'react-intl'; interface GroupModificationDialogProps { groupInfos: GroupInfos | undefined; @@ -31,6 +29,23 @@ const GroupModificationDialog: FunctionComponent = onUpdate, }) => { const { snackError } = useSnackMessage(); + const intl = useIntl(); + + const GroupModificationSchema = useMemo( + () => + yup + .object() + .shape({ + [GROUP_NAME]: yup + .string() + .trim() + .required(intl.formatMessage({ id: 'nameEmpty' })), + [SELECTED_USERS]: yup.string().nullable(), + }) + .required(), + [intl] + ); + type GroupModificationFormType = InferType; const formMethods = useForm({ resolver: yupResolver(GroupModificationSchema), }); diff --git a/src/pages/groups/modification/group-modification-form.tsx b/src/pages/groups/modification/group-modification-form.tsx index 8ef95db..fede9f5 100644 --- a/src/pages/groups/modification/group-modification-form.tsx +++ b/src/pages/groups/modification/group-modification-form.tsx @@ -6,24 +6,13 @@ */ import { TextInput } from '@gridsuite/commons-ui'; -import Grid from '@mui/material/Grid'; -import React, { FunctionComponent } from 'react'; -import yup from '../../../utils/yup-config'; +import { Grid } from '@mui/material'; +import { type FunctionComponent } from 'react'; import TableSelection from '../../common/table-selection'; export const GROUP_NAME = 'name'; export const SELECTED_USERS = 'users'; -export const GroupModificationSchema = yup - .object() - .shape({ - [GROUP_NAME]: yup.string().trim().required('nameEmpty'), - [SELECTED_USERS]: yup.string().nullable(), - }) - .required(); - -export type GroupModificationFormType = yup.InferType; - interface GroupModificationFormProps { usersOptions: string[]; selectedUsers?: string[]; diff --git a/src/pages/profiles/modification/profile-modification-dialog.tsx b/src/pages/profiles/modification/profile-modification-dialog.tsx index 26c41bb..aff120d 100644 --- a/src/pages/profiles/modification/profile-modification-dialog.tsx +++ b/src/pages/profiles/modification/profile-modification-dialog.tsx @@ -17,13 +17,14 @@ import ProfileModificationForm, { USER_QUOTA_CASE_NB, NETWORK_VISUALIZATION_PARAMETERS_ID, } from './profile-modification-form'; -import yup from '../../../utils/yup-config'; +import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; -import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; +import { type FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; -import { UserAdminSrv, UserProfile } from '../../../services'; -import { UUID } from 'crypto'; +import { UserAdminSrv, type UserProfile } from '../../../services'; +import type { UUID } from 'crypto'; +import { useIntl } from 'react-intl'; export interface ProfileModificationDialogProps { profileId: UUID | undefined; @@ -39,23 +40,37 @@ const ProfileModificationDialog: FunctionComponent { const { snackError } = useSnackMessage(); + const intl = useIntl(); const [dataFetchStatus, setDataFetchStatus] = useState(FetchStatus.IDLE); - const formSchema = yup - .object() - .shape({ - [PROFILE_NAME]: yup.string().trim().required('nameEmpty'), - [LOADFLOW_PARAM_ID]: yup.string().optional(), - [SECURITY_ANALYSIS_PARAM_ID]: yup.string().optional(), - [SENSITIVITY_ANALYSIS_PARAM_ID]: yup.string().optional(), - [SHORTCIRCUIT_PARAM_ID]: yup.string().optional(), - [VOLTAGE_INIT_PARAM_ID]: yup.string().optional(), - [USER_QUOTA_CASE_NB]: yup.number().positive('userQuotaPositive').nullable(), - [USER_QUOTA_BUILD_NB]: yup.number().positive('userQuotaPositive').nullable(), - [SPREADSHEET_CONFIG_COLLECTION_ID]: yup.string().optional(), - [NETWORK_VISUALIZATION_PARAMETERS_ID]: yup.string().optional(), - }) - .required(); + const formSchema = useMemo( + () => + yup + .object() + .shape({ + [PROFILE_NAME]: yup + .string() + .trim() + .required(intl.formatMessage({ id: 'nameEmpty' })), + [LOADFLOW_PARAM_ID]: yup.string().optional(), + [SECURITY_ANALYSIS_PARAM_ID]: yup.string().optional(), + [SENSITIVITY_ANALYSIS_PARAM_ID]: yup.string().optional(), + [SHORTCIRCUIT_PARAM_ID]: yup.string().optional(), + [VOLTAGE_INIT_PARAM_ID]: yup.string().optional(), + [USER_QUOTA_CASE_NB]: yup + .number() + .positive(intl.formatMessage({ id: 'userQuotaPositive' })) + .nullable(), + [USER_QUOTA_BUILD_NB]: yup + .number() + .positive(intl.formatMessage({ id: 'userQuotaPositive' })) + .nullable(), + [SPREADSHEET_CONFIG_COLLECTION_ID]: yup.string().optional(), + [NETWORK_VISUALIZATION_PARAMETERS_ID]: yup.string().optional(), + }) + .required(), + [intl] + ); const formMethods = useForm({ resolver: yupResolver(formSchema), diff --git a/src/pages/users/modification/user-modification-dialog.tsx b/src/pages/users/modification/user-modification-dialog.tsx index 157b0d2..bdc18b5 100644 --- a/src/pages/users/modification/user-modification-dialog.tsx +++ b/src/pages/users/modification/user-modification-dialog.tsx @@ -5,18 +5,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import UserModificationForm, { - USER_NAME, - USER_PROFILE_NAME, - USER_SELECTED_GROUPS, - UserModificationFormType, - UserModificationSchema, -} from './user-modification-form'; +import UserModificationForm, { USER_NAME, USER_PROFILE_NAME, USER_SELECTED_GROUPS } from './user-modification-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useForm } from 'react-hook-form'; import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'; import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui'; import { GroupInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services'; +import { useIntl } from 'react-intl'; +import type { InferType } from 'yup'; +import * as yup from 'yup'; interface UserModificationDialogProps { userInfos: UserInfos | undefined; @@ -32,10 +29,29 @@ const UserModificationDialog: FunctionComponent = ( onUpdate, }) => { const { snackError } = useSnackMessage(); + const intl = useIntl(); + + const UserModificationSchema = useMemo( + () => + yup + .object() + .shape({ + [USER_NAME]: yup + .string() + .trim() + .required(intl.formatMessage({ id: 'nameEmpty' })), + [USER_PROFILE_NAME]: yup.string().nullable(), + [USER_SELECTED_GROUPS]: yup.string().nullable(), + }) + .required(), + [intl] + ); + type UserModificationFormType = InferType; const formMethods = useForm({ resolver: yupResolver(UserModificationSchema), }); const { reset, setValue } = formMethods; + const [profileOptions, setProfileOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]); const [selectedGroups, setSelectedGroups] = useState([]); diff --git a/src/pages/users/modification/user-modification-form.tsx b/src/pages/users/modification/user-modification-form.tsx index ce036dc..1e3be79 100644 --- a/src/pages/users/modification/user-modification-form.tsx +++ b/src/pages/users/modification/user-modification-form.tsx @@ -6,26 +6,14 @@ */ import { AutocompleteInput, TextInput } from '@gridsuite/commons-ui'; -import Grid from '@mui/material/Grid'; -import React, { FunctionComponent } from 'react'; -import yup from '../../../utils/yup-config'; +import { Grid } from '@mui/material'; +import { type FunctionComponent } from 'react'; import TableSelection from '../../common/table-selection'; export const USER_NAME = 'sub'; export const USER_PROFILE_NAME = 'profileName'; export const USER_SELECTED_GROUPS = 'groups'; -export const UserModificationSchema = yup - .object() - .shape({ - [USER_NAME]: yup.string().trim().required('nameEmpty'), - [USER_PROFILE_NAME]: yup.string().nullable(), - [USER_SELECTED_GROUPS]: yup.string().nullable(), - }) - .required(); - -export type UserModificationFormType = yup.InferType; - interface UserModificationFormProps { profileOptions: string[]; groupOptions: string[]; diff --git a/src/translations/en.json b/src/translations/en.json index b26a64f..619ce61 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7,7 +7,6 @@ "cancel": "Cancel", "validate": "Validate", "parameters": "Parameters", - "nameEmpty": "The name is empty", "paramsChangingError": "An error occurred when changing the parameters", "paramsRetrievingError": "An error occurred while retrieving the parameters", "userQuotaPositive": "User quota must be positive", @@ -125,5 +124,6 @@ "announcements.form.errCreateAnnouncement": "Error while creating announcement:", "announcements.form.errCreateAnnouncement.noOverlapAllowedErr": "The date overlaps with another announcement date.", "announcements.form.errCreateAnnouncement.noSameDateErr": "The announcement start and end date must be different.", - "announcements.form.errCreateAnnouncement.startDateAfterEndDateErr": "The start date cannot be after the end date." + "announcements.form.errCreateAnnouncement.startDateAfterEndDateErr": "The start date cannot be after the end date.", + "announcements.form.errForm.startDateAfterEndDateErr": "End date must be after start date." } diff --git a/src/translations/fr.json b/src/translations/fr.json index fe4c156..4018e8e 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -7,7 +7,6 @@ "cancel": "Annuler", "validate": "Valider", "parameters": "Paramètres", - "nameEmpty": "Le nom est vide", "paramsChangingError": "Une erreur est survenue lors de la modification des paramètres", "paramsRetrievingError": "Une erreur est survenue lors de la récupération des paramètres", "userQuotaPositive": "Le quota utilisateur doit être positif", @@ -126,5 +125,6 @@ "announcements.form.errCreateAnnouncement": "Erreur lors de la création de l'annonce :", "announcements.form.errCreateAnnouncement.noOverlapAllowedErr": "La date d'annonce chevauche une autre date d'annonce.", "announcements.form.errCreateAnnouncement.noSameDateErr": "La date de début et de fin d'annonce doivent être différentes.", - "announcements.form.errCreateAnnouncement.startDateAfterEndDateErr": "La date de début d'annonce ne peut pas être après la date de fin." + "announcements.form.errCreateAnnouncement.startDateAfterEndDateErr": "La date de début d'annonce ne peut pas être après la date de fin.", + "announcements.form.errForm.startDateAfterEndDateErr": "La date de fin doit être après la date de début." } diff --git a/src/utils/yup-config.js b/src/utils/yup-config.js deleted file mode 100644 index 6bf1807..0000000 --- a/src/utils/yup-config.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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/. - */ - -// TODO: this file is going to be available soon in commons-ui - -import * as yup from 'yup'; - -yup.setLocale({ - mixed: { - required: 'YupRequired', - notType: ({ type }) => { - if (type === 'number') { - return 'YupNotTypeNumber'; - } else { - return 'YupNotTypeDefault'; - } - }, - }, -}); - -export default yup;