Skip to content

Commit 5b69ac7

Browse files
authored
Add the possibility to add et edit limits properties (#3364)
Signed-off-by: basseche <[email protected]>
1 parent cfe381f commit 5b69ac7

16 files changed

+379
-64
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
export enum LimitsPropertyName {
9+
LIMITS_TYPE = 'Limit type',
10+
}
11+
12+
export function getPropertyAvatar(type: string): string {
13+
const transformedType: LimitsPropertyName = type as LimitsPropertyName;
14+
15+
const descriptions: Record<LimitsPropertyName, string> = {
16+
[LimitsPropertyName.LIMITS_TYPE]: 'Ty',
17+
};
18+
19+
return descriptions[transformedType] ?? transformedType.substring(0, 2);
20+
}

src/components/dialogs/limits/limits-pane-utils.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ID,
1515
LIMIT_SETS_MODIFICATION_TYPE,
1616
LIMITS,
17+
LIMITS_PROPERTIES,
1718
NAME,
1819
OPERATIONAL_LIMITS_GROUPS,
1920
PERMANENT_LIMIT,
@@ -24,6 +25,7 @@ import {
2425
TEMPORARY_LIMIT_NAME,
2526
TEMPORARY_LIMIT_VALUE,
2627
TEMPORARY_LIMITS,
28+
VALUE,
2729
} from 'components/utils/field-constants';
2830
import {
2931
areArrayElementsUnique,
@@ -35,6 +37,7 @@ import yup from 'components/utils/yup-config';
3537
import {
3638
AttributeModification,
3739
CurrentLimits,
40+
CurrentLimitsData,
3841
OperationalLimitsGroup,
3942
OperationType,
4043
TemporaryLimit,
@@ -52,6 +55,7 @@ const limitsGroupValidationSchema = (isModification: boolean) => ({
5255
[NAME]: yup.string().nonNullable().required(),
5356
[APPLICABIlITY]: yup.string().nonNullable().required(),
5457
[CURRENT_LIMITS]: yup.object().shape(currentLimitsValidationSchema(isModification)),
58+
[LIMITS_PROPERTIES]: yup.array().of(limitsPropertyValidationSchema()),
5559
});
5660

5761
const temporaryLimitsValidationSchema = () => {
@@ -67,6 +71,12 @@ const temporaryLimitsValidationSchema = () => {
6771
}),
6872
});
6973
};
74+
const limitsPropertyValidationSchema = () => {
75+
return yup.object().shape({
76+
[NAME]: yup.string().required(),
77+
[VALUE]: yup.string().required(),
78+
});
79+
};
7080

