Skip to content

Commit d9fd02c

Browse files
authored
Sync study navigation (#3192)
Signed-off-by: Ayoub LABIDI <[email protected]>
1 parent b1416e1 commit d9fd02c

16 files changed

+365
-33
lines changed

src/components/app-top-bar.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import { RunButtonContainer } from './run-button-container';
2323
import { useComputationResultsCount } from '../hooks/use-computation-results-count';
2424
import { useParameterState } from './dialogs/parameters/use-parameters-state';
2525
import { STUDY_VIEWS, StudyView } from './utils/utils';
26+
import StudyNavigationSyncToggle from './study-navigation-sync-toggle';
2627

2728
const styles = {
28-
boxContent: (theme) => ({ display: 'flex', width: '100%', marginLeft: theme.spacing(4) }),
29+
boxContent: (theme) => ({ display: 'flex', overflow: 'hidden', width: '100%', marginLeft: theme.spacing(4) }),
2930
tabs: {},
3031
searchButton: {
3132
marginTop: 'auto',
@@ -38,7 +39,12 @@ const styles = {
3839
marginBottom: 'auto',
3940
marginRight: '10%',
4041
marginLeft: 'auto',
41-
flexShrink: 0,
42+
},
43+
syncToggleContainer: {
44+
marginTop: 'auto',
45+
marginBottom: 'auto',
46+
marginLeft: -4,
47+
marginRight: 1,
4248
},
4349
};
4450

@@ -126,6 +132,9 @@ const AppTopBar = ({ user, onChangeTab, userManager }) => {
126132
disabled={!isNodeBuilt(currentNode) || isNodeReadOnly(currentNode)}
127133
/>
128134
</Box>
135+
<Box sx={styles.syncToggleContainer}>
136+
<StudyNavigationSyncToggle />
137+
</Box>
129138
</Box>
130139
)}
131140
</TopBar>

src/components/app.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
mapColumnsDto,
7373
processSpreadsheetsCollectionData,
7474
} from './spreadsheet-view/add-spreadsheet/dialogs/add-spreadsheet-utils';
75+
import useStudyNavigationSync from 'hooks/use-study-navigation-sync';
7576

7677
const noUserManager = { instance: null, error: null };
7778

@@ -156,6 +157,8 @@ const App = () => {
156157
listenerCallbackMessage: updateConfig,
157158
});
158159

