Skip to content

Commit da88f3a

Browse files
add branch type
1 parent b26db07 commit da88f3a

File tree

9 files changed

+125
-82
lines changed

9 files changed

+125
-82
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright © 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+

src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-equipment-modification.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type UseEquipmentModificationProps = {
2525

2626
type EditableEquipmentType = Exclude<
2727
SpreadsheetEquipmentType,
28+
| SpreadsheetEquipmentType.BRANCH
2829
| SpreadsheetEquipmentType.BUS
2930
| SpreadsheetEquipmentType.BUSBAR_SECTION
3031
| SpreadsheetEquipmentType.DANGLING_LINE

src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-spreadsheet-equipments.ts

Lines changed: 74 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {
1616
resetEquipments,
1717
resetEquipmentsByTypes,
1818
updateEquipments,
19+
type UpdateEquipmentsAction,
1920
} from 'redux/actions';
2021
import { type AppState } from 'redux/reducer';
21-
import { SpreadsheetEquipmentType } from '../../../types/spreadsheet.type';
22+
import { isSpreadsheetEquipmentType, SpreadsheetEquipmentType } from '../../../types/spreadsheet.type';
2223
import { fetchAllEquipments } from 'services/study/network-map';
2324
import type { NodeAlias } from '../../../types/node-alias.type';
2425
import { isStatusBuilt } from '../../../../graph/util/model-functions';
@@ -27,6 +28,7 @@ import { type DeletedEquipment, isStudyNotification, type NetworkImpactsInfos }
2728
import { NodeType } from '../../../../graph/tree-node.type';
2829
import { validAlias } from '../../../hooks/use-node-aliases';
2930
import { fetchNetworkElementInfos } from 'services/study/network';
31+
import { EQUIPMENT_INFOS_TYPES } from '../../../../utils/equipment-types';
3032

3133
export const useSpreadsheetEquipments = (
3234
type: SpreadsheetEquipmentType,
@@ -45,10 +47,8 @@ export const useSpreadsheetEquipments = (
4547
const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid);
4648
const currentNode = useSelector((state: AppState) => state.currentTreeNode);
4749
const treeNodes = useSelector((state: AppState) => state.networkModificationTreeModel?.treeNodes);
48-
const [builtAliasedNodesIds, setBuiltAliasedNodesIds] = useState<UUID[]>();
49-
50+
const [builtAliasedNodesIds, setBuiltAliasedNodesIds] = useState<UUID[]>([]);
5051
const [isFetching, setIsFetching] = useState<boolean>(false);
51-
5252
const { fetchNodesEquipmentData } = useFetchEquipment(type);
5353

5454
// effect to keep builtAliasedNodesIds up-to-date (when we add/remove an alias or build/unbuild an aliased node)
@@ -61,25 +61,20 @@ export const useSpreadsheetEquipments = (
6161
.filter((nodeAlias) => validAlias(nodeAlias))
6262
.map((nodeAlias) => nodeAlias.id);
6363
if (aliasedNodesIds.length > 0) {
64-
treeNodes?.forEach((treeNode) => {
65-
if (
66-
aliasedNodesIds.includes(treeNode.id) &&
67-
(treeNode.type === NodeType.ROOT || isStatusBuilt(treeNode.data.globalBuildStatus))
68-
) {
69-
computedIds.push(treeNode.id);
70-
}
71-
});
64+
computedIds =
65+
treeNodes
66+
?.filter(
67+
(treeNode) =>
68+
aliasedNodesIds.includes(treeNode.id) &&
69+
(treeNode.type === NodeType.ROOT || isStatusBuilt(treeNode.data.globalBuildStatus))
70+
)
71+
.map((treeNode) => treeNode.id) ?? [];
7272
}
73+
computedIds.sort((a, b) => a.localeCompare(b));
7374
// Because of treeNodes: update the state only on real values changes (to avoid multiple effects for the watchers)
74-
setBuiltAliasedNodesIds((prevState) => {
75-
const currentIds = prevState;
76-
currentIds?.sort((a, b) => a.localeCompare(b));
77-
computedIds.sort((a, b) => a.localeCompare(b));
78-
if (JSON.stringify(currentIds) !== JSON.stringify(computedIds)) {
79-
return computedIds;
80-
}
81-
return prevState;
82-
});
75+
setBuiltAliasedNodesIds((prevState) =>
76+
JSON.stringify(prevState) !== JSON.stringify(computedIds) ? computedIds : prevState
77+
);
8378
}, [nodeAliases, treeNodes]);
8479

8580
const nodesIdToFetch = useMemo(() => {
@@ -92,17 +87,17 @@ export const useSpreadsheetEquipments = (
9287
nodesIdToFetch.add(currentNode.id);
9388
}
9489
// Then we do the same for the other nodes we need the data of (the ones defined in aliases)
95-
builtAliasedNodesIds.forEach((builtAliasNodeId) => {
90+
for (const builtAliasNodeId of builtAliasedNodesIds) {
9691
if (equipments.nodesId.find((nodeId) => nodeId === builtAliasNodeId) === undefined) {
9792
nodesIdToFetch.add(builtAliasNodeId);
9893
}
99-
});
94+
}
10095
return nodesIdToFetch;
10196
}, [currentNode?.id, equipments.nodesId, builtAliasedNodesIds]);
10297

10398
// effect to unload equipment data when we remove an alias or unbuild an aliased node
10499
useEffect(() => {
105-
if (!equipments || !builtAliasedNodesIds || !currentNode?.id) {
100+
if (!equipments || !builtAliasedNodesIds.length || !currentNode?.id) {
106101
return;
107102
}
108103
const currentNodeId = currentNode.id;
@@ -121,7 +116,7 @@ export const useSpreadsheetEquipments = (
121116
return;
122117
}
123118
// updating data related to impacted elements
124-
const nodeId = currentNode?.id as UUID;
119+
const nodeId = currentNode?.id as UUID; //TODO maybe do nothing if no current node?
125120

126121
// Handle updates and resets based on impacted element types
127122
if (impactedElementTypes.length > 0) {
@@ -133,7 +128,9 @@ export const useSpreadsheetEquipments = (
133128
Object.keys(allEquipments).includes(type)
134129
);
135130
if (impactedSpreadsheetEquipmentsTypes.length > 0) {
136-
dispatch(resetEquipmentsByTypes(impactedSpreadsheetEquipmentsTypes as SpreadsheetEquipmentType[]));
131+
dispatch(
132+
resetEquipmentsByTypes(impactedSpreadsheetEquipmentsTypes.filter(isSpreadsheetEquipmentType))
133+
);
137134
}
138135
}
139136

@@ -154,17 +151,51 @@ export const useSpreadsheetEquipments = (
154151
);
155152
} else {
156153
// here, we can fetch only the data for the modified equipment
157-
fetchNetworkElementInfos(
158-
studyUuid,
159-
nodeId,
160-
currentRootNetworkUuid,
161-
type,
162-
'TAB',
163-
equipmentToUpdateId,
164-
false
165-
).then((value: Identifiable) => {
166-
highlightUpdatedEquipment();
167-
dispatch(updateEquipments({ [type]: [value] }, nodeId));
154+
const promises = [
155+
fetchNetworkElementInfos(
156+
studyUuid,
157+
nodeId,
158+
currentRootNetworkUuid,
159+
type,
160+
EQUIPMENT_INFOS_TYPES.TAB.type,
161+
equipmentToUpdateId,
162+
false
163+
),
164+
];
165+
if (
166+
type === SpreadsheetEquipmentType.LINE ||
167+
type === SpreadsheetEquipmentType.TWO_WINDINGS_TRANSFORMER
168+
) {
169+
promises.push(
170+
fetchNetworkElementInfos(
171+
studyUuid,
172+
nodeId,
173+
currentRootNetworkUuid,
174+
SpreadsheetEquipmentType.BRANCH,
175+
EQUIPMENT_INFOS_TYPES.TAB.type,
176+
equipmentToUpdateId,
177+
false
178+
)
179+
);
180+
}
181+
Promise.allSettled(promises).then((results) => {
182+
const updates: UpdateEquipmentsAction['equipments'] = {};
183+
if (results[0].status === 'rejected') {
184+
//TODO show snackbar error?
185+
} else {
186+
updates[type] = results[0].value;
187+
}
188+
if (results.length > 1) {
189+
if (results[1].status === 'rejected') {
190+
//TODO show snackbar error?
191+
} else {
192+
updates[SpreadsheetEquipmentType.BRANCH] = results[1].value;
193+
}
194+
}
195+
if (Object.keys(updates).length > 1) {
196+
highlightUpdatedEquipment();
197+
dispatch(updateEquipments(updates, nodeId));
198+
}
168199
});
169200
}
170201
}
@@ -183,11 +214,12 @@ export const useSpreadsheetEquipments = (
183214
});
184215

185216
if (equipmentsToDelete.length > 0) {
186-
const equipmentsToDeleteArray: EquipmentToDelete[] = equipmentsToDelete.map((equipment) => ({
187-
equipmentType: equipment.equipmentType as SpreadsheetEquipmentType,
188-
equipmentId: equipment.equipmentId,
189-
nodeId: nodeId,
190-
}));
217+
const equipmentsToDeleteArray = equipmentsToDelete
218+
.filter((e) => isSpreadsheetEquipmentType(e.equipmentType))
219+
.map<EquipmentToDelete>((equipment) => ({
220+
equipmentType: equipment.equipmentType as unknown as SpreadsheetEquipmentType,
221+
equipmentId: equipment.equipmentId,
222+
}));
191223
dispatch(deleteEquipments(equipmentsToDeleteArray, nodeId));
192224
}
193225
}

src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/save/save-spreadsheet-button.tsx

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
77

8-
import { useState, MouseEvent, useCallback, useMemo, RefObject } from 'react';
8+
import { type MouseEvent, type RefObject, useCallback, useMemo, useState } from 'react';
99
import { Button, Menu, MenuItem } from '@mui/material';
1010
import { FormattedMessage } from 'react-intl';
1111
import SaveIcon from '@mui/icons-material/Save';
1212
import SaveSpreadsheetDialog from './save-spreadsheet-dialog';
13-
import { useCsvExport, useStateBoolean, FILTER_EQUIPMENTS } from '@gridsuite/commons-ui';
13+
import { EquipmentType, FILTER_EQUIPMENTS, useCsvExport, useStateBoolean } from '@gridsuite/commons-ui';
1414
import { SaveSpreadsheetCollectionDialog } from './save-spreadsheet-collection-dialog';
15-
import { NodeAlias } from '../../../types/node-alias.type';
15+
import type { NodeAlias } from '../../../types/node-alias.type';
1616
import { ROW_INDEX_COLUMN_ID } from '../../../constants';
17-
import { SpreadsheetTabDefinition } from '../../../types/spreadsheet.type';
18-
import { AgGridReact } from 'ag-grid-react';
19-
import { ColDef } from 'ag-grid-community';
17+
import { SpreadsheetEquipmentType, type SpreadsheetTabDefinition } from '../../../types/spreadsheet.type';
18+
import type { AgGridReact } from 'ag-grid-react';
19+
import type { ColDef } from 'ag-grid-community';
2020
import { spreadsheetStyles } from '../../../spreadsheet.style';
2121
import { useSelector } from 'react-redux';
22-
import { AppState } from '../../../../../redux/reducer';
22+
import type { AppState } from '../../../../../redux/reducer';
2323
import SaveNamingFilterDialog from './save-naming-filter-dialog';
2424

2525
enum SpreadsheetSaveOptionId {
@@ -60,15 +60,10 @@ export default function SaveSpreadsheetButton({
6060
const { downloadCSVData } = useCsvExport();
6161
const language = useSelector((state: AppState) => state.computedLanguage);
6262

63-
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
64-
setAnchorEl(event.currentTarget);
65-
}, []);
63+
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget), []);
64+
const handleClose = useCallback(() => setAnchorEl(null), []);
6665

67-
const handleClose = useCallback(() => {
68-
setAnchorEl(null);
69-
}, []);
70-
71-
const spreadsheetOptions = useMemo(
66+
const spreadsheetOptions = useMemo<Record<SpreadsheetSaveOptionId, SpreadsheetSaveOption>>(
7267
() => ({
7368
[SpreadsheetSaveOptionId.SAVE_MODEL]: {
7469
id: SpreadsheetSaveOptionId.SAVE_MODEL,
@@ -84,16 +79,14 @@ export default function SaveSpreadsheetButton({
8479
id: SpreadsheetSaveOptionId.EXPORT_CSV,
8580
label: 'spreadsheet/save/options/csv',
8681
action: () => {
87-
// Filter out the rowIndex column before exporting to CSV
88-
const columnsForExport = columns.filter((col) => col.colId !== ROW_INDEX_COLUMN_ID);
89-
9082
const exportDataAsCsv = gridRef.current?.api.exportDataAsCsv;
9183
if (!exportDataAsCsv) {
9284
console.error('Export API is not available.');
9385
return;
9486
}
9587
downloadCSVData({
96-
columns: columnsForExport,
88+
// Filter out the rowIndex column before exporting to CSV
89+
columns: columns.filter((col) => col.colId !== ROW_INDEX_COLUMN_ID),
9790
tableName: tableDefinition.name,
9891
language: language,
9992
exportDataAsCsv,
@@ -105,7 +98,12 @@ export default function SaveSpreadsheetButton({
10598
id: SpreadsheetSaveOptionId.SAVE_FILTER,
10699
label: 'spreadsheet/save/options/filter',
107100
action: saveFilterDialogOpen.setTrue,
108-
disabled: dataSize === 0 || !FILTER_EQUIPMENTS[tableDefinition.type],
101+
disabled:
102+
dataSize === 0 ||
103+
(tableDefinition.type === SpreadsheetEquipmentType.BRANCH
104+
? !FILTER_EQUIPMENTS[EquipmentType.LINE] ||
105+
!FILTER_EQUIPMENTS[EquipmentType.TWO_WINDINGS_TRANSFORMER]
106+
: !FILTER_EQUIPMENTS[tableDefinition.type as unknown as EquipmentType]),
109107
},
110108
}),
111109
[
@@ -130,25 +128,22 @@ export default function SaveSpreadsheetButton({
130128
[spreadsheetOptions, handleClose]
131129
);
132130

133-
const renderMenuItem = useCallback(
134-
(option: SpreadsheetSaveOption) => {
135-
return (
136-
<MenuItem key={option.id} onClick={() => handleMenuItemClick(option.id)} disabled={option?.disabled}>
137-
<FormattedMessage id={option.label} />
138-
</MenuItem>
139-
);
140-
},
141-
[handleMenuItemClick]
142-
);
143-
144131
return (
145132
<>
146133
<Button sx={spreadsheetStyles.spreadsheetButton} size={'small'} onClick={handleClick} disabled={disabled}>
147134
<SaveIcon />
148135
<FormattedMessage id="spreadsheet/save/button" />
149136
</Button>
150137
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
151-
{Object.values(spreadsheetOptions).map(renderMenuItem)}
138+
{Object.values(spreadsheetOptions).map((option) => (
139+
<MenuItem
140+
key={option.id}
141+
onClick={() => handleMenuItemClick(option.id)}
142+
disabled={option?.disabled}
143+
>
144+
<FormattedMessage id={option.label} />
145+
</MenuItem>
146+
))}
152147
</Menu>
153148
<SaveSpreadsheetDialog
154149
tableDefinition={tableDefinition}

src/components/spreadsheet-view/types/spreadsheet.type.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { GlobalFilter } from '../../results/common/global-filter/global-fil
1212

1313
export enum SpreadsheetEquipmentType {
1414
BATTERY = 'BATTERY',
15+
BRANCH = 'BRANCH', // LINE + TWO_WINDINGS_TRANSFORMER
1516
BUS = 'BUS',
1617
BUSBAR_SECTION = 'BUSBAR_SECTION',
1718
DANGLING_LINE = 'DANGLING_LINE',
@@ -30,6 +31,10 @@ export enum SpreadsheetEquipmentType {
3031
VSC_CONVERTER_STATION = 'VSC_CONVERTER_STATION',
3132
}
3233

34+
export function isSpreadsheetEquipmentType(type: string): type is SpreadsheetEquipmentType {
35+
return type in SpreadsheetEquipmentType;
36+
}
37+
3338
export interface SpreadsheetTabDefinition {
3439
uuid: UUID;
3540
index: number;

src/redux/reducer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ import {
259259
} from '../utils/store-sort-filter-fields';
260260
import type { UUID } from 'crypto';
261261
import type { GlobalFilter } from '../components/results/common/global-filter/global-filter-types';
262-
import type { ValueOf } from 'type-fest';
262+
import type { Entries, ValueOf } from 'type-fest';
263263
import { CopyType, StudyDisplayMode } from '../components/network-modification.type';
264264
import { CurrentTreeNode, NetworkModificationNodeData, RootNodeData } from '../components/graph/tree-node.type';
265265
import { COMPUTING_AND_NETWORK_MODIFICATION_TYPE } from '../utils/report/report.constant';
@@ -519,6 +519,7 @@ const emptySpreadsheetEquipmentsByNodes: SpreadsheetEquipmentsByNodes = {
519519
export type SpreadsheetNetworkState = Record<SpreadsheetEquipmentType, SpreadsheetEquipmentsByNodes>;
520520
const initialSpreadsheetNetworkState: SpreadsheetNetworkState = {
521521
[SpreadsheetEquipmentType.BATTERY]: emptySpreadsheetEquipmentsByNodes,
522+
[SpreadsheetEquipmentType.BRANCH]: emptySpreadsheetEquipmentsByNodes,
522523
[SpreadsheetEquipmentType.BUS]: emptySpreadsheetEquipmentsByNodes,
523524
[SpreadsheetEquipmentType.BUSBAR_SECTION]: emptySpreadsheetEquipmentsByNodes,
524525
[SpreadsheetEquipmentType.DANGLING_LINE]: emptySpreadsheetEquipmentsByNodes,
@@ -1252,7 +1253,11 @@ export const reducer = createReducer(initialState, (builder) => {
12521253
});
12531254

12541255
builder.addCase(LOAD_EQUIPMENTS, (state, action: LoadEquipmentsAction) => {
1255-
Object.entries(action.spreadsheetEquipmentByNodes.equipmentsByNodeId).forEach(([nodeId, equipments]) => {
1256+
(
1257+
Object.entries(action.spreadsheetEquipmentByNodes.equipmentsByNodeId) as Entries<
1258+
LoadEquipmentsAction['spreadsheetEquipmentByNodes']['equipmentsByNodeId']
1259+
>
1260+
).forEach(([nodeId, equipments]) => {
12561261
state.spreadsheetNetwork[action.equipmentType].equipmentsByNodeId[nodeId] = equipments;
12571262
});
12581263
//to remove duplicate

src/translations/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@
557557
"Type": "Type",
558558
"SUBSTATION": "Substation",
559559
"VOLTAGE_LEVEL": "Voltage Level",
560+
"BRANCH": "Branch",
560561
"LINE": "Line",
561562
"TIE_LINE": "Tie line",
562563
"SWITCH": "Switch",

src/translations/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@
554554
"Type": "Type",
555555
"SUBSTATION": "Site",
556556
"VOLTAGE_LEVEL": "Poste",
557+
"BRANCH": "Quadripôle",
557558
"LINE": "Ligne",
558559
"TIE_LINE": "Ligne d'interconnexion",
559560
"SWITCH": "disjoncteur",

0 commit comments

Comments
 (0)