diff --git a/src/components/index.ts b/src/components/index.ts index a5f3a83f9..a60c49a3b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -29,3 +29,4 @@ export * from './parameters'; export * from './menus'; export * from './muiTable'; export * from './resizablePanels'; +export * from './network-modifications'; diff --git a/src/components/inputs/reactHookForm/CountrySelectionInput.tsx b/src/components/inputs/reactHookForm/CountrySelectionInput.tsx new file mode 100644 index 000000000..fbac75a57 --- /dev/null +++ b/src/components/inputs/reactHookForm/CountrySelectionInput.tsx @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023, 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 { AutocompleteInput, AutocompleteInputProps } from './autocompleteInputs'; +import { useLocalizedCountries } from '../../../hooks'; +import { GsLang } from '../../../utils'; + +interface CountrySelectionInputProps extends Omit {} + +export function CountrySelectionInput(props: Readonly) { + const { locale } = useIntl(); + const { translate, countryCodes } = useLocalizedCountries(locale as GsLang); + + return ( + translate(countryCode as string)} + {...props} + /> + ); +} diff --git a/src/components/inputs/reactHookForm/autocompleteInputs/AutocompleteInput.tsx b/src/components/inputs/reactHookForm/autocompleteInputs/AutocompleteInput.tsx index b31003640..c87be27dd 100644 --- a/src/components/inputs/reactHookForm/autocompleteInputs/AutocompleteInput.tsx +++ b/src/components/inputs/reactHookForm/autocompleteInputs/AutocompleteInput.tsx @@ -85,7 +85,7 @@ export function AutocompleteInput({ return ( handleChange(data as Option)} {...(allowNewValue && { freeSolo: true, diff --git a/src/components/inputs/reactHookForm/expandableInput/DeletableRow.tsx b/src/components/inputs/reactHookForm/expandableInput/DeletableRow.tsx new file mode 100644 index 000000000..774bf92d9 --- /dev/null +++ b/src/components/inputs/reactHookForm/expandableInput/DeletableRow.tsx @@ -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 { PropsWithChildren, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Grid, IconButton, Tooltip } from '@mui/material'; +import { Delete as DeleteIcon, RestoreFromTrash as RestoreFromTrashIcon } from '@mui/icons-material'; + +export interface DeletableRowProps extends PropsWithChildren { + alignItems: string; + onClick: () => void; + deletionMark?: boolean | null; + disabledDeletion?: boolean | null; +} + +export function DeletableRow({ + alignItems, + onClick, + deletionMark, + disabledDeletion, + children, +}: Readonly) { + const intl = useIntl(); + const [isMouseHover, setIsMouseHover] = useState(false); + + return ( + setIsMouseHover(true)} + onMouseLeave={() => setIsMouseHover(false)} + > + {children} + + {isMouseHover && !disabledDeletion && ( + + + {deletionMark ? : } + + + )} + + + ); +} diff --git a/src/components/inputs/reactHookForm/expandableInput/ExpandableInput.tsx b/src/components/inputs/reactHookForm/expandableInput/ExpandableInput.tsx new file mode 100644 index 000000000..56e7518ee --- /dev/null +++ b/src/components/inputs/reactHookForm/expandableInput/ExpandableInput.tsx @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2023, 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 { useFieldArray } from 'react-hook-form'; +import { Button, Grid } from '@mui/material'; +import { ControlPoint as AddIcon } from '@mui/icons-material'; +import { FormattedMessage } from 'react-intl'; +import { DeletableRow } from './DeletableRow'; +import { ErrorInput, MidFormError } from '../errorManagement'; +import { mergeSx, type MuiStyles } from '../../../../utils'; + +const styles = { + button: (theme) => ({ + justifyContent: 'flex-start', + fontSize: 'small', + marginTop: theme.spacing(1), + }), + paddingButton: (theme) => ({ + paddingLeft: theme.spacing(2), + }), +} as const satisfies MuiStyles; + +export interface ExpandableInputProps { + name: string; + Field: React.ComponentType; + fieldProps?: any; + addButtonLabel?: string; + initialValue?: any; + getDeletionMark?: (idx: number) => boolean; + deleteCallback?: (idx: number) => boolean; + alignItems?: string; + watchProps?: boolean; + disabled?: boolean; + disabledDeletion?: (idx: number) => boolean; +} + +// This component is used to display Array of objects. +// We can manage 2 states for deletion: +// - only 1 state and 1 delete icon that removes the current line +// - a second state "mark for deletion" with a second icon: the line is not removed +// and we can cancel this mark to go back to normal state. +export function ExpandableInput({ + name, + Field, // Used to display each object of an array + fieldProps, // Props to pass to Field + addButtonLabel, + initialValue, // Initial value to display when we add a new entry to array + getDeletionMark, + deleteCallback, + alignItems = 'stretch', // default value for a flex container + watchProps = true, + disabled = false, + disabledDeletion, +}: Readonly) { + const { + fields: values, + append, + remove, + } = useFieldArray({ + name, + }); + + return ( + + + + + {watchProps && + values.map((value, idx) => ( + { + const shouldRemove = deleteCallback ? deleteCallback(idx) : true; + if (shouldRemove) { + remove(idx); + } + }} + deletionMark={getDeletionMark?.(idx)} + disabledDeletion={disabledDeletion?.(idx)} + > + + + ))} + + + + + ); +} diff --git a/src/components/inputs/reactHookForm/expandableInput/index.ts b/src/components/inputs/reactHookForm/expandableInput/index.ts new file mode 100644 index 000000000..17f7df8d5 --- /dev/null +++ b/src/components/inputs/reactHookForm/expandableInput/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 * from './ExpandableInput'; diff --git a/src/components/inputs/reactHookForm/index.ts b/src/components/inputs/reactHookForm/index.ts index b22d04f3c..8895de408 100644 --- a/src/components/inputs/reactHookForm/index.ts +++ b/src/components/inputs/reactHookForm/index.ts @@ -21,3 +21,5 @@ export * from './tableInputs'; export * from './text'; export * from './utils'; export * from './constants'; +export * from './expandableInput'; +export * from './CountrySelectionInput'; diff --git a/src/components/inputs/reactHookForm/text/TextInput.tsx b/src/components/inputs/reactHookForm/text/TextInput.tsx index 83841d4d6..fbd69ed40 100644 --- a/src/components/inputs/reactHookForm/text/TextInput.tsx +++ b/src/components/inputs/reactHookForm/text/TextInput.tsx @@ -102,7 +102,7 @@ export function TextInput({ fullWidth id={id ?? label} label={fieldLabel} - value={transformedValue} + value={transformedValue ?? ''} onChange={handleValueChanged} disabled={disabled} InputProps={{ diff --git a/src/components/network-modifications/common/form.utils.ts b/src/components/network-modifications/common/form.utils.ts new file mode 100644 index 000000000..735388f11 --- /dev/null +++ b/src/components/network-modifications/common/form.utils.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2022, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { FilledTextFieldProps, StandardTextFieldProps } from '@mui/material'; + +export const filledTextField: FilledTextFieldProps = { + variant: 'filled', +}; + +export const standardTextField: StandardTextFieldProps = { + variant: 'standard', +}; + +export const italicFontTextField = { + style: { fontStyle: 'italic' }, +}; diff --git a/src/components/network-modifications/common/index.ts b/src/components/network-modifications/common/index.ts new file mode 100644 index 000000000..cdf5c0b71 --- /dev/null +++ b/src/components/network-modifications/common/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2026, 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 * from './properties'; +export * from './form.utils'; diff --git a/src/components/network-modifications/common/properties/PropertiesForm.tsx b/src/components/network-modifications/common/properties/PropertiesForm.tsx new file mode 100644 index 000000000..e479ff49c --- /dev/null +++ b/src/components/network-modifications/common/properties/PropertiesForm.tsx @@ -0,0 +1,111 @@ +/** + * 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 } from '@mui/material'; +import { useCallback, useEffect, useState } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { fetchPredefinedProperties, initializedProperty } from './propertyUtils'; +import { PropertyForm } from './PropertyForm'; +import GridSection from '../../../grid/grid-section'; +import { FieldConstants, PredefinedProperties } from '../../../../utils'; +import { ExpandableInput } from '../../../inputs'; + +type PropertiesFormProps = { + id?: string; + networkElementType?: string; + isModification?: boolean; +}; + +export function PropertiesForm({ id, networkElementType, isModification = false }: Readonly) { + const additionalProperties = id + ? `${id}.${FieldConstants.ADDITIONAL_PROPERTIES}` + : FieldConstants.ADDITIONAL_PROPERTIES; + const watchProps = useWatch({ + name: additionalProperties, + }); + const { getValues, setValue } = useFormContext(); + const [predefinedProperties, setPredefinedProperties] = useState({}); + + useEffect(() => { + if (networkElementType) { + fetchPredefinedProperties(networkElementType).then((res) => { + if (res) { + setPredefinedProperties(res); + } + }); + } + }, [networkElementType]); + + const getDeletionMark = useCallback( + (idx: number) => { + const properties = getValues(`${additionalProperties}`); + if (properties && typeof properties[idx] !== 'undefined') { + return watchProps && properties[idx][FieldConstants.DELETION_MARK]; + } + return false; + }, + [getValues, watchProps, additionalProperties] + ); + + const deleteCallback = useCallback( + (idx: number) => { + let markedForDeletion = false; + const properties = getValues(`${additionalProperties}`); + if (properties && typeof properties[idx] !== 'undefined') { + markedForDeletion = properties[idx][FieldConstants.DELETION_MARK]; + } else { + return false; + } + + let canRemoveLine = true; + if (markedForDeletion) { + // just unmark + setValue(`${additionalProperties}.${idx}.${FieldConstants.DELETION_MARK}`, false, { + shouldDirty: true, + }); + canRemoveLine = false; + } else if ( + properties[idx][FieldConstants.PREVIOUS_VALUE] && + properties[idx][FieldConstants.ADDED] === false + ) { + // we should mark for deletion a property that actually exists in the network and not delete the property line straight away + setValue(`${additionalProperties}.${idx}.${FieldConstants.DELETION_MARK}`, true, { + shouldDirty: true, + }); + canRemoveLine = false; + } + // otherwise just delete the line + return canRemoveLine; + }, + [getValues, setValue, additionalProperties] + ); + + const modificationProperties = isModification + ? { + getDeletionMark, + deleteCallback, + watchProps, + } + : {}; + + const additionalProps = ( + + ); + + return ( + + + {additionalProps} + + ); +} diff --git a/src/components/network-modifications/common/properties/PropertyForm.tsx b/src/components/network-modifications/common/properties/PropertyForm.tsx new file mode 100644 index 000000000..1d6369768 --- /dev/null +++ b/src/components/network-modifications/common/properties/PropertyForm.tsx @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2023, 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 { useMemo } from 'react'; + +import { useWatch } from 'react-hook-form'; +import { FieldConstants, PredefinedProperties } from '../../../../utils'; +import GridItem from '../../../grid/grid-item'; +import { AutocompleteInput, TextInput } from '../../../inputs'; +import { italicFontTextField } from '../form.utils'; + +type PropertyFormProps = { + name: string; + index: string; + predefinedProperties: PredefinedProperties; +}; + +export const PropertyForm = ({ name, index, predefinedProperties }: PropertyFormProps) => { + const watchPropertyName = useWatch({ name: `${name}.${index}.${FieldConstants.NAME}` }); + const watchPropertyPreviousValue = useWatch({ + name: `${name}.${index}.${FieldConstants.PREVIOUS_VALUE}`, + }); + const watchPropertyDeletionMark = useWatch({ + name: `${name}.${index}.${FieldConstants.DELETION_MARK}`, + }); + const watchPropertyAdded = useWatch({ + name: `${name}.${index}.${FieldConstants.ADDED}`, + }); + + const predefinedNames = useMemo(() => { + const keys = Object.keys(predefinedProperties ?? {}); + return keys.sort((a, b) => a.localeCompare(b)); + }, [predefinedProperties]); + + const predefinedValues = useMemo(() => { + const values = predefinedProperties?.[watchPropertyName] ?? []; + return values.sort((a, b) => a.localeCompare(b)); + }, [watchPropertyName, predefinedProperties]); + + const nameField = ( + + ); + + const nameReadOnlyField = ( + + ); + + const valueField = ( + + ); + + const valueReadOnlyField = ( + + ); + + function renderPropertyLine() { + return ( + <> + {watchPropertyDeletionMark || (watchPropertyAdded === false && watchPropertyPreviousValue) ? ( + {nameReadOnlyField} + ) : ( + {nameField} + )} + {watchPropertyDeletionMark ? ( + {valueReadOnlyField} + ) : ( + {valueField} + )} + + ); + } + + return renderPropertyLine(); +}; diff --git a/src/components/network-modifications/common/properties/index.ts b/src/components/network-modifications/common/properties/index.ts new file mode 100644 index 000000000..78373ff41 --- /dev/null +++ b/src/components/network-modifications/common/properties/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2026, 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 * from './PropertiesForm'; +export * from './PropertyForm'; +export * from './properties.type'; +export * from './propertyUtils'; diff --git a/src/components/network-modifications/common/properties/properties.type.ts b/src/components/network-modifications/common/properties/properties.type.ts new file mode 100644 index 000000000..30589abe2 --- /dev/null +++ b/src/components/network-modifications/common/properties/properties.type.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 { FieldConstants } from '../../../../utils'; + +export type FilledProperty = Omit & { + [FieldConstants.VALUE]: string; +}; + +export type Property = { + [FieldConstants.NAME]: string; + [FieldConstants.VALUE]?: string | null; + [FieldConstants.PREVIOUS_VALUE]?: string | null; + [FieldConstants.DELETION_MARK]: boolean; + [FieldConstants.ADDED]: boolean; +}; + +export type Properties = { + [FieldConstants.ADDITIONAL_PROPERTIES]?: Property[]; +}; diff --git a/src/components/network-modifications/common/properties/propertyUtils.ts b/src/components/network-modifications/common/properties/propertyUtils.ts new file mode 100644 index 000000000..b5529f33c --- /dev/null +++ b/src/components/network-modifications/common/properties/propertyUtils.ts @@ -0,0 +1,203 @@ +/** + * 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 { FieldConstants, isBlankOrEmpty, PredefinedProperties } from '../../../../utils'; +import yup from '../../../../utils/yupConfig'; +import { fetchStudyMetadata } from '../../../../services'; +import { FilledProperty, Properties, Property } from './properties.type'; + +export type EquipmentWithProperties = { + properties?: Record; +}; + +export const fetchPredefinedProperties = (networkElementType: string): Promise => { + return fetchStudyMetadata().then((studyMetadata) => { + return studyMetadata.predefinedEquipmentProperties?.[networkElementType]; + }); +}; + +export const emptyProperties: Properties = { + [FieldConstants.ADDITIONAL_PROPERTIES]: [] as Property[], +}; + +export const createPropertyModification = (name: string, value: string | null): Property => { + return { + [FieldConstants.NAME]: name, + [FieldConstants.VALUE]: value, + [FieldConstants.PREVIOUS_VALUE]: value, + [FieldConstants.DELETION_MARK]: false, + [FieldConstants.ADDED]: true, + }; +}; + +export const initializedProperty = (): Property => { + return createPropertyModification('', null); +}; + +const isFilledProperty = (property: Property): property is FilledProperty => { + return property.value != null; +}; + +export const getFilledPropertiesFromModification = (properties: Property[] | undefined | null): FilledProperty[] => { + return ( + properties?.filter(isFilledProperty).map((p) => { + return { + [FieldConstants.NAME]: p[FieldConstants.NAME], + [FieldConstants.VALUE]: p[FieldConstants.VALUE], + [FieldConstants.PREVIOUS_VALUE]: p[FieldConstants.PREVIOUS_VALUE], + [FieldConstants.ADDED]: p[FieldConstants.ADDED], + [FieldConstants.DELETION_MARK]: p[FieldConstants.DELETION_MARK], + }; + }) ?? [] + ); +}; + +export const getPropertiesFromModification = (properties: Property[] | undefined | null): Properties => { + return { + [FieldConstants.ADDITIONAL_PROPERTIES]: properties + ? properties.map((p) => { + return { + [FieldConstants.NAME]: p[FieldConstants.NAME], + [FieldConstants.VALUE]: p[FieldConstants.VALUE], + [FieldConstants.PREVIOUS_VALUE]: p[FieldConstants.PREVIOUS_VALUE], + [FieldConstants.ADDED]: p[FieldConstants.ADDED], + [FieldConstants.DELETION_MARK]: p[FieldConstants.DELETION_MARK], + }; + }) + : [], + }; +}; + +export const copyEquipmentPropertiesForCreation = (equipmentInfos: EquipmentWithProperties): Properties => { + return { + [FieldConstants.ADDITIONAL_PROPERTIES]: equipmentInfos.properties + ? Object.entries(equipmentInfos.properties).map(([name, value]) => { + return { + [FieldConstants.NAME]: name, + [FieldConstants.VALUE]: value, + [FieldConstants.PREVIOUS_VALUE]: null, + [FieldConstants.DELETION_MARK]: false, + [FieldConstants.ADDED]: true, + }; + }) + : [], + }; +}; + +/* + We first load modification properties (empty at creation but could be filled later on), then we load properties + already present on the equipment (network). If one of the equipment properties key is present in the modification + we update the previousValue of this one, it means the modification change the network property value. + If not we add it as an unmodified property. We will be able to delete it or modify its value, but not it's name. + */ +export const mergeModificationAndEquipmentProperties = ( + modificationProperties: Property[], + equipment: EquipmentWithProperties +): Property[] => { + const newModificationProperties = new Map(); + modificationProperties.forEach((property) => { + if (property.name !== null) { + newModificationProperties.set(property.name, property); + } + }); + if (equipment.properties !== undefined) { + Object.entries(equipment.properties).forEach(([name, value]) => { + if (name !== null) { + let propertyToAdd; + // If the property is present in the modification and in the equipment + if (newModificationProperties.has(name)) { + const modProperty = newModificationProperties.get(name)!; + propertyToAdd = { + ...modProperty, + previousValue: value, // We set previous value of the modification to the equipment value + }; + } else { + propertyToAdd = { + [FieldConstants.NAME]: name, + [FieldConstants.VALUE]: null, + [FieldConstants.PREVIOUS_VALUE]: value, + [FieldConstants.DELETION_MARK]: false, + [FieldConstants.ADDED]: false, + }; + } + newModificationProperties.set(name, propertyToAdd); + } + }); + } + return Array.from(newModificationProperties.values()); +}; + +export function getConcatenatedProperties( + equipment: EquipmentWithProperties, + getValues: (name: string) => any, + id?: string +): any { + // ex: current Array [ {Object { name: "p1", value: "v2", previousValue: undefined, added: true, deletionMark: false } }, {...} ] + + const path = id ? `${id}.${FieldConstants.ADDITIONAL_PROPERTIES}` : `${FieldConstants.ADDITIONAL_PROPERTIES}`; + const modificationProperties = getValues(path); + return mergeModificationAndEquipmentProperties(modificationProperties, equipment); +} + +export const toModificationProperties = (properties: Properties) => { + const filteredProperties = properties[FieldConstants.ADDITIONAL_PROPERTIES]?.filter( + (p: Property) => !isBlankOrEmpty(p.value) || p[FieldConstants.DELETION_MARK] + ); + return filteredProperties === undefined || filteredProperties?.length === 0 ? null : filteredProperties; +}; + +const checkUniquePropertyNames = ( + properties: + | { + name: string; + }[] + | undefined +) => { + if (properties === undefined) { + return true; + } + const validValues = properties.filter((v) => v.name); + return validValues.length === new Set(validValues.map((v) => v.name)).size; +}; + +export const creationPropertiesSchema = yup.object({ + [FieldConstants.ADDITIONAL_PROPERTIES]: yup + .array() + .of( + yup.object().shape({ + [FieldConstants.NAME]: yup.string().required(), + [FieldConstants.VALUE]: yup.string().required(), + [FieldConstants.PREVIOUS_VALUE]: yup.string().nullable(), + [FieldConstants.DELETION_MARK]: yup.boolean().required(), + [FieldConstants.ADDED]: yup.boolean().required(), + }) + ) + .test('checkUniqueProperties', 'DuplicatedPropsError', (values) => checkUniquePropertyNames(values)), +}); + +export const modificationPropertiesSchema = yup.object({ + [FieldConstants.ADDITIONAL_PROPERTIES]: yup + .array() + .of( + yup.object().shape({ + [FieldConstants.NAME]: yup.string().required(), + [FieldConstants.VALUE]: yup + .string() + .nullable() + .when([FieldConstants.ADDED], { + is: (added: boolean) => added, + then: (schema) => schema.required(), + }), + [FieldConstants.PREVIOUS_VALUE]: yup.string().nullable(), + [FieldConstants.DELETION_MARK]: yup.boolean().required(), + [FieldConstants.ADDED]: yup.boolean().required(), + }) + ) + .test('checkUniqueProperties', 'DuplicatedPropsError', (values) => checkUniquePropertyNames(values)), +}); + +export const getPropertyValue = (properties: Record | undefined, keyName: string): string | undefined => + properties?.[keyName]; diff --git a/src/components/network-modifications/index.ts b/src/components/network-modifications/index.ts new file mode 100644 index 000000000..a55b30f6b --- /dev/null +++ b/src/components/network-modifications/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2026, 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 * from './common'; +export * from './substation'; diff --git a/src/components/network-modifications/substation/creation/SubstationCreationForm.tsx b/src/components/network-modifications/substation/creation/SubstationCreationForm.tsx new file mode 100644 index 000000000..5cf8bed07 --- /dev/null +++ b/src/components/network-modifications/substation/creation/SubstationCreationForm.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2026, 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 } from '@mui/material'; +import GridItem from '../../../grid/grid-item'; +import { TextInput } from '../../../inputs'; +import { CountrySelectionInput } from '../../../inputs/reactHookForm/CountrySelectionInput'; +import { FieldConstants } from '../../../../utils'; +import { PropertiesForm } from '../../common/properties/PropertiesForm'; +import { filledTextField } from '../../common'; + +export function SubstationCreationForm() { + const substationIdField = ; + + const substationNameField = ( + + ); + + const substationCountryField = ( + + ); + + return ( + <> + + {substationIdField} + {substationNameField} + {substationCountryField} + + + + ); +} diff --git a/src/components/network-modifications/substation/creation/index.ts b/src/components/network-modifications/substation/creation/index.ts new file mode 100644 index 000000000..996652424 --- /dev/null +++ b/src/components/network-modifications/substation/creation/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2026, 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 * from './SubstationCreationForm'; +export * from './substationCreation.types'; +export * from './substationCreation.utils'; diff --git a/src/components/network-modifications/substation/creation/substationCreation.types.ts b/src/components/network-modifications/substation/creation/substationCreation.types.ts new file mode 100644 index 000000000..8ba60f141 --- /dev/null +++ b/src/components/network-modifications/substation/creation/substationCreation.types.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2026, 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 { Property } from '../../common'; + +export type SubstationCreationDto = { + type: string; + equipmentId: string; + equipmentName: string | null; + country: string | null; + properties: Property[] | null; +}; diff --git a/src/components/network-modifications/substation/creation/substationCreation.utils.ts b/src/components/network-modifications/substation/creation/substationCreation.utils.ts new file mode 100644 index 000000000..76a4ede5e --- /dev/null +++ b/src/components/network-modifications/substation/creation/substationCreation.utils.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026, 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 { + creationPropertiesSchema, + getFilledPropertiesFromModification, + toModificationProperties, +} from '../../common/properties/propertyUtils'; +import yup from '../../../../utils/yupConfig'; +import { FieldConstants, sanitizeString } from '../../../../utils'; +import { SubstationCreationDto } from './substationCreation.types'; + +export const substationCreationFormToDto = (substationForm: SubstationCreationFormData): SubstationCreationDto => { + return { + type: 'SUBSTATION_CREATION', + equipmentId: substationForm.equipmentID, + equipmentName: sanitizeString(substationForm.equipmentName), + country: substationForm.country ?? null, + properties: toModificationProperties(substationForm), + }; +}; + +export const substationCreationDtoToForm = (substationDto: SubstationCreationDto): SubstationCreationFormData => { + return { + equipmentID: substationDto.equipmentId, + equipmentName: substationDto.equipmentName ?? '', + country: substationDto.country, + AdditionalProperties: getFilledPropertiesFromModification(substationDto.properties), + }; +}; + +export const substationCreationFormSchema = yup + .object() + .shape({ + [FieldConstants.EQUIPMENT_ID]: yup.string().required(), + [FieldConstants.EQUIPMENT_NAME]: yup.string().nullable(), + [FieldConstants.COUNTRY]: yup.string().nullable(), + }) + .concat(creationPropertiesSchema); + +export type SubstationCreationFormData = yup.InferType; + +export const substationCreationEmptyFormData: SubstationCreationFormData = { + equipmentID: '', + equipmentName: '', + country: null, + AdditionalProperties: [], +}; diff --git a/src/components/network-modifications/substation/index.ts b/src/components/network-modifications/substation/index.ts new file mode 100644 index 000000000..2836a5fdc --- /dev/null +++ b/src/components/network-modifications/substation/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2026, 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 * from './creation'; diff --git a/src/components/parameters/common/constant.ts b/src/components/parameters/common/constant.ts index 9fdd93594..c5ba92230 100644 --- a/src/components/parameters/common/constant.ts +++ b/src/components/parameters/common/constant.ts @@ -5,8 +5,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { KILO_VOLT, MEGA_VAR } from '../../../utils/constants/unitsConstants'; - export const PROVIDER = 'provider'; export const VOLTAGE_LEVEL = 'voltageLevel'; @@ -17,15 +15,6 @@ export const PARAM_SA_LOW_VOLTAGE_ABSOLUTE_THRESHOLD = 'lowVoltageAbsoluteThresh export const PARAM_SA_HIGH_VOLTAGE_PROPORTIONAL_THRESHOLD = 'highVoltageProportionalThreshold'; export const PARAM_SA_HIGH_VOLTAGE_ABSOLUTE_THRESHOLD = 'highVoltageAbsoluteThreshold'; -export const ReactivePowerAdornment = { - position: 'end', - text: MEGA_VAR, -}; -export const VoltageAdornment = { - position: 'end', - text: KILO_VOLT, -}; - export const VERSION_PARAMETER = 'version'; export const COMMON_PARAMETERS = 'commonParameters'; export const SPECIFIC_PARAMETERS = 'specificParametersPerProvider'; diff --git a/src/components/parameters/voltage-init/general-parameters.tsx b/src/components/parameters/voltage-init/general-parameters.tsx index c0643a22f..79b5499af 100644 --- a/src/components/parameters/voltage-init/general-parameters.tsx +++ b/src/components/parameters/voltage-init/general-parameters.tsx @@ -15,8 +15,9 @@ import { SHUNT_COMPENSATOR_ACTIVATION_THRESHOLD, UPDATE_BUS_VOLTAGE, } from './constants'; -import { LineSeparator, ParameterFloat, ParameterSwitch, ReactivePowerAdornment } from '../common'; +import { LineSeparator, ParameterFloat, ParameterSwitch } from '../common'; import { parametersStyles } from '../parameters-style'; +import { ReactivePowerAdornment } from '../../../utils/constants/adornments'; export interface GeneralParametersProps { withApplyModifications: boolean; diff --git a/src/components/parameters/voltage-init/voltage-limits-parameters.tsx b/src/components/parameters/voltage-init/voltage-limits-parameters.tsx index 35263d39a..aee984c5a 100644 --- a/src/components/parameters/voltage-init/voltage-limits-parameters.tsx +++ b/src/components/parameters/voltage-init/voltage-limits-parameters.tsx @@ -17,9 +17,9 @@ import { VOLTAGE_LIMITS_MODIFICATION, } from './constants'; import { ElementType, EquipmentType } from '../../../utils'; -import { VoltageAdornment } from '../common'; import { DndColumn, DndColumnType, DndTable, SELECTED } from '../../dnd-table'; import { FILTERS } from '../../../utils/constants/filterConstant'; +import { VoltageAdornment } from '../../../utils/constants/adornments'; export function VoltageLimitsParameters() { const intl = useIntl(); diff --git a/src/hooks/useModificationLabelComputer.tsx b/src/hooks/useModificationLabelComputer.tsx index a805281c2..34144521f 100644 --- a/src/hooks/useModificationLabelComputer.tsx +++ b/src/hooks/useModificationLabelComputer.tsx @@ -8,11 +8,11 @@ import { useIntl } from 'react-intl'; import { useCallback } from 'react'; import type { UUID } from 'node:crypto'; -import { MODIFICATION_TYPES, EquipmentType } from '../utils'; +import { MODIFICATION_TYPES, EquipmentType, ModificationType } from '../utils'; export interface NetworkModificationMetadata { uuid: UUID; - type: string; + type: ModificationType; date: Date; stashed: boolean; activated: boolean; diff --git a/src/services/index.ts b/src/services/index.ts index 12f8d1be1..a617a3fa3 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -17,3 +17,4 @@ export * from './userAdmin'; export * from './utils'; export * from './voltage-init'; export * from './short-circuit-analysis'; +export * from './networkModification'; diff --git a/src/services/networkModification.ts b/src/services/networkModification.ts new file mode 100644 index 000000000..7c1d40e7d --- /dev/null +++ b/src/services/networkModification.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026, 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 type { UUID } from 'node:crypto'; +import { backendFetch, backendFetchText, safeEncodeURIComponent } from './utils'; + +const PREFIX_NETWORK_MODIFICATION_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/network-modification`; + +function getUrl() { + return `${PREFIX_NETWORK_MODIFICATION_QUERIES}/v1/network-modifications`; +} + +export function fetchNetworkModification(modificationUuid: UUID) { + const modificationFetchUrl = `${getUrl()}/${safeEncodeURIComponent(modificationUuid)}`; + console.debug(modificationFetchUrl); + return backendFetch(modificationFetchUrl); +} + +export function updateModification({ modificationUuid, body }: { modificationUuid: UUID; body: string }) { + const url = `${getUrl()}/${safeEncodeURIComponent(modificationUuid)}`; + + console.info('Updating modification', { url }); + + return backendFetchText(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + }); +} diff --git a/src/services/security-analysis.ts b/src/services/security-analysis.ts index 751007d0b..2dc3da8ac 100644 --- a/src/services/security-analysis.ts +++ b/src/services/security-analysis.ts @@ -6,13 +6,9 @@ */ import type { UUID } from 'node:crypto'; -import { backendFetch, backendFetchJson, backendFetchText } from './utils'; +import { backendFetch, backendFetchJson, backendFetchText, safeEncodeURIComponent } from './utils'; import { PREFIX_STUDY_QUERIES } from './loadflow'; -export function safeEncodeURIComponent(value: string | null | undefined): string { - return value != null ? encodeURIComponent(value) : ''; -} - const PREFIX_SECURITY_ANALYSIS_SERVER_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/security-analysis`; export const getStudyUrl = (studyUuid: UUID | null) => diff --git a/src/services/sensitivity-analysis.ts b/src/services/sensitivity-analysis.ts index 3fecaeac0..f39a1d3fd 100644 --- a/src/services/sensitivity-analysis.ts +++ b/src/services/sensitivity-analysis.ts @@ -5,8 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { UUID } from 'node:crypto'; -import { backendFetch, backendFetchJson, backendFetchText } from './utils'; -import { safeEncodeURIComponent } from './security-analysis'; +import { backendFetch, backendFetchJson, backendFetchText, safeEncodeURIComponent } from './utils'; import { FactorsCount, SensitivityAnalysisParametersInfos } from '../utils'; import { PREFIX_STUDY_QUERIES } from './loadflow'; diff --git a/src/services/study.ts b/src/services/study.ts index 0b97def65..ee4a396ce 100644 --- a/src/services/study.ts +++ b/src/services/study.ts @@ -6,17 +6,20 @@ */ import type { UUID } from 'node:crypto'; -import { backendFetch, backendFetchJson } from './utils'; +import { backendFetch, backendFetchJson, safeEncodeURIComponent } from './utils'; import { NetworkVisualizationParameters } from '../components/parameters/network-visualizations/network-visualizations.types'; import { type ShortCircuitParametersInfos } from '../components/parameters/short-circuit/short-circuit-parameters.type'; import { VoltageInitStudyParameters } from '../components/parameters/voltage-init/voltage-init.type'; const PREFIX_STUDY_QUERIES = `${import.meta.env.VITE_API_GATEWAY}/study`; +const getStudyUrl = (studyUuid: UUID | null) => + `${PREFIX_STUDY_QUERIES}/v1/studies/${safeEncodeURIComponent(studyUuid)}`; + export function exportFilter(studyUuid: UUID, filterUuid?: UUID, token?: string) { console.info('get filter export on study root node'); return backendFetchJson( - `${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/filters/${filterUuid}/elements`, + `${getStudyUrl(studyUuid)}/filters/${filterUuid}/elements`, { method: 'get', headers: { 'Content-Type': 'application/json' }, @@ -32,12 +35,12 @@ export function getAvailableComponentLibraries(): Promise { export function getStudyNetworkVisualizationsParameters(studyUuid: UUID): Promise { console.info('get study network visualization parameters'); - return backendFetchJson(`${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/network-visualizations/parameters`); + return backendFetchJson(`${getStudyUrl(studyUuid)}/network-visualizations/parameters`); } export function setStudyNetworkVisualizationParameters(studyUuid: UUID, newParams: Record) { console.info('set study network visualization parameters'); - return backendFetch(`${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/network-visualizations/parameters`, { + return backendFetch(`${getStudyUrl(studyUuid)}/network-visualizations/parameters`, { method: 'POST', headers: { Accept: 'application/json', @@ -49,12 +52,12 @@ export function setStudyNetworkVisualizationParameters(studyUuid: UUID, newParam export function getStudyShortCircuitParameters(studyUuid: UUID): Promise { console.info('get study short-circuit parameters'); - return backendFetchJson(`${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/short-circuit-analysis/parameters`); + return backendFetchJson(`${getStudyUrl(studyUuid)}/short-circuit-analysis/parameters`); } export function updateVoltageInitParameters(studyUuid: UUID | null, newParams: VoltageInitStudyParameters) { console.info('set study voltage init parameters'); - const url = `${PREFIX_STUDY_QUERIES}/v1/studies/${studyUuid}/voltage-init/parameters`; + const url = `${getStudyUrl(studyUuid)}/voltage-init/parameters`; console.debug(url); return backendFetch(url, { method: 'POST', diff --git a/src/services/utils.ts b/src/services/utils.ts index d00fc54c2..6120b6c50 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -174,3 +174,7 @@ export const backendFetchFile = (url: string, init: RequestInit, token?: string) export const getRequestParamFromList = (paramName: string, params: string[] = []) => { return new URLSearchParams(params.map((param) => [paramName, param])); }; + +export function safeEncodeURIComponent(value: string | null | undefined): string { + return value != null ? encodeURIComponent(value) : ''; +} diff --git a/src/translations/en/commonButtonEn.ts b/src/translations/en/commonButtonEn.ts index 839a024d4..c629bdda7 100644 --- a/src/translations/en/commonButtonEn.ts +++ b/src/translations/en/commonButtonEn.ts @@ -10,4 +10,5 @@ export const commonButtonEn = { validate: 'Validate', 'button.changeType': 'Change', 'button.changeOperator': 'Change', + 'button.restore': 'Restore', }; diff --git a/src/translations/en/networkModificationsEn.ts b/src/translations/en/networkModificationsEn.ts index 8f4ace6f8..dc41bf4bc 100644 --- a/src/translations/en/networkModificationsEn.ts +++ b/src/translations/en/networkModificationsEn.ts @@ -76,4 +76,14 @@ export const networkModificationsEn = { 'network_modifications.CREATE_VOLTAGE_LEVEL_TOPOLOGY': 'Creating a busbar in voltage level {computedLabel}', 'network_modifications.CREATE_VOLTAGE_LEVEL_SECTION': 'Adding busbar section to voltage level {computedLabel}', 'network_modifications.MOVE_VOLTAGE_LEVEL_FEEDER_BAYS': 'Moving feeder bays in voltage level {computedLabel}', + ModificationReadError: 'An error occurred while fetching the modification', + CreateSubstation: 'Create substation', + SubstationCreationError: 'Error while creating substation', + AdditionalInformation: 'Additional information', + AddProperty: 'Add property', + PropertyName: 'Property name', + PropertyValue: 'Property value', + Name: 'Name', + Country: 'Country', + DuplicatedPropsError: 'Duplicated properties: each property must be unique', }; diff --git a/src/translations/fr/commonButtonFr.ts b/src/translations/fr/commonButtonFr.ts index 47f4a3afd..9c153de7b 100644 --- a/src/translations/fr/commonButtonFr.ts +++ b/src/translations/fr/commonButtonFr.ts @@ -10,4 +10,5 @@ export const commonButtonFr = { validate: 'Valider', 'button.changeType': 'Modifier', 'button.changeOperator': 'Modifier', + 'button.restore': 'Restaurer', }; diff --git a/src/translations/fr/networkModificationsFr.ts b/src/translations/fr/networkModificationsFr.ts index 08fd2ae91..1cf3ce204 100644 --- a/src/translations/fr/networkModificationsFr.ts +++ b/src/translations/fr/networkModificationsFr.ts @@ -80,4 +80,14 @@ export const networkModificationsFr = { 'network_modifications.CREATE_VOLTAGE_LEVEL_TOPOLOGY': "Ajout d'un jeu de barre dans le poste {computedLabel}", 'network_modifications.CREATE_VOLTAGE_LEVEL_SECTION': 'Ajout de section / tronçon dans le poste {computedLabel}', 'network_modifications.MOVE_VOLTAGE_LEVEL_FEEDER_BAYS': 'Déplacements de départs dans le poste {computedLabel}', + ModificationReadError: 'Une erreur est survenue lors de la récupération de la modification', + CreateSubstation: 'Créer un site', + SubstationCreationError: "Erreur lors de la création d'un site'", + AdditionalInformation: 'Informations complémentaires', + AddProperty: 'Ajouter une propriété', + PropertyName: 'Nom de la propriété', + PropertyValue: 'Valeur de la propriété', + Name: 'Nom', + Country: 'Pays', + DuplicatedPropsError: 'Propriétés dupliquées : chaque propriété doit être unique', }; diff --git a/src/utils/constants/adornments.ts b/src/utils/constants/adornments.ts new file mode 100644 index 000000000..e206e0442 --- /dev/null +++ b/src/utils/constants/adornments.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2026, 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 { + AMPERE, + KILO_AMPERE, + KILO_METER, + KILO_VOLT, + MEGA_VAR, + MEGA_VOLT_AMPERE, + MEGA_WATT, + MICRO_SIEMENS, + OHM, + PERCENTAGE, + SIEMENS, +} from './unitsConstants'; + +export const MicroSusceptanceAdornment = { + position: 'end', + text: MICRO_SIEMENS, +}; + +export const SusceptanceAdornment = { + position: 'end', + text: SIEMENS, +}; +export const OhmAdornment = { + position: 'end', + text: OHM, +}; +export const AmpereAdornment = { + position: 'end', + text: AMPERE, +}; + +export const KiloAmpereAdornment = { + position: 'end', + text: KILO_AMPERE, +}; + +export const ActivePowerAdornment = { + position: 'end', + text: MEGA_WATT, +}; +export const ReactivePowerAdornment = { + position: 'end', + text: MEGA_VAR, +}; +export const MVAPowerAdornment = { + position: 'end', + text: MEGA_VOLT_AMPERE, +}; +export const VoltageAdornment = { + position: 'end', + text: KILO_VOLT, +}; + +export const KilometerAdornment = { + position: 'end', + text: KILO_METER, +}; + +export const PercentageAdornment = { + position: 'end', + text: PERCENTAGE, +}; diff --git a/src/utils/constants/fieldConstants.ts b/src/utils/constants/fieldConstants.ts index 337fa6cda..8953e17a5 100644 --- a/src/utils/constants/fieldConstants.ts +++ b/src/utils/constants/fieldConstants.ts @@ -6,6 +6,8 @@ */ export enum FieldConstants { + ADDED = 'added', + ADDITIONAL_PROPERTIES = 'AdditionalProperties', AG_GRID_ROW_UUID = 'agGridRowUuid', API_CALL = 'apiCall', CASE_FILE = 'caseFile', @@ -17,11 +19,14 @@ export enum FieldConstants { COUNTRIES_1 = 'countries1', COUNTRIES_2 = 'countries2', COUNTRIES = 'countries', + COUNTRY = 'country', CURRENT_PARAMETERS = 'currentParameters', + DELETION_MARK = 'deletionMark', DESCRIPTION = 'description', DIRECTORY = 'directory', ENERGY_SOURCE = 'energySource', EQUIPMENT_ID = 'equipmentID', + EQUIPMENT_NAME = 'equipmentName', EQUIPMENT_IDS = 'equipmentIDs', EQUIPMENT_TABLE = 'equipmentTable', EQUIPMENT_TYPE = 'equipmentType', @@ -38,12 +43,14 @@ export enum FieldConstants { NOMINAL_VOLTAGE = 'nominalVoltage', OPERATION_TYPE = 'type', TYPE = 'type', + PREVIOUS_VALUE = 'previousValue', PROPERTY_NAME = 'propertyName', PROPERTY_OPERATOR = 'propertyOperator', PROPERTY = 'PROPERTY', PROPERTY_VALUES = 'propertyValues', SCRIPT = 'script', STUDY_NAME = 'studyName', + VALUE = 'value', VALUE_1 = 'value1', VALUE_2 = 'value2', } diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 39471fd78..e390ecb6a 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -4,6 +4,7 @@ * 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 * from './adornments'; export * from './fetchStatus'; export * from './fieldConstants'; export * from './uiConstants'; diff --git a/src/utils/index.ts b/src/utils/index.ts index decfdf2e0..9436b0781 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,12 +9,13 @@ export * from './constants'; export * from './conversionUtils'; export * from './error'; export * from './functions'; +export * from './labelUtils'; export * from './langs'; export * from './mapper'; export * from './navigator-clipboard'; export * from './constants/notificationsProvider'; export * from './styles'; export * from './types'; +export * from './ts-utils'; export * from './validation-functions'; -export * from './labelUtils'; export { default as yupConfig } from './yupConfig'; diff --git a/src/utils/labelUtils.ts b/src/utils/labelUtils.ts index 294b1abb9..95cfbabd4 100644 --- a/src/utils/labelUtils.ts +++ b/src/utils/labelUtils.ts @@ -13,10 +13,3 @@ export function getEquipmentTypeShortLabel(equipmentType: string | undefined): s } return ALL_EQUIPMENTS[equipmentType as keyof typeof ALL_EQUIPMENTS]?.shortLabel ?? ''; } - -export function getEquipmentTypeTagLabel(equipmentType: string | undefined): string { - if (!equipmentType) { - return ''; - } - return ALL_EQUIPMENTS[equipmentType as keyof typeof ALL_EQUIPMENTS]?.tagLabel ?? ''; -} diff --git a/src/utils/ts-utils.ts b/src/utils/ts-utils.ts new file mode 100644 index 000000000..0f37859bb --- /dev/null +++ b/src/utils/ts-utils.ts @@ -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/. + */ +export type Nullable = { [K in keyof T]: T[K] | null }; +export type DeepNullable = { + [K in keyof T]: DeepNullable | null; +}; + +export function notUndefined(value: T | undefined): value is T { + return value !== undefined; +} + +export function notNull(value: T | null): value is T { + return value !== null; +} + +export const removeNullFields = >(data: T): T => { + const result = Object.entries(data).reduce>((acc, [key, value]) => { + if (value === null) { + return acc; + } + + if (typeof value === 'object' && !Array.isArray(value)) { + const cleaned = removeNullFields(value); + if (Object.keys(cleaned).length > 0) { + acc[key] = cleaned; + } + return acc; + } + + acc[key] = value; + return acc; + }, {}); + + return result as T; +}; + +export function parseIntData(val: string | number, defaultValue: string | number) { + const intValue = Number.parseInt(String(val), 10); + return Number.isNaN(intValue) ? defaultValue : intValue; +} + +export function sanitizeString(val: string | null | undefined): string | null { + const trimmedValue = val?.trim(); + return trimmedValue === undefined || trimmedValue === '' ? null : trimmedValue; +} + +export const getIdOrSelf = (e: any) => e?.id ?? e;