Skip to content

Commit ce0e79a

Browse files
Visualize composite modifications (#538)
Signed-off-by: Mathieu DEHARBE <[email protected]>
1 parent fe600d3 commit ce0e79a

File tree

8 files changed

+259
-5
lines changed

8 files changed

+259
-5
lines changed

src/components/app-wrapper.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import {
4444
LIGHT_THEME,
4545
loginEn,
4646
loginFr,
47+
networkModificationsEn,
48+
networkModificationsFr,
4749
multipleSelectionDialogEn,
4850
multipleSelectionDialogFr,
4951
SnackbarProvider,
@@ -190,6 +192,7 @@ function getMuiTheme(theme: GsTheme, locale: GsLangUser) {
190192
const messages: Record<GsLangUser, IntlConfig['messages']> = {
191193
en: {
192194
...messages_en,
195+
...networkModificationsEn,
193196
...importParamsEn,
194197
...exportParamsEn,
195198
...loginEn,
@@ -212,6 +215,7 @@ const messages: Record<GsLangUser, IntlConfig['messages']> = {
212215
},
213216
fr: {
214217
...messages_fr,
218+
...networkModificationsFr,
215219
...importParamsFr,
216220
...exportParamsFr,
217221
...loginFr,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Copyright (c) 2024, 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 { SyntheticEvent, useEffect, useState } from 'react';
8+
import { useDispatch, useSelector } from 'react-redux';
9+
import Box from '@mui/material/Box';
10+
import Divider from '@mui/material/Divider';
11+
import { useForm } from 'react-hook-form';
12+
import { useIntl } from 'react-intl';
13+
import { List, ListItem } from '@mui/material';
14+
import {
15+
CustomMuiDialog,
16+
FieldConstants,
17+
NetworkModificationMetadata,
18+
NO_SELECTION_FOR_COPY,
19+
unscrollableDialogStyles,
20+
useModificationLabelComputer,
21+
useSnackMessage,
22+
yupConfig as yup,
23+
} from '@gridsuite/commons-ui';
24+
import { yupResolver } from '@hookform/resolvers/yup';
25+
import { AppState } from '../../../../redux/types';
26+
import { useParameterState } from '../../use-parameters-dialog';
27+
import { PARAM_LANGUAGE } from '../../../../utils/config-params';
28+
import { fetchCompositeModificationContent, saveCompositeModification } from '../../../../utils/rest-api';
29+
import CompositeModificationForm from './composite-modification-form';
30+
import { setSelectionForCopy } from '../../../../redux/actions';
31+
32+
const schema = yup.object().shape({
33+
[FieldConstants.NAME]: yup.string().trim().required('nameEmpty'),
34+
});
35+
36+
const emptyFormData = (name?: string) => ({
37+
[FieldConstants.NAME]: name,
38+
});
39+
40+
interface FormData {
41+
[FieldConstants.NAME]: string;
42+
}
43+
44+
interface CompositeModificationDialogProps {
45+
compositeModificationId: string;
46+
open: boolean;
47+
onClose: (event?: SyntheticEvent) => void;
48+
titleId: string;
49+
name: string;
50+
broadcastChannel: BroadcastChannel;
51+
}
52+
53+
export default function CompositeModificationDialog({
54+
compositeModificationId,
55+
open,
56+
onClose,
57+
titleId,
58+
name,
59+
broadcastChannel,
60+
}: Readonly<CompositeModificationDialogProps>) {
61+
const intl = useIntl();
62+
const [languageLocal] = useParameterState(PARAM_LANGUAGE);
63+
const [isFetching, setIsFetching] = useState(!!compositeModificationId);
64+
const { snackError } = useSnackMessage();
65+
const selectionForCopy = useSelector((state: AppState) => state.selectionForCopy);
66+
const [modifications, setModifications] = useState<NetworkModificationMetadata[]>([]);
67+
const dispatch = useDispatch();
68+
69+
const methods = useForm<FormData>({
70+
defaultValues: emptyFormData(name),
71+
resolver: yupResolver(schema),
72+
});
73+
74+
const { computeLabel } = useModificationLabelComputer();
75+
const getModificationLabel = (modif: NetworkModificationMetadata) => {
76+
if (!modif) {
77+
return null;
78+
}
79+
const labelData = {
80+
...modif,
81+
...computeLabel(modif),
82+
};
83+
return intl.formatMessage({ id: `network_modifications.${modif.type}` }, labelData);
84+
};
85+
86+
const generateNetworkModificationsList = () => {
87+
return (
88+
<List sx={unscrollableDialogStyles.scrollableContent}>
89+
{modifications &&
90+
modifications.map((modification: NetworkModificationMetadata) => (
91+
<Box key={modification.uuid}>
92+
<ListItem>
93+
<Box>{getModificationLabel(modification)}</Box>
94+
</ListItem>
95+
<Divider component="li" />
96+
</Box>
97+
))}
98+
</List>
99+
);
100+
};
101+
102+
useEffect(() => {
103+
setIsFetching(true);
104+
fetchCompositeModificationContent(compositeModificationId)
105+
.then((response) => {
106+
if (response) {
107+
setModifications(response);
108+
}
109+
})
110+
.catch((error) => {
111+
snackError({
112+
messageTxt: error.message,
113+
headerId: 'retrieveCompositeModificationError',
114+
});
115+
})
116+
.finally(() => setIsFetching(false));
117+
}, [compositeModificationId, name, snackError]);
118+
119+
const closeAndClear = (event?: SyntheticEvent) => {
120+
onClose(event);
121+
};
122+
123+
const onSubmit = (formData: FormData) => {
124+
saveCompositeModification(compositeModificationId, formData[FieldConstants.NAME])
125+
.then(() => {
126+
if (selectionForCopy.sourceItemUuid === compositeModificationId) {
127+
dispatch(setSelectionForCopy(NO_SELECTION_FOR_COPY));
128+
broadcastChannel.postMessage({
129+
NO_SELECTION_FOR_COPY,
130+
});
131+
}
132+
closeAndClear();
133+
})
134+
.catch((errorMessage) => {
135+
snackError({
136+
messageTxt: errorMessage,
137+
headerId: 'compositeModificationEditingError',
138+
headerValues: { name },
139+
});
140+
});
141+
};
142+
143+
return (
144+
<CustomMuiDialog
145+
open={open}
146+
onClose={closeAndClear}
147+
titleId={titleId}
148+
onSave={onSubmit}
149+
removeOptional
150+
isDataFetching={isFetching}
151+
language={languageLocal}
152+
formSchema={schema}
153+
formMethods={methods}
154+
unscrollableFullHeight
155+
>
156+
{!isFetching && (
157+
<Box sx={unscrollableDialogStyles.unscrollableContainer}>
158+
<CompositeModificationForm />
159+
{generateNetworkModificationsList()}
160+
</Box>
161+
)}
162+
</CustomMuiDialog>
163+
);
164+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) 2024, 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 { UniqueNameInput, ElementType, FieldConstants } from '@gridsuite/commons-ui';
9+
import { elementExists } from 'utils/rest-api';
10+
import { useSelector } from 'react-redux';
11+
import { AppState } from 'redux/types';
12+
import Box from '@mui/material/Box';
13+
14+
export default function CompositeModificationForm() {
15+
const activeDirectory = useSelector((state: AppState) => state.activeDirectory);
16+
return (
17+
<Box>
18+
<UniqueNameInput
19+
name={FieldConstants.NAME}
20+
label="nameProperty"
21+
elementType={ElementType.MODIFICATION}
22+
activeDirectory={activeDirectory}
23+
elementExists={elementExists}
24+
/>
25+
</Box>
26+
);
27+
}

src/components/directory-content.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import {
2424
import { Add as AddIcon } from '@mui/icons-material';
2525
import { AgGridReact } from 'ag-grid-react';
2626
import { SelectionForCopy } from '@gridsuite/commons-ui/dist/components/filter/filter.type';
27-
import { setActiveDirectory, setSelectionForCopy } from '../redux/actions';
27+
import { ContingencyListType, FilterType, NetworkModificationType } from '../utils/elementType';
2828
import * as constants from '../utils/UIconstants';
29-
import { ContingencyListType, FilterType } from '../utils/elementType';
29+
import { setActiveDirectory, setSelectionForCopy } from '../redux/actions';
3030
import { elementExists, getFilterById, updateElement } from '../utils/rest-api';
3131
import ContentContextualMenu from './menus/content-contextual-menu';
3232
import ContentToolbar from './toolbars/content-toolbar';
@@ -47,6 +47,7 @@ import NoContentDirectory from './no-content-directory';
4747
import { CUSTOM_ROW_CLASS, DirectoryContentTable } from './directory-content-table';
4848
import { useHighlightSearchedElement } from './search/use-highlight-searched-element';
4949
import EmptyDirectory from './empty-directory';
50+
import CompositeModificationDialog from './dialogs/network-modification/composite-modification/composite-modification-dialog';
5051
import { AppState } from '../redux/types';
5152

5253
const circularProgressSize = '70px';
@@ -193,7 +194,17 @@ export default function DirectoryContent() {
193194
setElementName('');
194195
};
195196

196-
/** Filters dialog: window status value to edit Expert filters */
197+
const [currentNetworkModificationId, setCurrentNetworkModificationId] = useState(null);
198+
const handleCloseCompositeModificationDialog = () => {
199+
setOpenDialog(constants.DialogsId.NONE);
200+
setCurrentNetworkModificationId(null);
201+
setActiveElement(null);
202+
setElementName('');
203+
};
204+
205+
/**
206+
* Filters dialog: window status value to edit Expert filters
207+
*/
197208
const [currentExpertFilterId, setCurrentExpertFilterId] = useState(null);
198209
const handleCloseExpertFilterDialog = () => {
199210
setOpenDialog(constants.DialogsId.NONE);
@@ -375,6 +386,12 @@ export default function DirectoryContent() {
375386
setOpenDialog(subtype);
376387
}
377388
break;
389+
case ElementType.MODIFICATION:
390+
if (subtype === NetworkModificationType.COMPOSITE.id) {
391+
setCurrentNetworkModificationId(event.data.elementUuid);
392+
setOpenDialog(subtype);
393+
}
394+
break;
378395
default:
379396
break;
380397
}
@@ -496,6 +513,17 @@ export default function DirectoryContent() {
496513
// TODO openDialog should also be aware of the dialog's type, not only its subtype, because
497514
// if/when two different dialogs have the same subtype, this function will display the wrong dialog.
498515
switch (openDialog) {
516+
case NetworkModificationType.COMPOSITE.id:
517+
return (
518+
<CompositeModificationDialog
519+
open
520+
titleId="MODIFICATION"
521+
compositeModificationId={currentNetworkModificationId ?? ''}
522+
onClose={handleCloseCompositeModificationDialog}
523+
name={name}
524+
broadcastChannel={broadcastChannel}
525+
/>
526+
);
499527
case ContingencyListType.CRITERIA_BASED.id:
500528
return (
501529
<CriteriaBasedEditionDialog

src/translations/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"CSVFileCommentContingencyList4": "# N-2 composite,ID_line1|ID_generator1",
9191
"contingencyListCreationError": "An error occurred while creating the contingency list: {name}",
9292
"contingencyListEditingError": "An error occurred while editing the contingency list: {name}",
93+
"compositeModificationEditingError": "An error occurred while editing the composite modification: {name}",
9394
"contingencyListCreation": "contingencyListCreation",
9495
"equipmentID": "Equipment ID",
9596
"equipments": "Equipments",
@@ -112,6 +113,7 @@
112113
"Min": "Min",
113114
"Max": "Max",
114115
"cannotRetrieveContingencyList": "Could not retrieve contingency list: ",
116+
"retrieveCompositeModificationError": "Could not retrieve composite modification content: ",
115117
"AddDescription": "Add a description (optional)",
116118
"PropertyName": "Property name",
117119
"getAppLinkError": "Error getting application link for type = {type}",

src/translations/fr.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
"edit": "Modifier",
7171
"createNewContingencyList": "Créer une liste d'aléas",
7272
"FORM": "Formulaire",
73-
"editContingencyList": "Editer la liste d'aléas",
74-
"editFilter": "Editer le filtre",
73+
"editContingencyList": "Éditer la liste d'aléas",
74+
"editFilter": "Éditer le filtre",
7575
"STUDY": "Étude",
7676
"SPREADSHEET_CONFIG": "Modèle de tableur",
7777
"VOLTAGE_INIT_PARAMETERS": "Paramètres (Initialisation du plan de tension)",
@@ -89,6 +89,7 @@
8989
"CSVFileCommentContingencyList4": "# N-2 mixte,ID_ligne1|ID_groupe1",
9090
"contingencyListCreationError": "Une erreur est survenue lors de la création de la liste d'aléas : {name}",
9191
"contingencyListEditingError": "Une erreur est survenue lors de l'édition de la liste d'aléas : {name}",
92+
"compositeModificationEditingError": "Une erreur est survenue lors de l'édition de la modification composite : {name}",
9293
"contingencyListCreation": "creationListeAleas",
9394
"equipmentID": "ID d'ouvrage",
9495
"equipments": "Ouvrages",
@@ -111,6 +112,7 @@
111112
"Min": "Min",
112113
"Max": "Max",
113114
"cannotRetrieveContingencyList": "Erreur d'accès à la liste d'aléas : ",
115+
"retrieveCompositeModificationError": "Erreur d'accès au contenu de la modification composite : ",
114116
"AddDescription": "Ajouter une description (optionnel)",
115117
"PropertyName": "Nom de la propriété",
116118
"getAppLinkError": "Erreur lors de la récupération du lien vers l'application pour le type = {type}",

src/utils/elementType.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export const FilterType = {
1111
EXPERT: { id: 'EXPERT', label: 'filter.expert' },
1212
};
1313

14+
export const NetworkModificationType = {
15+
COMPOSITE: { id: 'COMPOSITE_MODIFICATION', label: 'MODIFICATION' },
16+
};
17+
1418
export const ContingencyListType = {
1519
CRITERIA_BASED: { id: 'FORM', label: 'contingencyList.criteriaBased' },
1620
EXPLICIT_NAMING: {

src/utils/rest-api.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,29 @@ export interface CriteriaBasedEditionFormData {
514514
[FieldConstants.CRITERIA_BASED]?: CriteriaBasedData;
515515
}
516516

517+
/**
518+
* Get the basic data of the network modifications contained in a composite modification
519+
*/
520+
export function fetchCompositeModificationContent(id: string) {
521+
const url: string = `${PREFIX_EXPLORE_SERVER_QUERIES}/v1/explore/composite-modification/${id}/network-modifications`;
522+
523+
return backendFetchJson(url, {
524+
method: 'get',
525+
});
526+
}
527+
528+
export function saveCompositeModification(id: string, name: string) {
529+
const urlSearchParams = new URLSearchParams();
530+
urlSearchParams.append('name', name);
531+
532+
const url: string = `${PREFIX_EXPLORE_SERVER_QUERIES}/v1/explore/composite-modification/${id}?${urlSearchParams.toString()}`;
533+
534+
return backendFetch(url, {
535+
method: 'put',
536+
headers: { 'Content-Type': 'application/json' },
537+
});
538+
}
539+
517540
/**
518541
* Saves a Filter contingency list
519542
* @returns {Promise<Response>}

0 commit comments

Comments
 (0)