160+
useStudyNavigationSync();
161+
159162
const networkVisuParamsUpdated = useCallback(
160163
(event) => {
161164
const eventData = JSON.parse(event.data);

src/components/breadcrumbs/root-network-select.tsx

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

8-
import { useDispatch } from 'react-redux';
98
import { Box, ListItemText, MenuItem, Select, Theme } from '@mui/material';
10-
import { setCurrentRootNetworkUuid } from '../../redux/actions';
119
import { UUID } from 'crypto';
1210
import { RemoveRedEye, VisibilityOff } from '@mui/icons-material';
1311
import { RootNetworkMetadata } from '../graph/menus/network-modifications/network-modification-menu.type';
1412
import { useMemo } from 'react';
13+
import { useSyncNavigationActions } from 'hooks/use-sync-navigation-actions';
1514

1615
const styles = {
1716
selectRoot: (theme: Theme) => ({
@@ -30,7 +29,7 @@ interface RootNetworkSelectProps {
3029
}
3130

3231
export default function RootNetworkSelect({ currentRootNetworkUuid, rootNetworks }: Readonly<RootNetworkSelectProps>) {
33-
const dispatch = useDispatch();
32+
const { setCurrentRootNetworkUuidWithSync } = useSyncNavigationActions();
3433

3534
const filteredRootNetworks = useMemo(() => {
3635
return rootNetworks.filter((item) => item.rootNetworkUuid !== currentRootNetworkUuid);
@@ -42,7 +41,9 @@ export default function RootNetworkSelect({ currentRootNetworkUuid, rootNetworks
4241
id="breadCrumbsSelect"
4342
sx={styles.selectRoot}
4443
value={currentRootNetworkUuid}
45-
onChange={(event) => dispatch(setCurrentRootNetworkUuid(event.target.value as UUID))}
44+
onChange={(event) => {
45+
setCurrentRootNetworkUuidWithSync(event.target.value as UUID);
46+
}}
4647
renderValue={(value) => {
4748
const tag = rootNetworks.find((item) => item.rootNetworkUuid === value)?.tag;
4849
return (

src/components/graph/menus/root-network/root-network-node-editor.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ import {
2828

2929
import { SetStateAction, useCallback, useEffect, useState } from 'react';
3030
import { FormattedMessage } from 'react-intl';
31-
import { useDispatch, useSelector } from 'react-redux';
31+
import { useSelector } from 'react-redux';
3232

3333
import { UUID } from 'crypto';
3434
import { AppState } from 'redux/reducer';
3535
import { RootNetworkInfos, RootNetworkMetadata } from '../network-modifications/network-modification-menu.type';
3636
import { getCaseImportParameters } from 'services/network-conversion';
3737
import { deleteRootNetworks, updateRootNetwork } from 'services/root-network';
38-
import { setCurrentRootNetworkUuid } from 'redux/actions';
3938
import { isChecked, isPartial } from '../network-modifications/network-modification-node-editor-utils';
4039
import RootNetworkDialog, { FormData } from 'components/dialogs/root-network/root-network-dialog';
4140
import { customizeCurrentParameters, formatCaseImportParameters } from '../../util/case-import-parameters';
41+
import { useSyncNavigationActions } from 'hooks/use-sync-navigation-actions';
4242

4343
const styles = {
4444
checkboxListItem: (theme: Theme) => ({
@@ -126,10 +126,10 @@ const RootNetworkNodeEditor: React.FC<RootNetworkNodeEditorProps> = ({
126126

127127
const currentNode = useSelector((state: AppState) => state.currentTreeNode);
128128
const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid);
129+
const { setCurrentRootNetworkUuidWithSync } = useSyncNavigationActions();
129130

130131
const [rootNetworkModificationDialogOpen, setRootNetworkModificationDialogOpen] = useState(false);
131132
const [editedRootNetwork, setEditedRootNetwork] = useState<RootNetworkMetadata | undefined>(undefined);
132-
const dispatch = useDispatch();
133133

134134
const doDeleteRootNetwork = useCallback(() => {
135135
const selectedRootNetworksUuid = selectedItems.map((item) => item.rootNetworkUuid);
@@ -157,7 +157,7 @@ const RootNetworkNodeEditor: React.FC<RootNetworkNodeEditorProps> = ({
157157
return (
158158
<IconButton
159159
onClick={() => {
160-
dispatch(setCurrentRootNetworkUuid(rootNetwork.rootNetworkUuid));
160+
setCurrentRootNetworkUuidWithSync(rootNetwork.rootNetworkUuid);
161161
}}
162162
disabled={rootNetwork.isCreating || isRootNetworksProcessing}
163163
>
@@ -171,7 +171,7 @@ const RootNetworkNodeEditor: React.FC<RootNetworkNodeEditorProps> = ({
171171
</IconButton>
172172
);
173173
},
174-
[currentRootNetworkUuid, dispatch, isRootNetworksProcessing]
174+
[currentRootNetworkUuid, isRootNetworksProcessing, setCurrentRootNetworkUuidWithSync]
175175
);
176176

177177
useEffect(() => {

src/components/graph/menus/root-network/use-root-network-notifications.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import { useDispatch, useSelector } from 'react-redux';
1010
import { AppState } from 'redux/reducer';
1111
import { NotificationsUrlKeys, useNotificationsListener, useSnackMessage } from '@gridsuite/commons-ui';
1212
import { fetchRootNetworks } from 'services/root-network';
13-
import { setCurrentRootNetworkUuid, setMonoRootStudy, setRootNetworks } from 'redux/actions';
13+
import { setMonoRootStudy, setRootNetworks } from 'redux/actions';
1414
import { RootNetworkMetadata } from '../network-modifications/network-modification-menu.type';
1515
import {
1616
isRootNetworkDeletionStartedNotification,
1717
isRootNetworksUpdatedNotification,
1818
isRootNetworkUpdateFailedNotification,
1919
} from 'types/notification-types';
20+
import { useSyncNavigationActions } from 'hooks/use-sync-navigation-actions';
2021

2122
type UseRootNetworkNotificationsProps = {
2223
setIsRootNetworksProcessing: React.Dispatch<SetStateAction<boolean>>;
@@ -28,6 +29,7 @@ export const useRootNetworkNotifications = ({ setIsRootNetworksProcessing }: Use
2829
const studyUuid = useSelector((state: AppState) => state.studyUuid);
2930
const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid);
3031
const rootNetworks = useSelector((state: AppState) => state.rootNetworks);
32+
const { setCurrentRootNetworkUuidWithSync } = useSyncNavigationActions();
3133

3234
const doFetchRootNetworks = useCallback(() => {
3335
if (studyUuid) {
@@ -90,11 +92,11 @@ export const useRootNetworkNotifications = ({ setIsRootNetworksProcessing }: Use
9092
(rootNetwork) => !deletedRootNetworksUuids.includes(rootNetwork.rootNetworkUuid)
9193
);
9294
if (newSelectedRootNetwork) {
93-
dispatch(setCurrentRootNetworkUuid(newSelectedRootNetwork.rootNetworkUuid));
95+
setCurrentRootNetworkUuidWithSync(newSelectedRootNetwork.rootNetworkUuid);
9496
}
9597
}
9698
},
97-
[currentRootNetworkUuid, dispatch, rootNetworks]
99+
[currentRootNetworkUuid, rootNetworks, setCurrentRootNetworkUuidWithSync]
98100
);
99101

100102
useNotificationsListener(NotificationsUrlKeys.STUDY, {

src/components/network-modification-tree.jsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@ import { Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, useReactFlo
1010
import MapIcon from '@mui/icons-material/Map';
1111
import CenterFocusIcon from '@mui/icons-material/CenterFocusStrong';
1212
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
13-
import {
14-
reorderNetworkModificationTreeNodes,
15-
setCurrentTreeNode,
16-
setModificationsDrawerOpen,
17-
setToggleOptions,
18-
} from '../redux/actions';
13+
import { reorderNetworkModificationTreeNodes, setModificationsDrawerOpen, setToggleOptions } from '../redux/actions';
1914
import { useDispatch, useSelector } from 'react-redux';
2015
import { isSameNode } from './graph/util/model-functions';
2116
import PropTypes from 'prop-types';
@@ -36,6 +31,8 @@ import { updateNodesColumnPositions } from '../services/study/tree-subtree.ts';
3631
import { useSnackMessage } from '@gridsuite/commons-ui';
3732
import { groupIdSuffix } from './graph/nodes/labeled-group-node.type';
3833
import { StudyDisplayMode } from './network-modification.type';
34+
import { useSyncNavigationActions } from 'hooks/use-sync-navigation-actions';
35+
import { NodeType } from './graph/tree-node.type';
3936

4037
const styles = (theme) => ({
4138
flexGrow: 1,
@@ -57,6 +54,7 @@ const NetworkModificationTree = ({ onNodeContextMenu, studyUuid, onTreePanelResi
5754
const { snackError } = useSnackMessage();
5855

5956
const currentNode = useSelector((state) => state.currentTreeNode);
57+
const { setCurrentTreeNodeWithSync } = useSyncNavigationActions();
6058

6159
const treeModel = useSelector((state) => state.networkModificationTreeModel);
6260

@@ -125,24 +123,26 @@ const NetworkModificationTree = ({ onNodeContextMenu, studyUuid, onTreePanelResi
125123
);
126124
}, []);
127125

126+
// close modifications/ event scenario when current node is root
127+
useEffect(() => {
128+
if (currentNode?.type === NodeType.ROOT) {
129+
const newOptions = handleRootNodeClick(toggleOptions);
130+
if (newOptions !== toggleOptions) {
131+
dispatch(setToggleOptions(newOptions));
132+
}
133+
}
134+
}, [currentNode, dispatch, handleRootNodeClick, toggleOptions]);
135+
128136
const onNodeClick = useCallback(
129137
(event, node) => {
130-
if (node.type === 'NETWORK_MODIFICATION') {
138+
if (node.type === NodeType.NETWORK_MODIFICATION) {
131139
dispatch(setModificationsDrawerOpen());
132140
}
133-
if (node.type === 'ROOT') {
134-
handleRootNodeClick(toggleOptions, dispatch);
135-
136-
const newOptions = handleRootNodeClick(toggleOptions);
137-
if (newOptions !== toggleOptions) {
138-
dispatch(setToggleOptions(newOptions));
139-
}
140-
}
141141
if (!isSameNode(currentNode, node)) {
142-
dispatch(setCurrentTreeNode(node));
142+
setCurrentTreeNodeWithSync(node);
143143
}
144144
},
145-
[currentNode, dispatch, handleRootNodeClick, toggleOptions]
145+
[currentNode, dispatch, setCurrentTreeNodeWithSync]
146146
);
147147

148148
const toggleMinimap = useCallback(() => {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 { useDispatch, useSelector } from 'react-redux';
9+
import { ToggleButton, Tooltip } from '@mui/material';
10+
import { Sync, SyncDisabled } from '@mui/icons-material';
11+
import { selectSyncEnabled } from '../redux/actions';
12+
import { AppState } from 'redux/reducer';
13+
import { useIntl } from 'react-intl';
14+
import { useStudyScopedNavigationKeys } from 'hooks/use-study-scoped-navigation-keys';
15+
16+
const StudyNavigationSyncToggle = () => {
17+
const dispatch = useDispatch();
18+
const syncEnabled = useSelector((state: AppState) => state.syncEnabled);
19+
const currentTreeNode = useSelector((state: AppState) => state.currentTreeNode);
20+
const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid);
21+
const intl = useIntl();
22+
23+
const STORAGE_KEYS = useStudyScopedNavigationKeys();
24+
25+
const handleToggle = () => {
26+
const newValue = !syncEnabled;
27+
dispatch(selectSyncEnabled(newValue));
28+
try {
29+
localStorage.setItem(STORAGE_KEYS.SYNC_ENABLED, JSON.stringify(newValue));
30+
if (newValue) {
31+
// Save current values when enabling sync for other tabs
32+
localStorage.setItem(STORAGE_KEYS.TREE_NODE, JSON.stringify(currentTreeNode));
33+
localStorage.setItem(STORAGE_KEYS.ROOT_NETWORK_UUID, JSON.stringify(currentRootNetworkUuid));
34+
}
35+
} catch (err) {
36+
console.warn('Failed to toggle sync:', err);
37+
}
38+
};
39+
40+
return (
41+
<Tooltip
42+
title={
43+
syncEnabled
44+
? intl.formatMessage({ id: 'disableNavigationSync' })
45+
: intl.formatMessage({ id: 'enableNavigationSync' })
46+
}
47+
>
48+
<ToggleButton value={'sync'} selected={syncEnabled} onChange={handleToggle} size="small">
49+
{syncEnabled ? <Sync /> : <SyncDisabled />}
50+
</ToggleButton>
51+
</Tooltip>
52+
);
53+
};
54+
55+
export default StudyNavigationSyncToggle;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 const BASE_KEYS = {
9+
SYNC_ENABLED: 'SYNC_ENABLED',
10+
ROOT_NETWORK_UUID: 'ROOT_NETWORK_UUID',
11+
TREE_NODE: 'TREE_NODE',
12+
};

0 commit comments

Comments
 (0)