Skip to content

Commit 95fe136

Browse files
committed
add the possibility to add et edit limits properties
1 parent 7e840ea commit 95fe136

16 files changed

+332
-40
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 = 'LimitsType',
10+
ASSOCIATION = 'association',
11+
ORIGIN = 'origin',
12+
}
13+
14+
export function getPropertyAvatar(type: string): string {
15+
const transformedType: LimitsPropertyName | undefined = type as LimitsPropertyName;
16+
17+
const descriptions: Record<LimitsPropertyName, string> = {
18+
[LimitsPropertyName.LIMITS_TYPE]: 'Ty',
19+
[LimitsPropertyName.ASSOCIATION]: 'As',
20+
[LimitsPropertyName.ORIGIN]: 'Pr',
21+
};
22+
23+
return descriptions[transformedType] ?? transformedType.substring(0, 2);
24+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import yup from 'components/utils/yup-config';
3434
import {
3535
AttributeModification,
3636
CurrentLimits,
37+
CurrentLimitsData,
3738
OperationalLimitsGroup,
3839
OperationType,
3940
TemporaryLimit,
@@ -135,6 +136,7 @@ export const formatOpLimitGroupsToFormInfos = (
135136
id: opLimitGroup.id + opLimitGroup.applicability,
136137
name: opLimitGroup.id,
137138
applicability: opLimitGroup.applicability,
139+
limitsProperties: opLimitGroup.limitsProperties,
138140
currentLimits: {
139141
id: opLimitGroup.currentLimits.id,
140142
permanentLimit: opLimitGroup.currentLimits.permanentLimit,
@@ -168,6 +170,7 @@ export const sanitizeLimitsGroups = (
168170
limitsGroups.map(({ currentLimits, ...baseData }) => ({
169171
...baseData,
170172
id: baseData.name,
173+
limitsProperties: baseData.limitsProperties,
171174
currentLimits: !currentLimits
172175
? {
173176
id: '',
@@ -233,7 +236,7 @@ export const combineFormAndMapServerLimitsGroups = (
233236
// updates limit values :
234237
updatedOpLG.forEach((opLG: OperationalLimitsGroupFormInfos) => {
235238
const equivalentFromMapServer = mapServerBranch.currentLimits?.find(
236-
(currentLimit: CurrentLimits) =>
239+
(currentLimit: CurrentLimitsData) =>
237240
currentLimit.id === opLG.name && currentLimit.applicability === opLG[APPLICABIlITY]
238241
);
239242
if (equivalentFromMapServer !== undefined) {
@@ -245,7 +248,7 @@ export const combineFormAndMapServerLimitsGroups = (
245248
});
246249

247250
// adds all the operational limits groups from mapServerBranch THAT ARE NOT DELETED by the netmod
248-
mapServerBranch.currentLimits?.forEach((currentLimit: CurrentLimits) => {
251+
mapServerBranch.currentLimits?.forEach((currentLimit: CurrentLimitsData) => {
249252
const equivalentFromNetMod = updatedOpLG.find(
250253
(opLG: OperationalLimitsGroupFormInfos) =>
251254
currentLimit.id === opLG.name && currentLimit.applicability === opLG[APPLICABIlITY]
@@ -255,6 +258,7 @@ export const combineFormAndMapServerLimitsGroups = (
255258
id: currentLimit.id + currentLimit.applicability,
256259
name: currentLimit.id,
257260
applicability: currentLimit.applicability,
261+
limitsProperties: currentLimit.limitsProperties,
258262
currentLimits: {
259263
id: currentLimit.id,
260264
permanentLimit: null,

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

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

88
import { Grid } from '@mui/material';
99
import {
10-
CURRENT_LIMITS,
1110
LIMITS,
1211
OPERATIONAL_LIMITS_GROUPS,
1312
SELECTED_LIMITS_GROUP_1,
@@ -159,7 +158,7 @@ export function LimitsPane({
159158
index === indexSelectedLimitSet && (
160159
<LimitsSidePane
161160
key={operationalLimitsGroup.id}
162-
limitsGroupFormName={`${id}.${OPERATIONAL_LIMITS_GROUPS}[${index}].${CURRENT_LIMITS}`}
161+
OplimitsGroupFormName={`${id}.${OPERATIONAL_LIMITS_GROUPS}[${index}]`}
163162
limitsGroupApplicabilityName={`${id}.${OPERATIONAL_LIMITS_GROUPS}[${index}]`}
164163
clearableFields={clearableFields}
165164
permanentCurrentLimitPreviousValue={
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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, useState } from 'react';
10+
import { AddCircle, Delete } from '@mui/icons-material';
11+
import { LimitsPropertyName } from './limits-constants';
12+
import { useIntl } from 'react-intl';
13+
import { LimitsProperty } from '../../../services/network-modification-types';
14+
import { useFormContext, useWatch } from 'react-hook-form';
15+
16+
export interface LimitsPropertiesSideStackProps {
17+
formName: string;
18+
}
19+
export function LimitsPropertiesSideStack({ formName, ...props }: Readonly<LimitsPropertiesSideStackProps>) {
20+
const { setValue } = useFormContext();
21+
const limitsProperties: LimitsProperty[] = useWatch({ name: formName });
22+
23+
const [isEditing, setIsEditing] = useState<boolean>(false);
24+
const [hovered, setHovered] = useState<boolean>(false);
25+
const [propertyName, setPropertyName] = useState<string>('');
26+
const [propertyValue, setPropertyValue] = useState<string>('');
27+
const [editorError, setEditorError] = useState<string>('');
28+
const intl = useIntl();
29+
30+
const handleKeyPress = useCallback(
31+
(event: React.KeyboardEvent<HTMLInputElement>) => {
32+
if (event.key === 'Enter') {
33+
if (!propertyName.trim() || !propertyValue.trim()) {
34+
return;
35+
}
36+
if (!limitsProperties?.length) {
37+
setValue(formName, [{ name: propertyName, value: propertyValue }]);
38+
} else if (limitsProperties.find((l) => l.name === propertyName)) {
39+
setEditorError(intl.formatMessage({ id: 'UsedPropertyName' }));
40+
return;
41+
} else {
42+
setValue(formName, [...limitsProperties, { name: propertyName, value: propertyValue }]);
43+
}
44+
setIsEditing(false);
45+
}
46+
},
47+
[formName, intl, limitsProperties, propertyName, propertyValue, setValue]
48+
);
49+
50+
const handleOnChange = useCallback((value: string) => {
51+
setPropertyName(value);
52+
setEditorError('');
53+
}, []);
54+
55+
const handleDelete = useCallback(
56+
(index: number) => {
57+
if (limitsProperties?.length <= index) {
58+
return;
59+
}
60+
setValue(formName, [...limitsProperties.slice(0, index), ...limitsProperties.slice(index + 1)]);
61+
},
62+
[formName, limitsProperties, setValue]
63+
);
64+
65+
return (
66+
<Stack direction="column" spacing={2} paddingBottom={2} flexWrap="wrap">
67+
<Stack direction="row" {...props} sx={{ display: 'flex', flexWrap: 'wrap' }}>
68+
{limitsProperties?.map((property: LimitsProperty, index: number) => (
69+
<LimitsTagChip
70+
key={`${property.name}`}
71+
limitsProperty={property}
72+
onDelete={() => handleDelete(index)}
73+
/>
74+
))}
75+
{!isEditing ? (
76+
<IconButton color="primary" sx={{ verticalAlign: 'center' }} onClick={() => setIsEditing(true)}>
77+
<AddCircle />
78+
</IconButton>
79+
) : (
80+
''
81+
)}
82+
</Stack>
83+
{isEditing ? (
84+
<Box
85+
display="flex"
86+
gap={2}
87+
width="100%"
88+
onMouseEnter={() => setHovered(true)}
89+
onMouseLeave={() => setHovered(false)}
90+
>
91+
<Autocomplete
92+
options={Object.values(LimitsPropertyName)}
93+
size="small"
94+
onChange={(event, value) => handleOnChange(value ?? '')}
95+
renderInput={(params: AutocompleteRenderInputParams) => (
96+
<TextField
97+
label={intl.formatMessage({ id: 'PropertyName' })}
98+
variant="outlined"
99+
{...params}
100+
onChange={(event) => handleOnChange(event.target.value)}
101+
fullWidth
102+
error={!!editorError?.length}
103+
helperText={editorError}
104+
onKeyDown={handleKeyPress}
105+
/>
106+
)}
107+
sx={{ flex: 1 }}
108+
freeSolo={true}
109+
/>
110+
<TextField
111+
size="small"
112+
label={intl.formatMessage({ id: 'PropertyValue' })}
113+
sx={{ flex: 1 }}
114+
onKeyDown={handleKeyPress}
115+
onChange={(event) => setPropertyValue(event.target.value)}
116+
/>
117+
{hovered && (
118+
<IconButton sx={{ verticalAlign: 'center' }} onClick={() => setIsEditing(false)}>
119+
<Delete />
120+
</IconButton>
121+
)}
122+
</Box>
123+
) : (
124+
''
125+
)}
126+
</Stack>
127+
);
128+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 { useMemo } from 'react';
11+
import { LimitsTagChip } from './limits-tag-chip';
12+
import { useWatch } from 'react-hook-form';
13+
14+
const MAX_PROPERTIES_TO_RENDER: number = 2;
15+
16+
export interface LimitsPropertiesStackProps {
17+
formName: string;
18+
}
19+
export function LimitsPropertiesStack({ formName, ...props }: Readonly<LimitsPropertiesStackProps>) {
20+
const limitsProperties: LimitsProperty[] | undefined = useWatch({ name: formName });
21+
const propertiesToRender: LimitsProperty[] = useMemo(() => {
22+
if (!limitsProperties) {
23+
return [];
24+
}
25+
return limitsProperties.length < MAX_PROPERTIES_TO_RENDER ? limitsProperties : limitsProperties?.slice(0, 2);
26+
}, [limitsProperties]);
27+
28+
return (
29+
<Stack direction="row" {...props}>
30+
{propertiesToRender.map((property: LimitsProperty) => (
31+
<LimitsTagChip key={`${property.name}$${property.value}`} limitsProperty={property} />
32+
))}
33+
{limitsProperties && propertiesToRender.length !== limitsProperties.length ? (
34+
<Avatar>{`+${limitsProperties.length - propertiesToRender.length}`}</Avatar>
35+
) : (
36+
''
37+
)}
38+
</Stack>
39+
);
40+
}

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
import { Box, Grid } from '@mui/material';
88
import { FormattedMessage, useIntl } from 'react-intl';
99
import {
10-
DndColumnType,
1110
ColumnNumeric,
1211
ColumnText,
1312
DndColumn,
13+
DndColumnType,
1414
FloatInput,
15-
SelectInput,
1615
Option,
16+
SelectInput,
1717
} from '@gridsuite/commons-ui';
1818
import {
1919
APPLICABIlITY,
20+
CURRENT_LIMITS,
21+
LIMITS_PROPERTIES,
2022
PERMANENT_LIMIT,
2123
TEMPORARY_LIMIT_DURATION,
2224
TEMPORARY_LIMIT_MODIFICATION_TYPE,
@@ -33,11 +35,11 @@ import { TemporaryLimit } from '../../../services/network-modification-types';
3335
import TemporaryLimitsTable from './temporary-limits-table';
3436
import LimitsChart from './limitsChart';
3537
import { CurrentTreeNode } from '../../graph/tree-node.type';
36-
import GridSection from '../commons/grid-section';
3738
import { APPLICABILITY } from '../../network/constants';
39+
import { LimitsPropertiesSideStack } from './limits-properties-side-stack';
3840

3941
export interface LimitsSidePaneProps {
40-
limitsGroupFormName: string;
42+
OplimitsGroupFormName: string;
4143
limitsGroupApplicabilityName?: string;
4244
permanentCurrentLimitPreviousValue: number | null | undefined;
4345
temporaryLimitsPreviousValues: TemporaryLimit[];
@@ -49,7 +51,7 @@ export interface LimitsSidePaneProps {
4951
}
5052

5153
export function LimitsSidePane({
52-
limitsGroupFormName,
54+
OplimitsGroupFormName,
5355
limitsGroupApplicabilityName,
5456
permanentCurrentLimitPreviousValue,
5557
temporaryLimitsPreviousValues,
@@ -61,6 +63,10 @@ export function LimitsSidePane({
6163
}: Readonly<LimitsSidePaneProps>) {
6264
const intl = useIntl();
6365
const { setError, getValues } = useFormContext();
66+
const limitsGroupFormName = useMemo(
67+
(): string => `${OplimitsGroupFormName}.${CURRENT_LIMITS}`,
68+
[OplimitsGroupFormName]
69+
);
6470
const columnsDefinition: ((ColumnText | ColumnNumeric) & { initialValue: string | null })[] = useMemo(() => {
6571
return [
6672
{
@@ -220,8 +226,8 @@ export function LimitsSidePane({
220226
return (
221227
<Box sx={{ p: 2 }}>
222228
{limitsGroupApplicabilityName && (
223-
<>
224-
<GridSection title={selectedLimitSetName ?? ''} isLiteralText />
229+
<Box>
230+
<LimitsPropertiesSideStack formName={`${OplimitsGroupFormName}.${LIMITS_PROPERTIES}`} />
225231
<Grid container justifyContent="flex-start" alignItems="center" sx={{ paddingBottom: '15px' }}>
226232
<Grid item xs={2}>
227233
<FormattedMessage id="Applicability" />
@@ -249,7 +255,7 @@ export function LimitsSidePane({
249255
/>
250256
</Grid>
251257
</Grid>
252-
</>
258+
</Box>
253259
)}
254260
<Box>
255261
<LimitsChart
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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, Chip, ChipProps, Tooltip } from '@mui/material';
9+
import { getPropertyAvatar } from './limits-constants';
10+
import * as React from 'react';
11+
import { LimitsProperty } from '../../../services/network-modification-types';
12+
13+
export interface LimitTagChipProps extends ChipProps {
14+
limitsProperty: LimitsProperty;
15+
}
16+
17+
export function LimitsTagChip({ limitsProperty, onDelete, ...props }: Readonly<LimitTagChipProps>) {
18+
return (
19+
<Tooltip title={limitsProperty.name + ' : ' + limitsProperty.value}>
20+
<Chip
21+
avatar={<Avatar>{getPropertyAvatar(limitsProperty.name)}</Avatar>}
22+
label={limitsProperty.value}
23+
sx={{ maxWidth: onDelete ? '200px' : '180px', margin: 0.5, borderRadius: '4px' }}
24+
onDelete={onDelete}
25+
{...props}
26+
/>
27+
</Tooltip>
28+
);
29+
}

0 commit comments

Comments
 (0)