diff --git a/src/components/grid-layout/cards/diagrams/diagram.type.ts b/src/components/grid-layout/cards/diagrams/diagram.type.ts index c967c01af9..db6df10391 100644 --- a/src/components/grid-layout/cards/diagrams/diagram.type.ts +++ b/src/components/grid-layout/cards/diagrams/diagram.type.ts @@ -28,19 +28,6 @@ export type SubstationDiagramParams = DiagramBaseParams & { type: DiagramType.SUBSTATION; substationId: string; }; -export type NetworkAreaDiagramParams = DiagramBaseParams & { - type: DiagramType.NETWORK_AREA_DIAGRAM; - nadConfigUuid: UUID | undefined; - filterUuid: UUID | undefined; - currentFilterUuid?: UUID; - voltageLevelIds: string[]; - voltageLevelToExpandIds: string[]; - voltageLevelToOmitIds: string[]; - positions: DiagramConfigPosition[]; -}; - -export type DiagramParams = VoltageLevelDiagramParams | SubstationDiagramParams | NetworkAreaDiagramParams; - // diagrams model export type DiagramBase = { type: DiagramType; @@ -57,12 +44,12 @@ export type SubstationDiagram = DiagramBase & { }; export type NetworkAreaDiagram = DiagramBase & { type: DiagramType.NETWORK_AREA_DIAGRAM; + svg: DiagramSvg | null; title?: string; nadConfigUuid: UUID | undefined; filterUuid: UUID | undefined; currentFilterUuid: UUID | undefined; currentNadConfigUuid?: UUID; - initialVoltageLevelIds: string[]; voltageLevelIds: string[]; voltageLevelToExpandIds: string[]; voltageLevelToOmitIds: string[]; @@ -87,7 +74,7 @@ export interface SldSvg { } export interface VoltageLevel { - id?: string; + id: string; substationId: UUID; country?: string; name?: string; diff --git a/src/components/grid-layout/cards/diagrams/networkAreaDiagram/diagram-controls.tsx b/src/components/grid-layout/cards/diagrams/networkAreaDiagram/diagram-controls.tsx index 62512028a8..07dbc1a19f 100644 --- a/src/components/grid-layout/cards/diagrams/networkAreaDiagram/diagram-controls.tsx +++ b/src/components/grid-layout/cards/diagrams/networkAreaDiagram/diagram-controls.tsx @@ -24,11 +24,10 @@ import { } from '@gridsuite/commons-ui'; import IconButton from '@mui/material/IconButton'; import UploadIcon from '@mui/icons-material/Upload'; -import Button from '@mui/material/Button'; import SaveIcon from '@mui/icons-material/Save'; import SearchIcon from '@mui/icons-material/Search'; import AddLocationAltOutlinedIcon from '@mui/icons-material/AddLocationAltOutlined'; -import { Tooltip } from '@mui/material'; +import { FormControlLabel, Switch, type Theme, Tooltip } from '@mui/material'; import { AppState } from 'redux/reducer.type'; import { FormattedMessage, useIntl } from 'react-intl'; import type { UUID } from 'node:crypto'; @@ -38,13 +37,16 @@ import { fetchNetworkElementInfos } from 'services/study/network'; import { EQUIPMENT_INFOS_TYPES } from 'components/utils/equipment-types'; import VoltageLevelSearchMenu from './voltage-level-search-menu'; +const getControlsBackgroundColor = (theme: Theme) => + theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.background.default; + const styles = { actionIcon: (theme) => ({ width: theme.spacing(3), height: theme.spacing(3), }), panel: (theme) => ({ - backgroundColor: theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.background.default, + backgroundColor: getControlsBackgroundColor(theme), borderRadius: theme.spacing(1), padding: theme.spacing(0.5), display: 'block', @@ -52,20 +54,23 @@ const styles = { top: theme.spacing(1), left: theme.spacing(1), }), - buttonPanel: (theme) => ({ - borderRadius: theme.spacing(1), - padding: theme.spacing(0.5), - display: 'block', - position: 'absolute', - top: '5px', - right: '5px', - }), icon: { fontSize: 'medium', }, - button: { - minWidth: 'auto', - }, + + editModeSwitch: (theme) => ({ + position: 'absolute', + top: theme.spacing(1), + right: theme.spacing(1), + height: theme.spacing(4), + margin: 0, + paddingLeft: theme.spacing(1), + borderRadius: theme.spacing(1), + backgroundColor: getControlsBackgroundColor(theme), + '& .MuiFormControlLabel-label': { + fontSize: theme.typography.body2.fontSize, + }, + }), divider: (theme) => ({ borderColor: theme.palette.grey[600], margin: '2px 4px', @@ -77,7 +82,7 @@ interface DiagramControlsProps { onUpdate?: (data: IElementUpdateDialog) => void; onLoad?: (elementUuid: UUID, elementType: ElementType, elementName: string) => void; isEditNadMode: boolean; - onToggleEditNadMode?: (isEditMode: boolean) => void; + onToggleEditNadMode?: () => void; onExpandAllVoltageLevels?: () => void; onAddVoltageLevel: (vlId: string) => void; onAddVoltageLevelsFromFilter: (elementUuid: UUID) => void; @@ -185,7 +190,7 @@ const DiagramControls: React.FC = ({ }; const handleToggleEditMode = () => { - onToggleEditNadMode?.(!isEditNadMode); + onToggleEditNadMode?.(); }; const handleVoltageLevelSelect = useCallback( @@ -321,11 +326,12 @@ const DiagramControls: React.FC = ({ )} - - - + } + control={} + /> {studyUuid && ( <> {isSaveDialogOpen && ( diff --git a/src/components/grid-layout/cards/diagrams/networkAreaDiagram/network-area-diagram-content.tsx b/src/components/grid-layout/cards/diagrams/networkAreaDiagram/network-area-diagram-content.tsx index 4bd6c1081d..78b11ee6bf 100644 --- a/src/components/grid-layout/cards/diagrams/networkAreaDiagram/network-area-diagram-content.tsx +++ b/src/components/grid-layout/cards/diagrams/networkAreaDiagram/network-area-diagram-content.tsx @@ -55,7 +55,9 @@ import { EQUIPMENT_INFOS_TYPES } from 'components/utils/equipment-types'; import GenericEquipmentPopover from 'components/tooltips/generic-equipment-popover'; import { GenericEquipmentInfos } from 'components/tooltips/equipment-popover-type'; import { GenericPopoverContent } from 'components/tooltips/generic-popover-content'; -import { selectActiveWorkspaceId } from 'redux/slices/workspace-selectors'; +import { selectActiveWorkspaceId, selectPanelEditMode } from 'redux/slices/workspace-selectors'; +import type { RootState } from 'redux/store'; +import { useWorkspacePanelActions } from 'components/workspace/hooks/use-workspace-panel-actions'; import { getLocalStoragePanelState, saveLocalStoragePanelState } from 'redux/session-storage/workspace-local-storage'; import { PanelType } from 'components/workspace/types/workspace.types'; import { DiagramAdditionalMetadata } from '../diagram.type'; @@ -86,7 +88,6 @@ type NetworkAreaDiagramContentProps = { readonly onMoveNode: (voltageLevelId: string, x: number, y: number) => void; readonly onMoveTextNode: (voltageLevelId: string, shiftX: number, shiftY: number) => void; readonly onReplaceNad: (name: string, nadConfigUuid?: UUID, filterUuid?: UUID) => void; - readonly onSaveNad?: () => void; }; const NetworkAreaDiagramContent = memo(function NetworkAreaDiagramContent(props: NetworkAreaDiagramContentProps) { @@ -112,7 +113,6 @@ const NetworkAreaDiagramContent = memo(function NetworkAreaDiagramContent(props: loadingState, isNadCreationFromFilter, showInSpreadsheet, - onSaveNad, } = props; const svgRef = useRef(null); const { snackError, snackInfo } = useSnackMessage(); @@ -128,7 +128,8 @@ const NetworkAreaDiagramContent = memo(function NetworkAreaDiagramContent(props: const [shouldDisplayMenu, setShouldDisplayMenu] = useState(false); const currentNode = useSelector((state: AppState) => state.currentTreeNode); const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); - const [isEditNadMode, setIsEditNadMode] = useState(false); + const isEditNadMode = useSelector((state: RootState) => selectPanelEditMode(state, nadPanelId)); + const { togglePanelEditMode } = useWorkspacePanelActions(); const workspaceId = useSelector(selectActiveWorkspaceId); // Workaround for https://github.com/react/react/issues/35187 and https://github.com/react/react/issues/35034: @@ -163,15 +164,9 @@ const NetworkAreaDiagramContent = memo(function NetworkAreaDiagramContent(props: diagramViewerRef.current.enableDragInteraction = isEditNadMode; } - // save nad when exiting edit mode - const handleSetIsEditNadMode = useCallback( - (newMode: boolean) => { - if (isEditNadMode && !newMode) { - onSaveNad?.(); - } - setIsEditNadMode(newMode); - }, - [isEditNadMode, onSaveNad] + const handleToggleEditNadMode = useCallback( + () => togglePanelEditMode({ panelId: nadPanelId }), + [nadPanelId, togglePanelEditMode] ); const handleToggleHover: OnToggleNadHoverCallbackType = useEffectEvent( @@ -600,7 +595,7 @@ const NetworkAreaDiagramContent = memo(function NetworkAreaDiagramContent(props: onUpdate={handleUpdateNadConfig} onLoad={handleReplaceNadConfig} isEditNadMode={isEditNadMode} - onToggleEditNadMode={handleSetIsEditNadMode} + onToggleEditNadMode={handleToggleEditNadMode} onExpandAllVoltageLevels={handleExpandAllVoltageLevels} onAddVoltageLevel={handleAddVoltageLevel} onAddVoltageLevelsFromFilter={handleAddVoltageLevelsFromFilter} diff --git a/src/components/study-container.jsx b/src/components/study-container.jsx index c88fb04b06..feb887d26e 100644 --- a/src/components/study-container.jsx +++ b/src/components/study-container.jsx @@ -26,6 +26,7 @@ import { getWorkspacesMetadata, getWorkspace } from '../services/study/workspace import { getLocalStorageActiveWorkspaceId, getLocalStoragePanelStates, + normalizeWorkspacePanels, } from '../redux/session-storage/workspace-local-storage'; import WaitingLoader from './utils/waiting-loader'; @@ -524,6 +525,7 @@ export function StudyContainer() { .then((workspace) => { if (workspace) { const savedPanels = getLocalStoragePanelStates(studyUuid, workspace.id); + workspace.panels = normalizeWorkspacePanels(workspace.panels); workspace.panels.forEach((panel, index) => { panel.zIndex = savedPanels[panel.id]?.zIndex ?? index + 1; }); diff --git a/src/components/workspace/core/panel-header.tsx b/src/components/workspace/core/panel-header.tsx index 7db090e5a0..90ea664661 100644 --- a/src/components/workspace/core/panel-header.tsx +++ b/src/components/workspace/core/panel-header.tsx @@ -20,35 +20,30 @@ import type { AppState } from '../../../redux/reducer.type'; import { SldAssociationButton } from './sld-association-button'; import { setDirtyComputationParameters } from 'redux/actions'; import { SelectOptionsDialog } from 'utils/dialogs'; +import { getPanelBorder } from './utils/panel-border'; +import { selectPanelEditMode } from '../../../redux/slices/workspace-selectors'; +import type { RootState } from '../../../redux/store'; -const getHeaderStyles = (theme: Theme, isFocused: boolean, maximized: boolean) => { - let backgroundColor: string; - let border: string; - if (theme.palette.mode === 'light') { - backgroundColor = isFocused ? theme.palette.grey[200] : 'white'; - border = `1px solid ${theme.palette.grey[500]}`; - } else { - backgroundColor = '#292e33'; - border = - isFocused && !maximized ? `1px solid ${theme.palette.grey[100]}` : `1px solid ${theme.palette.grey[800]}`; +const getHeaderBackground = (theme: Theme, isFocused: boolean) => { + if (theme.palette.mode !== 'light') { + return '#292e33'; } - - return { - paddingLeft: theme.spacing(1), - display: 'flex', - alignItems: 'center', - backgroundColor, - border, - borderRadius: theme.spacing(2) + ' ' + theme.spacing(2) + ' 0 0', - borderBottom: 'none', - cursor: 'grab', - userSelect: 'none', - '&:active': { - cursor: 'grabbing', - }, - }; + return isFocused ? theme.palette.grey[200] : 'white'; }; +const getHeaderStyles = (theme: Theme, isFocused: boolean, maximized: boolean, isEditing: boolean) => ({ + paddingLeft: theme.spacing(1), + display: 'flex', + alignItems: 'center', + backgroundColor: getHeaderBackground(theme, isFocused), + borderBottom: getPanelBorder(theme, isFocused, maximized, isEditing), + cursor: 'grab', + userSelect: 'none', + '&:active': { + cursor: 'grabbing', + }, +}); + const styles = { title: { display: 'flex', @@ -99,6 +94,7 @@ export const PanelHeader = memo(({ panelId, title, panelType, pinned, maximized, const { deletePanel, minimizePanel, maximizePanel, pinPanel } = useWorkspacePanelActions(); const displayTitle = intl.messages[title] ? intl.formatMessage({ id: title }) : title || ''; const isDirtyComputationParameters = useSelector((state: AppState) => state.isDirtyComputationParameters); + const isEditing = useSelector((state: RootState) => selectPanelEditMode(state, panelId)); const [isConfirmCloseOpen, setIsConfirmCloseOpen] = useState(false); const handleClose = () => { @@ -126,7 +122,7 @@ export const PanelHeader = memo(({ panelId, title, panelType, pinned, maximized, }, []); return ( - getHeaderStyles(theme, isFocused, maximized)}> + getHeaderStyles(theme, isFocused, maximized, isEditing)}> {getPanelConfig(panelType).icon} diff --git a/src/components/workspace/core/panel.tsx b/src/components/workspace/core/panel.tsx index b045b2f1c2..3cf2a27385 100644 --- a/src/components/workspace/core/panel.tsx +++ b/src/components/workspace/core/panel.tsx @@ -9,7 +9,7 @@ import { memo, useCallback } from 'react'; import { Box, Theme } from '@mui/material'; import { Rnd, type RndDragCallback, type RndResizeCallback } from 'react-rnd'; import { useSelector } from 'react-redux'; -import { selectPanel } from '../../../redux/slices/workspace-selectors'; +import { selectPanel, selectPanelEditMode } from '../../../redux/slices/workspace-selectors'; import { useWorkspacePanelActions } from '../hooks/use-workspace-panel-actions'; import type { RootState } from '../../../redux/store'; import { PANEL_CONTENT_REGISTRY } from '../panel-contents/panel-content-registry'; @@ -20,19 +20,10 @@ import type { AppState } from '../../../redux/reducer.type'; import { getSnapZone, type SnapRect } from './utils/snap-utils'; import { calculatePanelDimensions, positionToRelative, sizeToRelative } from './utils/coordinate-utils'; import PanelErrorBoundary from './panel-error-boundary'; +import { getPanelBorder } from './utils/panel-border'; const RESIZE_HANDLE_SIZE = 12; -const getBorder = (theme: Theme, isFocused: boolean, maximized: boolean) => { - if (theme.palette.mode === 'light') { - return `1px solid ${theme.palette.grey[500]}`; - } - if (isFocused && !maximized) { - return `1px solid ${theme.palette.grey[100]}`; - } - return `1px solid ${theme.palette.grey[800]}`; -}; - const styles = { panel: { display: 'flex', @@ -54,8 +45,6 @@ const styles = { overflow: 'hidden', position: 'relative', backgroundColor: theme.palette.mode === 'light' ? theme.palette.background.paper : '#292e33', - borderRadius: '0 0 ' + theme.spacing(2) + ' ' + theme.spacing(2), - borderTop: 'none', }), resizeHandles: { bottomRight: { width: RESIZE_HANDLE_SIZE, height: RESIZE_HANDLE_SIZE, right: 0, bottom: 0 }, @@ -78,6 +67,7 @@ export const Panel = memo(({ panelId, containerRect, snapPreview, onSnapPreview, const studyUuid = useSelector((state: AppState) => state.studyUuid); const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); const currentNode = useSelector((state: AppState) => state.currentTreeNode); + const isEditing = useSelector((state: RootState) => selectPanelEditMode(state, panelId)); const handleDrag = useCallback( (e: MouseEvent) => { @@ -161,7 +151,11 @@ export const Panel = memo(({ panelId, containerRect, snapPreview, onSnapPreview, > ({ ...styles.panel, boxShadow: isFocused ? theme.shadows[18] : 'none' })} + sx={(theme) => ({ + ...styles.panel, + boxShadow: isFocused ? theme.shadows[18] : 'none', + border: getPanelBorder(theme, isFocused, panel.maximized, isEditing), + })} > - ({ - ...styles.content(theme), - border: getBorder(theme, isFocused, panel.maximized), - })} - > + {studyUuid && currentRootNetworkUuid && currentNode ? ( {PANEL_CONTENT_REGISTRY[panel.type]({ diff --git a/src/components/workspace/core/utils/panel-border.ts b/src/components/workspace/core/utils/panel-border.ts new file mode 100644 index 0000000000..0d8bbe6cb7 --- /dev/null +++ b/src/components/workspace/core/utils/panel-border.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import type { Theme } from '@mui/material'; + +export const getPanelBorder = (theme: Theme, isFocused: boolean, maximized: boolean, isEditing: boolean) => { + if (isEditing) { + return isFocused ? `2px solid ${theme.palette.primary.main}` : `1px solid ${theme.palette.primary.main}`; + } + if (theme.palette.mode === 'light') { + return `1px solid ${theme.palette.grey[500]}`; + } + if (isFocused && !maximized) { + return `1px solid ${theme.palette.grey[100]}`; + } + return `1px solid ${theme.palette.grey[800]}`; +}; diff --git a/src/components/workspace/core/workspace-switcher.tsx b/src/components/workspace/core/workspace-switcher.tsx index 8cf1f661f4..c55fda12ab 100644 --- a/src/components/workspace/core/workspace-switcher.tsx +++ b/src/components/workspace/core/workspace-switcher.tsx @@ -66,6 +66,7 @@ import { AppState } from 'redux/reducer.type'; import { getLocalStoragePanelStates, clearLocalStorageWorkspaceState, + normalizeWorkspacePanels, saveLocalStorageActiveWorkspaceId, } from '../../../redux/session-storage/workspace-local-storage'; @@ -148,6 +149,7 @@ export const WorkspaceSwitcher = memo(() => { if (!studyUuid) return; const workspace = await getWorkspace(studyUuid, workspaceId); const savedPanels = getLocalStoragePanelStates(studyUuid, workspaceId); + workspace.panels = normalizeWorkspacePanels(workspace.panels); workspace.panels.forEach((panel, index) => { panel.zIndex = savedPanels[panel.id]?.zIndex ?? index + 1; }); diff --git a/src/components/workspace/diagrams/common/use-diagram-notifications.ts b/src/components/workspace/diagrams/common/use-diagram-notifications.ts index dc7b1918f5..d8447ca53e 100644 --- a/src/components/workspace/diagrams/common/use-diagram-notifications.ts +++ b/src/components/workspace/diagrams/common/use-diagram-notifications.ts @@ -20,9 +20,9 @@ import { selectActiveWorkspaceId } from '../../../../redux/slices/workspace-sele interface UseDiagramNotificationsProps { currentRootNetworkUuid: UUID; - onNotification: (newConfigUuid?: UUID) => void; - currentNadConfigUuid?: UUID; + onNotification: () => void; panelId?: UUID; + onNadConfigUpdate?: () => void; } /** @@ -33,6 +33,7 @@ export const useDiagramNotifications = ({ currentRootNetworkUuid, onNotification, panelId, + onNadConfigUpdate, }: UseDiagramNotificationsProps) => { const workspaceId = useSelector(selectActiveWorkspaceId); @@ -58,11 +59,10 @@ export const useDiagramNotifications = ({ if (isRootNetworkNotification) { onNotification(); } else if (isMatchingNadConfigNotification) { - const newConfigUuid = eventData.payload as UUID; - onNotification(newConfigUuid); + onNadConfigUpdate?.(); } }, - [currentRootNetworkUuid, onNotification, workspaceId, panelId] + [currentRootNetworkUuid, onNotification, onNadConfigUpdate, workspaceId, panelId] ); useNotificationsListener(NotificationsUrlKeys.STUDY, { listenerCallbackMessage: handleNotification }); diff --git a/src/components/workspace/diagrams/nad/use-nad-diagram.ts b/src/components/workspace/diagrams/nad/use-nad-diagram.ts index c7fcf236e4..c9fd9e1fcc 100644 --- a/src/components/workspace/diagrams/nad/use-nad-diagram.ts +++ b/src/components/workspace/diagrams/nad/use-nad-diagram.ts @@ -8,19 +8,24 @@ import type { UUID } from 'node:crypto'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { ErrorMessageDescriptor, extractErrorMessageDescriptor, PARAM_LANGUAGE } from '@gridsuite/commons-ui'; +import { + ErrorMessageDescriptor, + extractErrorMessageDescriptor, + PARAM_LANGUAGE, + useDebounce, +} from '@gridsuite/commons-ui'; import { AppState } from '../../../../redux/reducer.type'; -import { DiagramType, NetworkAreaDiagram } from '../../../grid-layout/cards/diagrams/diagram.type'; +import { DiagramType, type DiagramSvg, NetworkAreaDiagram } from '../../../grid-layout/cards/diagrams/diagram.type'; import { fetchSvg, getNetworkAreaDiagramUrl } from '../../../../services/study'; -import { deleteNadConfig, saveNadConfig } from '../../../../services/study/workspace'; +import { getPanels, saveNadConfig } from '../../../../services/study/workspace'; import { mergePositions } from '../../../grid-layout/cards/diagrams/diagram-utils'; -import type { DiagramMetadata } from '@powsybl/network-viewer'; +import type { DiagramConfigPosition } from '../../../../services/explore'; import { useDiagramNotifications } from '../common/use-diagram-notifications'; import { isNodeBuilt } from '../../../graph/util/model-functions'; import { selectActiveWorkspaceId, selectNadDiagramFields } from '../../../../redux/slices/workspace-selectors'; import type { RootState } from '../../../../redux/store'; import { useWorkspacePanelActions } from '../../hooks/use-workspace-panel-actions'; -import { PERSISTENT_NAD_FIELDS } from '../../types/workspace.types'; +import { isNADPanel } from '../../hooks/workspace-panel-utils'; interface UseNadDiagramProps { panelId: UUID; @@ -29,33 +34,18 @@ interface UseNadDiagramProps { currentRootNetworkUuid: UUID; } -function getPersistentFieldsChanges(prev: NetworkAreaDiagram, next: NetworkAreaDiagram): Partial { - const changes: Partial = {}; - for (const field of PERSISTENT_NAD_FIELDS) { - const prevValue = prev[field]; - const nextValue = next[field]; +const NAD_CONFIG_SAVE_DEBOUNCE_MS = 700; - // Deep equality check for arrays - const hasChanged = - Array.isArray(prevValue) && Array.isArray(nextValue) - ? JSON.stringify(prevValue) !== JSON.stringify(nextValue) - : prevValue !== nextValue; +const hasStoredVoltageLevels = ( + source?: Pick +) => Boolean(source?.currentNadConfigUuid || source?.nadConfigUuid || source?.filterUuid); - if (hasChanged) { - Object.assign(changes, { [field]: nextValue }); - } - } - return changes; -} - -// Base reset state for loading new configs const BASE_RESET_STATE = { currentFilterUuid: undefined, voltageLevelIds: [], voltageLevelToExpandIds: [], positions: [], currentNadConfigUuid: undefined, - initialVoltageLevelIds: [], voltageLevelToOmitIds: [], svg: null, }; @@ -68,6 +58,9 @@ export const useNadDiagram = ({ panelId, studyUuid, currentNodeId, currentRootNe const networkVisuParams = useSelector((state: AppState) => state.networkVisualizationsParameters); const language = useSelector((state: AppState) => state[PARAM_LANGUAGE]); + const isStored = hasStoredVoltageLevels(initialFields); + const canFetchDiagram = isStored || Boolean(initialFields?.initialVoltageLevelIds?.length); + const [diagram, setDiagram] = useState(() => ({ type: DiagramType.NETWORK_AREA_DIAGRAM, svg: null, @@ -76,8 +69,7 @@ export const useNadDiagram = ({ panelId, studyUuid, currentNodeId, currentRootNe filterUuid: initialFields?.filterUuid, currentFilterUuid: initialFields?.currentFilterUuid, currentNadConfigUuid: initialFields?.currentNadConfigUuid, - initialVoltageLevelIds: initialFields?.initialVoltageLevelIds || [], - voltageLevelIds: initialFields?.initialVoltageLevelIds || [], + voltageLevelIds: isStored ? [] : initialFields?.initialVoltageLevelIds || [], voltageLevelToExpandIds: [], voltageLevelToOmitIds: initialFields?.voltageLevelToOmitIds || [], positions: [], @@ -87,97 +79,116 @@ export const useNadDiagram = ({ panelId, studyUuid, currentNodeId, currentRootNe const abortControllerRef = useRef(undefined); - const setDiagramAndSync = useCallback( - (updater: React.SetStateAction, syncToBackend = true) => { - setDiagram((prev) => { - const next = typeof updater === 'function' ? updater(prev) : updater; + // latest diagram snapshot source of truth used for async work + // it doesn't copy the svg from the state, it's a shallow object copy + const diagramRef = useRef(diagram); - const changes = getPersistentFieldsChanges(prev, next); - if (Object.keys(changes).length > 0) { - updateNADFields({ panelId, fields: changes, syncToBackend }); - } + const updateDiagram = useCallback((updates: Partial) => { + diagramRef.current = { ...diagramRef.current, ...updates }; + setDiagram(diagramRef.current); + }, []); - return next; - }); - }, - [updateNADFields, panelId] - ); + const saveNad = useCallback(() => { + if (!workspaceId) { + return; + } + const { + svg, + title, + voltageLevelIds, + positions, + voltageLevelToOmitIds, + nadConfigUuid, + filterUuid, + currentFilterUuid, + } = diagramRef.current; + + saveNadConfig(studyUuid, workspaceId, panelId, { + title, + nadConfig: { scalingFactor: svg?.additionalMetadata?.scalingFactor, voltageLevelIds, positions }, + nadConfigUuid, + filterUuid, + currentFilterUuid, + voltageLevelToOmitIds, + }) + .then((savedUuid) => updateDiagram({ currentNadConfigUuid: savedUuid ?? undefined })) + .catch((error) => console.error('Failed to save NAD config:', error)); + }, [studyUuid, workspaceId, panelId, updateDiagram]); + + const debounceSaveNad = useDebounce(saveNad, NAD_CONFIG_SAVE_DEBOUNCE_MS); - // Helper to process SVG data - extracted to reduce nesting const processSvgData = useCallback( - (svgData: any) => { + (svgData: DiagramSvg | null) => { if (!svgData) return; - const vlIdsFromSvg = - (svgData.additionalMetadata as { voltageLevels?: { id: string }[] })?.voltageLevels?.map( - (vl) => vl.id - ) ?? []; + const vlIdsFromSvg = svgData.additionalMetadata?.voltageLevels.map((vl) => vl.id) ?? []; - console.info(`Number of voltage levels for NAD '${diagram.title}' : '${vlIdsFromSvg.length}'`); + console.info(`Number of voltage levels for NAD panel '${panelId}' : '${vlIdsFromSvg.length}'`); - setDiagramAndSync((prev) => { - const filteredOmitIds = prev.voltageLevelToOmitIds.filter((id) => !vlIdsFromSvg.includes(id)); - - return { - ...prev, - svg: svgData, - voltageLevelIds: [...new Set([...prev.voltageLevelIds, ...vlIdsFromSvg])], - voltageLevelToExpandIds: [], - voltageLevelToOmitIds: filteredOmitIds, - positions: mergePositions(prev.positions, svgData.metadata as DiagramMetadata), - }; + const { voltageLevelIds, voltageLevelToOmitIds, positions } = diagramRef.current; + updateDiagram({ + svg: svgData, + voltageLevelIds: [...new Set([...voltageLevelIds, ...vlIdsFromSvg])], + voltageLevelToExpandIds: [], + voltageLevelToOmitIds: voltageLevelToOmitIds.filter((id) => !vlIdsFromSvg.includes(id)), + positions: mergePositions(positions, svgData.metadata ?? undefined), }); }, - [setDiagramAndSync, diagram.title] + [panelId, updateDiagram] ); const handleFetchError = useCallback((error: any) => { setGlobalError(extractErrorMessageDescriptor(error, '')); }, []); - const fetchDiagram = useCallback(() => { - if (!currentNode || !isNodeBuilt(currentNode)) { - // Abort any still pending fetch so its late response can't overwrite this error - abortControllerRef.current?.abort(); - setGlobalError({ descriptor: { id: 'InvalidNode' } }); - setLoading(false); - return Promise.resolve(); - } + const fetchDiagram = useCallback( + (persistAfterFetch = false) => { + if (!canFetchDiagram || !networkVisuParams) { + setLoading(true); + return; + } - // Abort any still pending fetch so its response can be ignored - abortControllerRef.current?.abort(); - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - setLoading(true); - setGlobalError(undefined); - - // we use setDiagram to capture current state without adding diagram to dependencies - return setDiagram((currentDiagram) => { - const nadConfigUuid = currentDiagram.currentNadConfigUuid || currentDiagram.nadConfigUuid; - const body: any = { - nadPositionsGenerationMode: networkVisuParams?.networkAreaDiagramParameters.nadPositionsGenerationMode, - voltageLevelIds: currentDiagram.voltageLevelIds, - voltageLevelToExpandIds: currentDiagram.voltageLevelToExpandIds, - // positions will be retreived from the saved nadConfig. - // Unsaved node drags are lost on refetch and it is fine, - // we will add confirmation dialog or a snackbar for this case in future dev - positions: nadConfigUuid ? [] : currentDiagram.positions, - voltageLevelToOmitIds: currentDiagram.voltageLevelToOmitIds, - nadConfigUuid, - filterUuid: currentDiagram.currentFilterUuid || currentDiagram.filterUuid, + if (!currentNode || !isNodeBuilt(currentNode)) { + // Abort any still pending fetch so its late response can't overwrite this error + abortControllerRef.current?.abort(); + setGlobalError({ descriptor: { id: 'InvalidNode' } }); + setLoading(false); + return; + } + + // Abort any still pending fetch so its response can be ignored + abortControllerRef.current?.abort(); + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + setLoading(true); + setGlobalError(undefined); + + const current = diagramRef.current; + const body = { + nadPositionsGenerationMode: networkVisuParams.networkAreaDiagramParameters.nadPositionsGenerationMode, + voltageLevelIds: current.voltageLevelIds, + voltageLevelToExpandIds: current.voltageLevelToExpandIds, + voltageLevelToOmitIds: current.voltageLevelToOmitIds, + nadConfigUuid: current.currentNadConfigUuid || current.nadConfigUuid, + filterUuid: current.currentFilterUuid || current.filterUuid, language, }; - const url = getNetworkAreaDiagramUrl(studyUuid, currentNodeId, currentRootNetworkUuid); - - fetchSvg(url, { + fetchSvg(getNetworkAreaDiagramUrl(studyUuid, currentNodeId, currentRootNetworkUuid), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: abortController.signal, }) - .then(processSvgData) + .then((svgData) => { + processSvgData(svgData as DiagramSvg | null); + // From a config or a filter the server rebuilds the diagram, only a panel whose + // voltage levels live nowhere else needs a config of its own + if (persistAfterFetch || !hasStoredVoltageLevels(current)) { + debounceSaveNad(); + } + }) .catch((error) => { // a newer fetchDiagram call already aborted this request, so its response is no longer relevant if (!abortController.signal.aborted) { @@ -189,181 +200,133 @@ export const useNadDiagram = ({ panelId, studyUuid, currentNodeId, currentRootNe setLoading(false); } }); - return currentDiagram; - }); - }, [ - currentNode, - language, - studyUuid, - currentNodeId, - currentRootNetworkUuid, - networkVisuParams, - processSvgData, - handleFetchError, - ]); - - const updateDiagram = useCallback( - (updates: Partial, shouldFetch: boolean, syncToBackend = true) => { - setDiagramAndSync((prev) => ({ ...prev, ...updates }), syncToBackend); - - if (shouldFetch) { - fetchDiagram(); - } }, - [setDiagramAndSync, fetchDiagram] + [ + currentNode, + language, + studyUuid, + currentNodeId, + currentRootNetworkUuid, + processSvgData, + handleFetchError, + debounceSaveNad, + canFetchDiagram, + networkVisuParams, + ] ); - // Update position in local state only - no Redux dispatch, no fetch - const moveNode = useCallback((voltageLevelId: string, x: number, y: number) => { - setDiagram((prev) => ({ - ...prev, - positions: prev.positions.map((p) => - p.voltageLevelId === voltageLevelId ? { ...p, xPosition: x, yPosition: y } : p - ), - })); - }, []); - - // Update position in local state only - no Redux dispatch, no fetch - const moveTextNode = useCallback((voltageLevelId: string, shiftX: number, shiftY: number) => { - setDiagram((prev) => ({ - ...prev, - positions: prev.positions.map((p) => - p.voltageLevelId === voltageLevelId ? { ...p, xLabelPosition: shiftX, yLabelPosition: shiftY } : p - ), - })); - }, []); - - const handleSaveNad = useCallback(async () => { - if (diagram.voltageLevelIds.length === 0 || !workspaceId) { - return; - } + const editDiagram = useCallback( + (updates: Partial) => { + updateDiagram(updates); + fetchDiagram(true); + }, + [updateDiagram, fetchDiagram] + ); - const scalingFactor = (diagram.svg?.additionalMetadata as { scalingFactor?: number })?.scalingFactor; + const replaceDiagram = useCallback( + (definition: Partial) => { + updateDiagram({ ...BASE_RESET_STATE, ...definition }); + const { title, nadConfigUuid, filterUuid, currentNadConfigUuid, currentFilterUuid, voltageLevelToOmitIds } = + diagramRef.current; + updateNADFields({ + panelId, + fields: { + title, + nadConfigUuid, + filterUuid, + currentNadConfigUuid, + currentFilterUuid, + voltageLevelToOmitIds, + }, + }); + fetchDiagram(); + }, + [panelId, updateDiagram, updateNADFields, fetchDiagram] + ); - try { - const savedUuid = await saveNadConfig(studyUuid, workspaceId, panelId, { - id: diagram.currentNadConfigUuid || null, - scalingFactor, - voltageLevelIds: diagram.voltageLevelIds, - positions: diagram.positions, + const movePosition = useCallback( + (voltageLevelId: string, position: Partial) => { + updateDiagram({ + positions: diagramRef.current.positions.map((p) => + p.voltageLevelId === voltageLevelId ? { ...p, ...position } : p + ), }); + debounceSaveNad(); + }, + [updateDiagram, debounceSaveNad] + ); - setDiagramAndSync( - (prev) => ({ ...prev, currentNadConfigUuid: savedUuid, initialVoltageLevelIds: [] }), - false - ); - } catch (error) { - console.error('Failed to save NAD config:', error); - } - }, [diagram, studyUuid, workspaceId, panelId, setDiagramAndSync]); + const moveNode = useCallback( + (voltageLevelId: string, x: number, y: number) => movePosition(voltageLevelId, { xPosition: x, yPosition: y }), + [movePosition] + ); + + const moveTextNode = useCallback( + (voltageLevelId: string, shiftX: number, shiftY: number) => + movePosition(voltageLevelId, { xLabelPosition: shiftX, yLabelPosition: shiftY }), + [movePosition] + ); const replaceNadConfig = useCallback( (title: string, nadConfigUuid?: UUID, filterUuid?: UUID) => { - // Cleanup saved config if exists - if (diagram.currentNadConfigUuid && workspaceId) { - deleteNadConfig(studyUuid, workspaceId, panelId).catch((error) => - console.error('Failed to delete NAD config:', error) - ); + if (!workspaceId) { + return; } + // A layout save queued before the replace would write back the NAD being left + debounceSaveNad.clear(); - updateDiagram( - { - title, - nadConfigUuid, - filterUuid, - ...BASE_RESET_STATE, - }, - true - ); - }, - [diagram.currentNadConfigUuid, workspaceId, studyUuid, panelId, updateDiagram] - ); + saveNadConfig(studyUuid, workspaceId, panelId, { + title, + nadConfig: null, + nadConfigUuid, + filterUuid, + currentFilterUuid: undefined, + voltageLevelToOmitIds: [], + }).catch((error) => console.error('Failed to replace NAD config:', error)); - const handleNotification = useCallback( - (newConfigUuid?: UUID) => { - if (newConfigUuid) { - // NAD config updated from another tab - updateDiagram( - { - currentNadConfigUuid: newConfigUuid, - initialVoltageLevelIds: [], - voltageLevelIds: [], - positions: [], - svg: null, - }, - true, - false - ); - } else { - // Root network notification (loadflow, etc.) - fetchDiagram(); - } + replaceDiagram({ title, nadConfigUuid, filterUuid }); }, - [updateDiagram, fetchDiagram] + [workspaceId, studyUuid, panelId, debounceSaveNad, replaceDiagram] ); - // Initial fetch and when node or root network changes - useEffect(() => { - fetchDiagram(); - }, [currentNodeId, currentRootNetworkUuid, fetchDiagram]); + const loadNadConfig = useCallback(() => { + if (!workspaceId) { + return; + } + getPanels(studyUuid, workspaceId, [panelId]) + .then(([panel]) => { + if (!panel || !isNADPanel(panel)) { + return; + } + replaceDiagram({ + title: panel.title, + nadConfigUuid: panel.nadConfigUuid, + filterUuid: panel.filterUuid, + currentNadConfigUuid: panel.currentNadConfigUuid, + currentFilterUuid: panel.currentFilterUuid, + voltageLevelToOmitIds: panel.voltageLevelToOmitIds || [], + }); + }) + .catch((error) => console.error('Failed to fetch updated NAD panel:', error)); + }, [studyUuid, workspaceId, panelId, replaceDiagram]); - // Sync cross-tab updates + // Fetch on mount, and whenever what the request is built from changes useEffect(() => { - const nadConfigChanged = initialFields?.nadConfigUuid !== diagram.nadConfigUuid; - const filterChanged = initialFields?.filterUuid !== diagram.filterUuid; - const currentFilterChanged = initialFields?.currentFilterUuid !== diagram.currentFilterUuid; - const omitIdsChanged = - JSON.stringify(initialFields?.voltageLevelToOmitIds ?? []) !== - JSON.stringify(diagram.voltageLevelToOmitIds); - - if (nadConfigChanged || filterChanged) { - // Full reset when NAD is replaced - updateDiagram( - { - nadConfigUuid: initialFields?.nadConfigUuid, - filterUuid: initialFields?.filterUuid, - ...BASE_RESET_STATE, - }, - true, - false - ); - } else if (currentFilterChanged || omitIdsChanged) { - // Incremental update for filter/omit changes - updateDiagram( - { - currentFilterUuid: initialFields?.currentFilterUuid, - voltageLevelToOmitIds: initialFields?.voltageLevelToOmitIds || [], - }, - false, - false - ); - } - }, [ - diagram.nadConfigUuid, - diagram.filterUuid, - diagram.currentFilterUuid, - diagram.voltageLevelToOmitIds, - initialFields?.nadConfigUuid, - initialFields?.filterUuid, - initialFields?.currentFilterUuid, - initialFields?.voltageLevelToOmitIds, - updateDiagram, - ]); + fetchDiagram(); + }, [fetchDiagram]); useDiagramNotifications({ currentRootNetworkUuid, - onNotification: handleNotification, - currentNadConfigUuid: diagram.currentNadConfigUuid, + onNotification: fetchDiagram, panelId, + onNadConfigUpdate: loadNadConfig, }); return { diagram, loading, globalError, - fetchDiagram, - updateDiagram, - handleSaveNad, + editDiagram, replaceNadConfig, moveNode, moveTextNode, diff --git a/src/components/workspace/hooks/use-workspace-panel-actions.ts b/src/components/workspace/hooks/use-workspace-panel-actions.ts index f580d7a76f..f82dbed5d6 100644 --- a/src/components/workspace/hooks/use-workspace-panel-actions.ts +++ b/src/components/workspace/hooks/use-workspace-panel-actions.ts @@ -18,7 +18,7 @@ import { selectPanelByType, selectPanels, } from '../../../redux/slices/workspace-selectors'; -import type { PanelState, PersistentNADFields, SpreadsheetPanel } from '../types/workspace.types'; +import type { PanelState, NadPanelFields, SpreadsheetPanel } from '../types/workspace.types'; import { PanelType } from '../types/workspace.types'; import { type AppDispatch, store } from '../../../redux/store'; import { EquipmentType } from '@gridsuite/commons-ui'; @@ -322,24 +322,28 @@ export const useWorkspacePanelActions = () => { currentFilterUuid: filterUuid, }); saveAndFocusPanel(newPanel); + // The panel must exist server side before its diagram saves a config for it + panelBackendManager.flush(); }, [saveAndFocusPanel] ); const updateNADFields = useCallback( - ({ - panelId, - fields, - syncToBackend = true, - }: { - panelId: UUID; - fields: Partial; - syncToBackend?: boolean; - }) => { + ({ panelId, fields }: { panelId: UUID; fields: Partial }) => { const panel = selectPanel(store.getState(), panelId); if (!panel || !isNADPanel(panel)) return; - savePanels([{ ...panel, ...fields }], syncToBackend); + savePanels([{ ...panel, ...fields }], false); + }, + [savePanels] + ); + + const togglePanelEditMode = useCallback( + ({ panelId }: { panelId: UUID }) => { + const panel = selectPanel(store.getState(), panelId); + if (panel?.editMode === undefined) return; + // Transient UI state - don't sync to backend + savePanels([{ ...panel, editMode: !panel.editMode }], false); }, [savePanels] ); @@ -383,6 +387,7 @@ export const useWorkspacePanelActions = () => { savePanels([updatedSld]); saveAndFocusPanel(newNadPanel); + panelBackendManager.flush(); }, [savePanels, saveAndFocusPanel] ); @@ -471,6 +476,7 @@ export const useWorkspacePanelActions = () => { associateSldToNad, dissociateSldFromNad, updateNADFields, + togglePanelEditMode, addToNadNavigationHistory, createNadAndAssociateSld, openToolPanel, diff --git a/src/components/workspace/hooks/workspace-panel-utils.ts b/src/components/workspace/hooks/workspace-panel-utils.ts index 426101a163..fdcf6e2f45 100644 --- a/src/components/workspace/hooks/workspace-panel-utils.ts +++ b/src/components/workspace/hooks/workspace-panel-utils.ts @@ -105,6 +105,7 @@ export const createNADPanel = ({ title: title || config.title, initialVoltageLevelIds, navigationHistory: navigationHistory || [], + editMode: false, ...(nadConfigUuid && { nadConfigUuid }), ...(filterUuid && { filterUuid }), ...(currentFilterUuid && { currentFilterUuid }), diff --git a/src/components/workspace/panel-contents/diagrams/nad/nad-panel-content.tsx b/src/components/workspace/panel-contents/diagrams/nad/nad-panel-content.tsx index 6fcd3ed8ea..66575ead3f 100644 --- a/src/components/workspace/panel-contents/diagrams/nad/nad-panel-content.tsx +++ b/src/components/workspace/panel-contents/diagrams/nad/nad-panel-content.tsx @@ -7,7 +7,6 @@ import { memo, useCallback } from 'react'; import { Box } from '@mui/material'; -import type { DiagramAdditionalMetadata } from '../../../../grid-layout/cards/diagrams/diagram.type'; import NetworkAreaDiagramContent from '../../../../grid-layout/cards/diagrams/networkAreaDiagram/network-area-diagram-content'; import { DiagramMetadata } from '@powsybl/network-viewer'; import type { UUID } from 'node:crypto'; @@ -35,13 +34,12 @@ export const NadPanelContent = memo(function NadPanelContent({ }: NadPanelContentProps) { const { addToNadNavigationHistory, associateVoltageLevelWithNad } = useWorkspacePanelActions(); - const { diagram, loading, globalError, updateDiagram, handleSaveNad, replaceNadConfig, moveNode, moveTextNode } = - useNadDiagram({ - panelId, - studyUuid, - currentNodeId, - currentRootNetworkUuid, - }); + const { diagram, loading, globalError, editDiagram, replaceNadConfig, moveNode, moveTextNode } = useNadDiagram({ + panelId, + studyUuid, + currentNodeId, + currentRootNetworkUuid, + }); const { handleShowInSpreadsheet } = useDiagramNavigation(); @@ -64,25 +62,11 @@ export const NadPanelContent = memo(function NadPanelContent({ [panelId, addToNadNavigationHistory, associateVoltageLevelWithNad] ); - const handleUpdateVoltageLevels = useCallback( - (params: { voltageLevelIds: string[]; voltageLevelToExpandIds: string[]; voltageLevelToOmitIds: string[] }) => { - updateDiagram(params, true); - }, - [updateDiagram] - ); - const handleUpdateVoltageLevelsFromFilter = useCallback( (filterUuid?: UUID) => { - updateDiagram({ currentFilterUuid: filterUuid }, true); - }, - [updateDiagram] - ); - - const handleReplaceNad = useCallback( - (name: string, nadConfigUuid?: UUID, filterUuid?: UUID) => { - replaceNadConfig(name, nadConfigUuid, filterUuid); + editDiagram({ currentFilterUuid: filterUuid }); }, - [replaceNadConfig] + [editDiagram] ); return ( @@ -96,13 +80,13 @@ export const NadPanelContent = memo(function NadPanelContent({ > diff --git a/src/components/workspace/types/workspace.types.ts b/src/components/workspace/types/workspace.types.ts index 6da3a84fcf..8cd47f5ef0 100644 --- a/src/components/workspace/types/workspace.types.ts +++ b/src/components/workspace/types/workspace.types.ts @@ -45,6 +45,8 @@ interface BasePanel { maximized: boolean; pinned: boolean; zIndex?: number; // Client-only, not persisted to backend + // WHY in BasePanel and not in NADPanel ? Does it exist in other panels ? + editMode?: boolean; // Client-only, not persisted to backend restorePosition?: PanelPosition; restoreSize?: PanelSize; } @@ -57,21 +59,14 @@ export interface NADPanel extends BasePanel { voltageLevelToOmitIds?: string[]; currentNadConfigUuid?: UUID; navigationHistory?: string[]; - initialVoltageLevelIds?: string[]; // For initial diagram load + initialVoltageLevelIds?: string[]; // Client-only, not persisted to backend } -// Persistent NAD fields that sync to Redux and backend -export const PERSISTENT_NAD_FIELDS = [ - 'title', - 'voltageLevelToOmitIds', - 'currentFilterUuid', - 'currentNadConfigUuid', - 'nadConfigUuid', - 'filterUuid', - 'initialVoltageLevelIds', -] as const satisfies readonly (keyof NADPanel)[]; - -export type PersistentNADFields = Pick; +// Everything that decides which NAD a panel shows, the same set the save endpoint writes +export type NadPanelFields = Pick< + NADPanel, + 'title' | 'nadConfigUuid' | 'filterUuid' | 'currentNadConfigUuid' | 'currentFilterUuid' | 'voltageLevelToOmitIds' +>; export interface SLDVoltageLevelPanel extends BasePanel { type: PanelType.SLD_VOLTAGE_LEVEL; diff --git a/src/components/workspace/utils/panel-backend-manager.ts b/src/components/workspace/utils/panel-backend-manager.ts index 86258fbba2..f875766a35 100644 --- a/src/components/workspace/utils/panel-backend-manager.ts +++ b/src/components/workspace/utils/panel-backend-manager.ts @@ -14,6 +14,7 @@ const DEBOUNCE_DELAY_MS = 700; export interface IPanelBackendManager { debounceUpdate(studyUuid: UUID, workspaceId: UUID, panels: PanelState[]): void; debounceDelete(studyUuid: UUID, workspaceId: UUID, panelIds: UUID[]): void; + flush(): void; } export class PanelBackendManager implements IPanelBackendManager { @@ -28,7 +29,7 @@ export class PanelBackendManager implements IPanelBackendManager { this.debounceDelayMs = debounceDelayMs; } - private flush(): void { + flush(): void { if (!this.currentStudyUuid || !this.currentWorkspaceId) return; const studyUuid = this.currentStudyUuid; diff --git a/src/redux/session-storage/workspace-local-storage.ts b/src/redux/session-storage/workspace-local-storage.ts index baf4dbc869..135f9cbc4f 100644 --- a/src/redux/session-storage/workspace-local-storage.ts +++ b/src/redux/session-storage/workspace-local-storage.ts @@ -7,7 +7,7 @@ import type { UUID } from 'node:crypto'; import { LOCAL_STORAGE_KEY_PREFIX } from '../../utils/config-params'; -import { PanelType } from '../../components/workspace/types/workspace.types'; +import { PanelType, type PanelState } from '../../components/workspace/types/workspace.types'; import { Viewport } from '@xyflow/react'; import { ViewBoxLike } from '@svgdotjs/svg.js'; @@ -33,6 +33,20 @@ export type OtherPanelLocalState = BasePanelLocalState & { export type PanelLocalState = TreePanelLocalState | NADPanelLocalState | OtherPanelLocalState; +export function normalizeWorkspacePanelState(panel: PanelState): PanelState { + if (panel.type === PanelType.NAD) { + return { + ...panel, + editMode: panel.editMode ?? false, + }; + } + return panel; +} + +export function normalizeWorkspacePanels(panels: PanelState[]): PanelState[] { + return panels.map((panel) => normalizeWorkspacePanelState(panel)); +} + interface WorkspacesLocalState { activeWorkspaceId?: UUID; workspaces: Record; diff --git a/src/redux/slices/workspace-selectors.ts b/src/redux/slices/workspace-selectors.ts index 2abe9c9647..9465e18799 100644 --- a/src/redux/slices/workspace-selectors.ts +++ b/src/redux/slices/workspace-selectors.ts @@ -150,3 +150,5 @@ export const selectNadNavigationHistory = createSelector([selectPanel], (panel): if (panel?.type !== PanelType.NAD) return undefined; return panel.navigationHistory; }); + +export const selectPanelEditMode = createSelector([selectPanel], (panel): boolean => !!panel?.editMode); diff --git a/src/services/study/workspace.ts b/src/services/study/workspace.ts index 8e6f998231..11178bcaae 100644 --- a/src/services/study/workspace.ts +++ b/src/services/study/workspace.ts @@ -113,13 +113,19 @@ export function saveNadConfig( studyUuid: UUID, workspaceId: UUID, panelId: UUID, - config: { - id?: UUID | null; - scalingFactor?: number; - voltageLevelIds: string[]; - positions: DiagramConfigPosition[]; + request: { + title?: string; + nadConfig: { + scalingFactor?: number; + voltageLevelIds: string[]; + positions: DiagramConfigPosition[]; + } | null; + nadConfigUuid?: UUID; + filterUuid?: UUID; + currentFilterUuid?: UUID; + voltageLevelToOmitIds: string[]; } -): Promise { +): Promise { console.info('save NAD config'); const url = `${getStudyUrl(studyUuid)}/workspaces/${workspaceId}/panels/${panelId}/current-nad-config`; console.debug(url); @@ -129,18 +135,6 @@ export function saveNadConfig( 'Content-Type': 'application/json', clientId: getClientId(), }, - body: JSON.stringify(config), + body: JSON.stringify(request), }); } - -export function deleteNadConfig(studyUuid: UUID, workspaceId: UUID, panelId: UUID): Promise { - console.info('delete NAD config'); - const url = `${getStudyUrl(studyUuid)}/workspaces/${workspaceId}/panels/${panelId}/current-nad-config`; - console.debug(url); - return backendFetch(url, { - method: 'DELETE', - headers: { - clientId: getClientId(), - }, - }).then(() => {}); -} diff --git a/src/translations/messages-en.ts b/src/translations/messages-en.ts index 9260be9aa8..cad2063ae6 100644 --- a/src/translations/messages-en.ts +++ b/src/translations/messages-en.ts @@ -938,7 +938,6 @@ const messages_en = { diagramConfigSave: 'Create a new network area diagram', diagramConfigUpdate: 'Replace an existing network area diagram', EditNad: 'Edit', - apply: 'Apply', applied: 'Applied', notApplied: 'Not applied', ModificationsSelection: 'Modification selection', diff --git a/src/translations/messages-fr.ts b/src/translations/messages-fr.ts index ec3dcca722..39c45f82e1 100644 --- a/src/translations/messages-fr.ts +++ b/src/translations/messages-fr.ts @@ -949,7 +949,6 @@ const messages_fr = { diagramConfigSave: 'Créer une image nodale de zone', diagramConfigUpdate: 'Remplacer une image nodale de zone', EditNad: 'Éditer', - apply: 'Appliquer', applied: 'Appliqué', notApplied: 'Non appliqué', ModificationsSelection: 'Sélection des modifications', diff --git a/src/types/notification-types.ts b/src/types/notification-types.ts index 9daee4d293..f22c0fae95 100644 --- a/src/types/notification-types.ts +++ b/src/types/notification-types.ts @@ -555,7 +555,7 @@ export interface WorkspacePanelsDeletedEventData extends CommonStudyEventData { export interface WorkspaceNadConfigUpdatedEventData extends CommonStudyEventData { headers: WorkspaceNadConfigUpdatedEventDataHeaders; - payload: string; // config UUID + payload: string; } export function isComputationResultColumnFilterUpdatedNotification(