7181
const currentLimitsValidationSchema = (isModification = false) => ({
7282
[PERMANENT_LIMIT]: isModification
@@ -138,6 +148,7 @@ export const formatOpLimitGroupsToFormInfos = (
138148
id: opLimitGroup.id + opLimitGroup.applicability,
139149
name: opLimitGroup.id,
140150
applicability: opLimitGroup.applicability,
151+
limitsProperties: opLimitGroup.limitsProperties,
141152
currentLimits: {
142153
id: opLimitGroup.currentLimits.id,
143154
permanentLimit: opLimitGroup.currentLimits.permanentLimit,
@@ -225,12 +236,13 @@ export const updateTemporaryLimits = (
225236
return updatedTemporaryLimits;
226237
};
227238

228-
export const mapServerLimitsGroupsToFormInfos = (currentLimits: CurrentLimits[]) => {
229-
return currentLimits?.map((currentLimit: CurrentLimits) => {
239+
export const mapServerLimitsGroupsToFormInfos = (currentLimits: CurrentLimitsData[]) => {
240+
return currentLimits?.map((currentLimit: CurrentLimitsData) => {
230241
return {
231242
id: currentLimit.id + currentLimit.applicability,
232243
name: currentLimit.id,
233244
applicability: currentLimit.applicability,
245+
limitsProperties: currentLimit.limitsProperties,
234246
currentLimits: {
235247
id: currentLimit.id,
236248
permanentLimit: null,
@@ -253,7 +265,7 @@ export const combineFormAndMapServerLimitsGroups = (
253265
// updates limit values :
254266
for (const opLG of updatedOpLG) {
255267
const equivalentFromMapServer = mapServerBranch.currentLimits?.find(
256-
(currentLimit: CurrentLimits) =>
268+
(currentLimit: CurrentLimitsData) =>
257269
currentLimit.id === opLG.name && currentLimit.applicability === opLG[APPLICABIlITY]
258270
);
259271
if (equivalentFromMapServer !== undefined) {
@@ -265,7 +277,7 @@ export const combineFormAndMapServerLimitsGroups = (
265277
}
266278

267279
// adds all the operational limits groups from mapServerBranch THAT ARE NOT DELETED by the netmod
268-
mapServerBranch.currentLimits?.forEach((currentLimit: CurrentLimits) => {
280+
for (const currentLimit of mapServerBranch.currentLimits) {
269281
const equivalentFromNetMod = updatedOpLG.find(
270282
(opLG: OperationalLimitsGroupFormInfos) =>
271283
currentLimit.id === opLG.name && currentLimit.applicability === opLG[APPLICABIlITY]
@@ -275,14 +287,15 @@ export const combineFormAndMapServerLimitsGroups = (
275287
id: currentLimit.id + currentLimit.applicability,
276288
name: currentLimit.id,
277289
applicability: currentLimit.applicability,
290+
limitsProperties: currentLimit.limitsProperties,
278291
currentLimits: {
279292
id: currentLimit.id,
280293
permanentLimit: null,
281294
temporaryLimits: formatToTemporaryLimitsFormInfos(currentLimit.temporaryLimits),
282295
},
283296
});
284297
}
285-
});
298+
}
286299

287300
return updatedOpLG;
288301
};
@@ -344,6 +357,7 @@ export const addModificationTypeToOpLimitsGroups = (
344357
id: limitsGroupForm.id,
345358
name: limitsGroupForm.name,
346359
applicability: limitsGroupForm.applicability,
360+
limitsProperties: limitsGroupForm.limitsProperties,
347361
currentLimits: currentLimits,
348362
modificationType: LIMIT_SETS_MODIFICATION_TYPE.MODIFY_OR_ADD,
349363
temporaryLimitsModificationType: TEMPORARY_LIMIT_MODIFICATION_TYPE.REPLACE,

src/components/dialogs/limits/limits-pane.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import { Box, Grid } from '@mui/material';
99
import {
10-
CURRENT_LIMITS,
1110
ENABLE_OLG_MODIFICATION,
1211
LIMITS,
1312
OPERATIONAL_LIMITS_GROUPS,
@@ -220,7 +219,7 @@ export function LimitsPane({
220219
index === indexSelectedLimitSet && (
221220
<LimitsSidePane
222221
key={operationalLimitsGroup.id}
223-
limitsGroupFormName={`${id}.${OPERATIONAL_LIMITS_GROUPS}[${index}].${CURRENT_LIMITS}`}
222+
opLimitsGroupFormName={`${id}.${OPERATIONAL_LIMITS_GROUPS}[${index}]`}
224223
limitsGroupApplicabilityName={`${id}.${OPERATIONAL_LIMITS_GROUPS}[${index}]`}
225224
clearableFields={clearableFields}
226225
permanentCurrentLimitPreviousValue={
@@ -239,6 +238,7 @@ export function LimitsPane({
239238
selectedLimitSetName={operationalLimitsGroup.name}
240239
checkLimitSetUnicity={checkLimitSetUnicity}
241240
disabled={!olgEditable}
241+
isModification={isAModification}
242242
/>
243243
)
244244
)}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
import { LimitsTagChip } from './limits-tag-chip';
8+
import { Autocomplete, AutocompleteRenderInputParams, Box, Stack, TextField, IconButton } from '@mui/material';
9+
import { useCallback, useMemo, useState } from 'react';
10+
import { Delete } from '@mui/icons-material';
11+
import { useIntl } from 'react-intl';
12+
import { LimitsProperty } from '../../../services/network-modification-types';
13+
import { useFieldArray } from 'react-hook-form';
14+
import { usePredefinedProperties } from '@gridsuite/commons-ui';
15+
import AddIcon from '@mui/icons-material/ControlPoint';
16+
17+
export interface LimitsPropertiesSideStackProps {
18+
name: string;
19+
disabled?: boolean;
20+
}
21+
export function LimitsPropertiesSideStack({ name, disabled }: Readonly<LimitsPropertiesSideStackProps>) {
22+
const {
23+
fields: limitsProperties,
24+
append,
25+
remove,
26+
} = useFieldArray<{ [key: string]: LimitsProperty[] }>({ name: name });
27+
28+
const [isEditing, setIsEditing] = useState<boolean>(false);
29+
const [hovered, setHovered] = useState<boolean>(false);
30+
const [propertyName, setPropertyName] = useState<string>('');
31+
const [propertyValue, setPropertyValue] = useState<string>('');
32+
const [nameEditorError, setNameEditorError] = useState<string>('');
33+
const [valueEditorError, setValueEditorError] = useState<string>('');
34+
const intl = useIntl();
35+
36+
const [predefinedProperties] = usePredefinedProperties('limitsGroup');
37+
const predefinedPropertiesNames = useMemo(() => {
38+
return Object.keys(predefinedProperties ?? {}).sort((a, b) => a.localeCompare(b));
39+
}, [predefinedProperties]);
40+
41+
const handleKeyPress = useCallback(
42+
(event: React.KeyboardEvent<HTMLInputElement>) => {
43+
if (event.key === 'Enter') {
44+
setNameEditorError('');
45+
setValueEditorError('');
46+
47+
let error = false;
48+
if (propertyName.trim() === '') {
49+
setNameEditorError(intl.formatMessage({ id: 'FieldNotEmpty' }));
50+
error = true;
51+
}
52+
if (propertyValue.trim() === '') {
53+
setValueEditorError(intl.formatMessage({ id: 'FieldNotEmpty' }));
54+
error = true;
55+
}
56+
57+
if (error) {
58+
return;
59+
}
60+
61+
if (limitsProperties.some((l) => l.name === propertyName)) {
62+
setNameEditorError(intl.formatMessage({ id: 'UniqueName' }));
63+
return;
64+
} else {
65+
append({ name: propertyName, value: propertyValue });
66+
setPropertyName('');
67+
setPropertyValue('');
68+
}
69+
setIsEditing(false);
70+
}
71+
},
72+
[append, intl, limitsProperties, propertyName, propertyValue]
73+
);
74+
75+
const handleOnChange = useCallback((value: string) => {
76+
setPropertyName(value);
77+
setNameEditorError('');
78+
}, []);
79+
80+
return (
81+
<Stack direction="column" spacing={2} paddingBottom={2} flexWrap="wrap">
82+
<Stack direction="row" sx={{ display: 'flex', flexWrap: 'wrap' }}>
83+
{limitsProperties?.map((property: LimitsProperty, index: number) => (
84+
<LimitsTagChip
85+
key={`${property.name}`}
86+
limitsProperty={property}
87+
onDelete={() => remove(index)}
88+
disabled={disabled}
89+
showTooltip
90+
/>
91+
))}
92+
{!isEditing && (
93+
<IconButton
94+
color="primary"
95+
sx={{ verticalAlign: 'center' }}
96+
onClick={() => setIsEditing(true)}
97+
disabled={disabled}
98+
>
99+
<AddIcon />
100+
</IconButton>
101+
)}
102+
</Stack>
103+
{isEditing && !disabled ? (
104+
<Box
105+
display="flex"
106+
gap={2}
107+
width="100%"
108+
onMouseEnter={() => setHovered(true)}
109+
onMouseLeave={() => setHovered(false)}
110+
>
111+
<Autocomplete
112+
options={Object.values(predefinedPropertiesNames)}
113+
size="small"
114+
onChange={(event, value) => handleOnChange(value ?? '')}
115+
renderInput={(params: AutocompleteRenderInputParams) => (
116+
<TextField
117+
label={intl.formatMessage({ id: 'PropertyName' })}
118+
variant="outlined"
119+
{...params}
120+
onChange={(event) => handleOnChange(event.target.value)}
121+
fullWidth
122+
error={nameEditorError !== ''}
123+
helperText={nameEditorError}
124+
onKeyDown={handleKeyPress}
125+
/>
126+
)}
127+
sx={{ flex: 1 }}
128+
freeSolo={true}
129+
/>
130+
<TextField
131+
size="small"
132+
label={intl.formatMessage({ id: 'PropertyValue' })}
133+
sx={{ flex: 1, verticalAlign: 'center' }}
134+
onKeyDown={handleKeyPress}
135+
onChange={(event) => setPropertyValue(event.target.value)}
136+
error={valueEditorError !== ''}
137+
helperText={valueEditorError}
138+
/>
139+
{hovered && (
140+
<IconButton
141+
sx={{ verticalAlign: 'center' }}
142+
onClick={() => {
143+
setIsEditing(false);
144+
setNameEditorError('');
145+
}}
146+
>
147+
<Delete />
148+
</IconButton>
149+
)}
150+
</Box>
151+
) : (
152+
''
153+
)}
154+
</Stack>
155+
);
156+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { Avatar, Stack } from '@mui/material';
9+
import { LimitsProperty } from '../../../services/network-modification-types';
10+
import { LimitsTagChip } from './limits-tag-chip';
11+
import { useWatch } from 'react-hook-form';
12+
13+
const MAX_PROPERTIES_TO_RENDER: number = 2;
14+
15+
export interface LimitsPropertiesStackProps {
16+
name: string;
17+
}
18+
19+
function getLimitsPropertiesToRender(limitsProperties: LimitsProperty[]) {
20+
return limitsProperties.length < MAX_PROPERTIES_TO_RENDER ? limitsProperties : limitsProperties?.slice(0, 2);
21+
}
22+
23+
export function LimitsPropertiesStack({ name }: Readonly<LimitsPropertiesStackProps>) {
24+
const limitsProperties: LimitsProperty[] | undefined = useWatch({ name: name });
25+
const propertiesToRender: LimitsProperty[] = getLimitsPropertiesToRender(limitsProperties ?? []);
26+
27+
return (
28+
<Stack direction="row" sx={{ alignItems: 'center' }}>
29+
{propertiesToRender.map((property: LimitsProperty) => (
30+
<LimitsTagChip key={`${property.name}`} limitsProperty={property} />
31+
))}
32+
{limitsProperties && propertiesToRender.length !== limitsProperties.length ? (
33+
<Avatar
34+
sx={{ width: 30, height: 30 }}
35+
>{`+${limitsProperties.length - propertiesToRender.length}`}</Avatar>
36+
) : (
37+
''
38+
)}
39+
</Stack>
40+
);
41+
}

0 commit comments

Comments
 (0)