diff --git a/src/components/grid-layout/dialog/map-dialog.tsx b/src/components/grid-layout/dialog/map-dialog.tsx index 40a8c88aa8..359a31489e 100644 --- a/src/components/grid-layout/dialog/map-dialog.tsx +++ b/src/components/grid-layout/dialog/map-dialog.tsx @@ -10,7 +10,7 @@ import type { UUID } from 'node:crypto'; import { EquipmentType, LineFlowMode, NetworkVisualizationParameters, useStateBoolean } from '@gridsuite/commons-ui'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'redux/reducer'; -import { resetMapEquipment, setMapDataLoading, setOpenMap, setReloadMapNeeded } from 'redux/actions'; +import { resetMapEquipment, setMapDataLoading, setMapState, setOpenMap, setReloadMapNeeded } from 'redux/actions'; import NetworkMapPanel, { NetworkMapPanelRef } from 'components/network/network-map-panel'; import { Close } from '@mui/icons-material'; import { FormattedMessage } from 'react-intl'; @@ -61,6 +61,12 @@ export const MapDialog = (props: MapDialogProps) => { return; // Do not close the map but only the drawing mode } } + if (networkMapPanelRef.current) { + const currentMapState = networkMapPanelRef.current.getCurrentMapState?.(); + if (currentMapState) { + dispatch(setMapState(currentMapState)); + } + } dispatch(setOpenMap(false)); dispatch(resetMapEquipment()); dispatch(setMapDataLoading(false)); diff --git a/src/components/network/network-map-panel.tsx b/src/components/network/network-map-panel.tsx index e25d9692a7..5e82d864da 100644 --- a/src/components/network/network-map-panel.tsx +++ b/src/components/network/network-map-panel.tsx @@ -5,7 +5,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import type { Writable } from 'type-fest'; import { type Coordinate, DRAW_EVENT, @@ -45,7 +44,13 @@ import { type UseStateBooleanReturn, } from '@gridsuite/commons-ui'; import { isNodeBuilt, isNodeEdited, isSameNodeAndBuilt } from '../graph/util/model-functions'; -import { openDiagram, resetMapEquipment, setMapDataLoading, setReloadMapNeeded } from '../../redux/actions'; +import { + openDiagram, + resetMapEquipment, + setMapDataLoading, + setMapState, + setReloadMapNeeded, +} from '../../redux/actions'; import GSMapEquipments from './gs-map-equipments'; import { Box, Button, LinearProgress, Tooltip, useTheme } from '@mui/material'; import { EQUIPMENT_TYPES } from '../utils/equipment-types'; @@ -57,7 +62,7 @@ import RunningStatus from 'components/utils/running-status'; import { useGetStudyImpacts } from 'hooks/use-get-study-impacts'; import { ROOT_NODE_LABEL } from '../../constants/node.constant'; import type { UUID } from 'node:crypto'; -import { AppState } from 'redux/reducer'; +import { AppState, MapState } from 'redux/reducer'; import { isReactFlowRootNodeData } from 'redux/utils'; import { isLoadflowResultNotification, isRootNetworksUpdatedNotification } from 'types/notification-types'; import { CurrentTreeNode } from 'components/graph/tree-node.type'; @@ -70,6 +75,7 @@ import SelectionCreationPanel from './selection-creation-panel/selection-creatio import { useEquipmentMenu } from '../../hooks/use-equipment-menu'; import useEquipmentDialogs from 'hooks/use-equipment-dialogs'; import { getNominalVoltageColor } from 'utils/colors'; +import { getNominalVoltageIntervalName } from './utils/nominal-voltage-filter-utils'; const INITIAL_POSITION = [0, 0] as const; const INITIAL_ZOOM = 9; @@ -133,6 +139,7 @@ type NetworkMapPanelProps = { export type NetworkMapPanelRef = { leaveDrawingMode: () => void; + getCurrentMapState: () => MapState; }; export const NetworkMapPanel = forwardRef( @@ -167,6 +174,7 @@ export const NetworkMapPanel = forwardRef state.isNetworkModificationTreeModelUpToDate ); + const mapState = useSelector((state: AppState) => state.mapState); const theme = useTheme(); const { snackInfo } = useSnackMessage(); @@ -183,7 +191,8 @@ export const NetworkMapPanel = forwardRef(); + const [filteredNominalVoltages, setFilteredNominalVoltages] = useState([]); + const [hasInitializedFilters, setHasInitializedFilters] = useState(false); const [geoData, setGeoData] = useState(); const geoDataRef = useRef(); @@ -546,10 +555,15 @@ export const NetworkMapPanel = forwardRef((newValues) => { - setFilteredNominalVoltages(newValues); - setNominalVoltages(newValues); - }, []); + const handleFilteredNominalVoltagesChange = useCallback( + (newValues) => { + setFilteredNominalVoltages(newValues); + setNominalVoltages(newValues); + // Store filters in Redux immediately + dispatch(setMapState({ filteredNominalVoltages: newValues })); + }, + [dispatch] + ); // loads all root node geo-data then saves them in redux // it will be considered as the source of truth to check whether we need to fetch geo-data for a specific equipment or not const loadRootNodeGeoData = useCallback(() => { @@ -704,13 +718,21 @@ export const NetworkMapPanel = forwardRef { - if (isFullReload) { + if (isFullReload && !mapState?.filteredNominalVoltages) { + // Only reset filters if no saved state exists handleFilteredNominalVoltagesChange(mapEquipments.getNominalVoltages()); } } ); }, - [currentNode, handleFilteredNominalVoltagesChange, currentRootNetworkUuid, mapEquipments, studyUuid] + [ + currentNode, + handleFilteredNominalVoltagesChange, + currentRootNetworkUuid, + mapEquipments, + studyUuid, + mapState?.filteredNominalVoltages, + ] ); const updateMapEquipments = useCallback( @@ -850,7 +872,13 @@ export const NetworkMapPanel = forwardRef { + const currentMapState = networkMapRef.current?.getCurrentViewState(); + + const center: [number, number] = [ + currentMapState?.center.lng ?? INITIAL_POSITION[0], + currentMapState?.center.lat ?? INITIAL_POSITION[1], + ]; + + return { + zoom: currentMapState?.zoom ?? INITIAL_ZOOM, + center, + filteredNominalVoltages: filteredNominalVoltages, + }; + }, [filteredNominalVoltages]); + useImperativeHandle( ref, () => ({ leaveDrawingMode, + getCurrentMapState, }), - [leaveDrawingMode] + [leaveDrawingMode, getCurrentMapState] ); const handleDrawingModeChange = useCallback( @@ -1121,8 +1180,9 @@ export const NetworkMapPanel = forwardRef} - initialZoom={INITIAL_ZOOM} + // Use saved state for initial position and zoom + initialPosition={mapState?.center} + initialZoom={mapState?.zoom} lineFullPath={lineFullPath} lineParallelPath={lineParallelPath} lineFlowMode={lineFlowMode} @@ -1175,7 +1235,7 @@ export const NetworkMapPanel = forwardRef { + // Only initialize once when mapEquipments are loaded if ( nominalVoltagesFromMapEquipments !== undefined && nominalVoltagesFromMapEquipments.length > 0 && - filteredNominalVoltages === undefined + !hasInitializedFilters ) { - handleFilteredNominalVoltagesChange(nominalVoltagesFromMapEquipments); + setHasInitializedFilters(true); + + // Check if we have saved state to restore + if (mapState?.filteredNominalVoltages && mapState.filteredNominalVoltages.length > 0) { + // Get intervals from saved voltages + const savedIntervals = new Set( + mapState.filteredNominalVoltages + .map((v) => getNominalVoltageIntervalName(v)) + .filter((interval): interval is string => interval !== undefined) + ); + + // Filter current voltages by matching intervals + const voltagesMatchingIntervals = nominalVoltagesFromMapEquipments.filter((v) => { + const interval = getNominalVoltageIntervalName(v); + return interval && savedIntervals.has(interval); + }); + + if (voltagesMatchingIntervals.length > 0) { + // Restore voltages from matching intervals + setFilteredNominalVoltages(voltagesMatchingIntervals); + } else { + // No matching intervals found, initialize with all + setFilteredNominalVoltages(nominalVoltagesFromMapEquipments); + } + } else { + // No saved state, initialize with all voltages + setFilteredNominalVoltages(nominalVoltagesFromMapEquipments); + } } - }, [filteredNominalVoltages, handleFilteredNominalVoltagesChange, nominalVoltagesFromMapEquipments]); + }, [nominalVoltagesFromMapEquipments, mapState?.filteredNominalVoltages, hasInitializedFilters]); function renderNominalVoltageFilter() { return ( diff --git a/src/components/network/nominal-voltage-filter.tsx b/src/components/network/nominal-voltage-filter.tsx index bb00dafc13..4d75dfe82b 100644 --- a/src/components/network/nominal-voltage-filter.tsx +++ b/src/components/network/nominal-voltage-filter.tsx @@ -64,17 +64,17 @@ export default function NominalVoltageFilter({ const vlListValues = nominalVoltages.filter( (vnom) => getNominalVoltageIntervalName(vnom) === interval.name ); - return { ...interval, vlListValues, isChecked: true }; + // Check if all voltages in this interval are present in filteredNominalVoltages + const isChecked = vlListValues.length > 0 && vlListValues.every((v) => filteredNominalVoltages.includes(v)); + return { ...interval, vlListValues, isChecked }; }); setVoltageLevelIntervals(newIntervals); - }, [nominalVoltages]); + }, [nominalVoltages, filteredNominalVoltages]); const handleToggle = useCallback( (interval: VoltageLevelValuesInterval) => { - let newFiltered: number[]; - - // we "inverse" the selection for vlListValues - newFiltered = [...filteredNominalVoltages]; + // Toggle all voltages in this interval + const newFiltered = [...filteredNominalVoltages]; for (const vnom of interval.vlListValues) { const currentIndex = newFiltered.indexOf(vnom); if (currentIndex === -1) { @@ -83,11 +83,9 @@ export default function NominalVoltageFilter({ newFiltered.splice(currentIndex, 1); // previously present, we remove it } } - setVoltageLevelIntervals((prev) => - prev.map((i) => (i.name === interval.name ? { ...i, isChecked: !i.isChecked } : i)) - ); - onChange(newFiltered); // update filteredNominalVoltages + // Update parent state - the useEffect will handle updating voltageLevelIntervals + onChange(newFiltered); }, [filteredNominalVoltages, onChange] ); diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 18433aa917..0ae567540f 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -31,6 +31,7 @@ import type { ComputingStatusParameters, DiagramGridLayoutConfig, GlobalFilterSpreadsheetState, + MapState, NodeSelectionForCopy, OneBusShortCircuitAnalysisDiagram, SpreadsheetFilterState, @@ -1550,3 +1551,15 @@ export function updateNodeAliases(nodeAliases: NodeAlias[]): UpdateNodeAliasesAc nodeAliases, }; } + +export const SET_MAP_STATE = 'SET_MAP_STATE'; +export type SetMapStateAction = Readonly> & { + mapState: Partial; +}; + +export function setMapState(mapState: Partial): SetMapStateAction { + return { + type: SET_MAP_STATE, + mapState, + }; +} diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index ba17f58ce8..c4492c03c1 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -235,6 +235,8 @@ import { type UpdateTableDefinitionAction, USE_NAME, type UseNameAction, + SetMapStateAction, + SET_MAP_STATE, } from './actions'; import { getLocalStorageComputedLanguage, @@ -560,6 +562,12 @@ export interface DiagramGridLayoutConfig { export type LogsPaginationState = Record; +export interface MapState { + zoom?: number; + center?: [number, number]; + filteredNominalVoltages?: number[]; +} + export interface AppState extends CommonStoreState, AppConfigState { signInCallbackError: Error | null; authenticationRouterError: AuthenticationRouterErrorState | null; @@ -651,6 +659,7 @@ export interface AppState extends CommonStoreState, AppConfigState { toggleOptions: StudyDisplayMode[]; highlightedModificationUuid: UUID | null; mapOpen: boolean; + mapState: MapState; } export type LogsFilterState = Record; @@ -776,6 +785,7 @@ const initialState: AppState = { freezeMapUpdates: false, isMapEquipmentsInitialized: false, networkAreaDiagramDepth: 0, + mapState: {}, spreadsheetNetwork: { ...initialSpreadsheetNetworkState }, globalFilterSpreadsheetState: {}, spreadsheetOptionalLoadingParameters: { @@ -1977,6 +1987,13 @@ export const reducer = createReducer(initialState, (builder) => { builder.addCase(UPDATE_NODE_ALIASES, (state, action: UpdateNodeAliasesAction) => { state.nodeAliases = action.nodeAliases; }); + + builder.addCase(SET_MAP_STATE, (state, action: SetMapStateAction) => { + state.mapState = { + ...state.mapState, + ...action.mapState, + }; + }); }); function updateSubstationAfterVLDeletion(