diff --git a/src/components/app-wrapper.jsx b/src/components/app-wrapper.jsx index ecde531cf6..d4d119e30c 100644 --- a/src/components/app-wrapper.jsx +++ b/src/components/app-wrapper.jsx @@ -196,6 +196,7 @@ const lightTheme = createTheme({ overlay: { background: '#e6e6e6', }, + highlightColor: '#1976D214', }, networkModificationPanel: { backgroundColor: 'white', @@ -307,6 +308,7 @@ const darkTheme = createTheme({ overlay: { background: '#121212', }, + highlightColor: '#90CAF929', }, networkModificationPanel: { backgroundColor: '#252525', diff --git a/src/components/graph/menus/network-modifications/network-modifications-table.tsx b/src/components/graph/menus/network-modifications/network-modifications-table.tsx index ef08ea18aa..3d3b1179c7 100644 --- a/src/components/graph/menus/network-modifications/network-modifications-table.tsx +++ b/src/components/graph/menus/network-modifications/network-modifications-table.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React, { useCallback, useMemo, SetStateAction } from 'react'; +import React, { useCallback, useMemo, SetStateAction, useRef, useEffect, useState } from 'react'; import { CustomAGGrid, NetworkModificationMetadata, useModificationLabelComputer } from '@gridsuite/commons-ui'; import { CellClickedEvent, @@ -20,7 +20,7 @@ import { ValueGetterParams, } from 'ag-grid-community'; import { RemoveRedEye as RemoveRedEyeIcon } from '@mui/icons-material'; -import { Badge, Box, Theme } from '@mui/material'; +import { Badge, Box, Theme, useTheme } from '@mui/material'; import { useSelector } from 'react-redux'; import { AppState } from 'redux/reducer'; import { useIntl } from 'react-intl'; @@ -33,6 +33,7 @@ import SwitchCellRenderer from './switch-cell-renderer'; import { AGGRID_LOCALES } from '../../../../translations/not-intl/aggrid-locales'; import { ExcludedNetworkModifications } from './network-modification-menu.type'; import { NetworkModificationNameCellRenderer } from 'components/custom-aggrid/cell-renderers'; +import { AgGridReact } from 'ag-grid-react'; const styles = { container: (theme: Theme) => ({ @@ -78,8 +79,13 @@ const NetworkModificationsTable: React.FC = ({ setModificationsToExclude, ...nameHeaderProps }) => { + const gridRef = useRef(null); + const theme = useTheme(); + const rootNetworks = useSelector((state: AppState) => state.rootNetworks); const isMonoRootStudy = useSelector((state: AppState) => state.isMonoRootStudy); + const hightlightedModificationUuid = useSelector((state: AppState) => state.hightlightedModificationUuid); + const [isGridReady, setIsGridReady] = useState(false); const intl = useIntl(); const { computeLabel } = useModificationLabelComputer(); @@ -194,17 +200,53 @@ const NetworkModificationsTable: React.FC = ({ const getRowId = (params: GetRowIdParams) => params.data.uuid; - const getRowStyle = useCallback((cellData: RowClassParams) => { - const style: RowStyle = {}; - if (!cellData?.data?.activated) { - style.opacity = 0.4; - } - return style; + const getRowStyle = useCallback( + (cellData: RowClassParams) => { + const style: RowStyle = {}; + if (!cellData?.data?.activated) { + style.opacity = 0.4; + } + + // Highlight the selected modification + if (cellData?.data?.uuid === hightlightedModificationUuid) { + style.backgroundColor = theme.aggrid.highlightColor; + } + return style; + }, + [hightlightedModificationUuid, theme] + ); + + const onGridReady = useCallback(() => { + setIsGridReady(true); }, []); + useEffect(() => { + if (!gridRef.current?.api) { + return; + } + const gridApi = gridRef.current.api; + + const tryHighlight = () => { + let node = undefined; + if (hightlightedModificationUuid) { + node = gridApi.getRowNode(hightlightedModificationUuid); + } + + if (node) { + gridApi.ensureNodeVisible(node, 'middle'); + } + gridApi.removeEventListener('modelUpdated', tryHighlight); + }; + + tryHighlight(); + + gridApi.addEventListener('modelUpdated', tryHighlight); + }, [isGridReady, hightlightedModificationUuid]); + return ( = ({ rowDragManaged={!isRowDragDisabled} suppressNoRowsOverlay={true} overrideLocales={AGGRID_LOCALES} + onGridReady={onGridReady} /> ); diff --git a/src/components/graph/menus/network-modifications/node-editor.tsx b/src/components/graph/menus/network-modifications/node-editor.tsx index 88b4f1ec30..4d5a3002a1 100644 --- a/src/components/graph/menus/network-modifications/node-editor.tsx +++ b/src/components/graph/menus/network-modifications/node-editor.tsx @@ -10,7 +10,7 @@ import NetworkModificationNodeEditor from './network-modification-node-editor'; import { ComputingType } from '@gridsuite/commons-ui'; import { useDispatch, useSelector } from 'react-redux'; -import { setToggleOptions } from '../../../../redux/actions'; +import { setHighlightModification, setToggleOptions } from '../../../../redux/actions'; import { Alert, Box } from '@mui/material'; import { AppState } from '../../../../redux/reducer'; import { CheckCircleOutlined } from '@mui/icons-material'; @@ -49,6 +49,8 @@ const NodeEditor = () => { const toggleOptions = useSelector((state: AppState) => state.toggleOptions); const closeModificationsDrawer = () => { + dispatch(setHighlightModification(null)); + dispatch(setToggleOptions(toggleOptions.filter((option) => option !== StudyDisplayMode.MODIFICATIONS))); }; diff --git a/src/components/graph/menus/root-network/root-network-modification-results.tsx b/src/components/graph/menus/root-network/root-network-modification-results.tsx index ad26334bdf..abca50d6ef 100644 --- a/src/components/graph/menus/root-network/root-network-modification-results.tsx +++ b/src/components/graph/menus/root-network/root-network-modification-results.tsx @@ -8,15 +8,35 @@ import { useIntl } from 'react-intl'; import { useModificationLabelComputer } from '@gridsuite/commons-ui'; import { useCallback } from 'react'; import { Modification } from './root-network.types'; -import { Typography } from '@mui/material'; +import { Box, Theme, Typography } from '@mui/material'; +import { UUID } from 'crypto'; +import { AppState } from 'redux/reducer'; +import { useDispatch, useSelector } from 'react-redux'; +import { setCentedNode, setCurrentTreeNode, setHighlightModification, setModificationsDrawerOpen } from 'redux/actions'; +import { StudyDisplayMode } from 'components/network-modification.type'; +import { useDisplayModes } from 'hooks/use-display-modes'; interface ModificationResultsProps { modifications: Modification[]; + nodeUuid: UUID; } -export const ModificationResults: React.FC = ({ modifications }) => { +const styles = { + itemHover: (theme: Theme) => ({ + borderRadius: 1, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.aggrid.highlightColor, + }, + }), +}; +export const ModificationResults: React.FC = ({ modifications, nodeUuid }) => { const intl = useIntl(); const { computeLabel } = useModificationLabelComputer(); + const treeNodes = useSelector((state: AppState) => state.networkModificationTreeModel?.treeNodes); + const dispatch = useDispatch(); + const toggleOptions = useSelector((state: AppState) => state.toggleOptions); + const { applyModes } = useDisplayModes(); const getModificationLabel = useCallback( (modification?: Modification): React.ReactNode => { @@ -28,18 +48,42 @@ export const ModificationResults: React.FC = ({ modifi { id: 'network_modifications.' + modification.messageType }, { // @ts-ignore - ...computeLabel(modification), + ...computeLabel(modification, false), } ); }, [computeLabel, intl] ); + + const handleClick = useCallback( + (modification: Modification) => { + dispatch(setModificationsDrawerOpen()); + const node = treeNodes?.find((node) => node.id === nodeUuid); + if (node) { + dispatch(setCurrentTreeNode(node)); + dispatch(setCentedNode(node)); + } + if (toggleOptions.includes(StudyDisplayMode.EVENT_SCENARIO)) { + applyModes(toggleOptions.filter((option) => option !== StudyDisplayMode.EVENT_SCENARIO)); + } + + dispatch(setHighlightModification(modification.modificationUuid)); + }, + [applyModes, dispatch, nodeUuid, toggleOptions, treeNodes] + ); + return ( <> {modifications.map((modification) => ( - - {modification.impactedEquipmentId + ' - '} {getModificationLabel(modification)} - + + handleClick(modification)} + sx={{ cursor: 'pointer', pt: 0.5, pb: 0.5 }} + > + {modification.impactedEquipmentId + ' - '} {getModificationLabel(modification)} + + ))} ); diff --git a/src/components/graph/menus/root-network/root-network-modifications-search-results.tsx b/src/components/graph/menus/root-network/root-network-modifications-search-results.tsx index b92269feb6..25cded1c31 100644 --- a/src/components/graph/menus/root-network/root-network-modifications-search-results.tsx +++ b/src/components/graph/menus/root-network/root-network-modifications-search-results.tsx @@ -60,7 +60,7 @@ export const RootNetworkModificationsSearchResults: React.FC - + ))} diff --git a/src/components/graph/menus/root-network/root-network-nodes-search-results.tsx b/src/components/graph/menus/root-network/root-network-nodes-search-results.tsx index ff0ce6ea57..02be59b7bd 100644 --- a/src/components/graph/menus/root-network/root-network-nodes-search-results.tsx +++ b/src/components/graph/menus/root-network/root-network-nodes-search-results.tsx @@ -5,8 +5,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { DeviceHubIcon, OverflowableText } from '@gridsuite/commons-ui'; -import { Box, Divider } from '@mui/material'; -import React from 'react'; +import { Box, Divider, Theme } from '@mui/material'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { setCentedNode, setCurrentTreeNode } from 'redux/actions'; +import { AppState } from 'redux/reducer'; interface RootNetworkNodesSearchResultsProps { results: string[]; @@ -20,23 +23,47 @@ const styles = { rootNameTitle: { display: 'flex', alignItems: 'center', + pt: 1, mb: 1, }, + + itemHover: (theme: Theme) => ({ + mb: 1, + borderRadius: 1, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.aggrid.highlightColor, + }, + }), iconMinSize: { minHeight: '20px', minWidth: '20px', }, }; export const RootNetworkNodesSearchResults: React.FC = ({ results }) => { + const dispatch = useDispatch(); + const treeNodes = useSelector((state: AppState) => state.networkModificationTreeModel?.treeNodes); + + const handleClick = useCallback( + (nodeName: string) => { + const node = treeNodes?.find((node) => node.data.label === nodeName); + if (node) { + dispatch(setCurrentTreeNode(node)); + dispatch(setCentedNode(node)); + } + }, + [dispatch, treeNodes] + ); + return ( {results.map((result) => ( - - + + handleClick(result)}> - + ))} diff --git a/src/components/graph/menus/root-network/root-network-panel-header.tsx b/src/components/graph/menus/root-network/root-network-panel-header.tsx index f83716b94d..b7f41475aa 100644 --- a/src/components/graph/menus/root-network/root-network-panel-header.tsx +++ b/src/components/graph/menus/root-network/root-network-panel-header.tsx @@ -24,7 +24,7 @@ import { UUID } from 'crypto'; import { GetCaseImportParametersReturn, getCaseImportParameters } from 'services/network-conversion'; import { customizeCurrentParameters, formatCaseImportParameters } from '../../util/case-import-parameters'; import { useDispatch, useSelector } from 'react-redux'; -import { setMonoRootStudy } from 'redux/actions'; +import { setHighlightModification, setMonoRootStudy } from 'redux/actions'; import { CustomDialog } from 'components/utils/custom-dialog'; import SearchIcon from '@mui/icons-material/Search'; @@ -189,7 +189,8 @@ const RootNetworkPanelHeader: React.FC = ({ const minimizeRootNetworkPanel = useCallback(() => { setIsSearchActive(false); setIsRootNetworkPanelMinimized((prev) => !prev); - }, [setIsRootNetworkPanelMinimized, setIsSearchActive]); + dispatch(setHighlightModification(null)); + }, [dispatch, setIsRootNetworkPanelMinimized, setIsSearchActive]); const openSearch = useCallback(() => { setIsSearchActive(true); diff --git a/src/components/graph/menus/root-network/root-network-panel-search.tsx b/src/components/graph/menus/root-network/root-network-panel-search.tsx index 8a71237051..892490a338 100644 --- a/src/components/graph/menus/root-network/root-network-panel-search.tsx +++ b/src/components/graph/menus/root-network/root-network-panel-search.tsx @@ -17,6 +17,8 @@ import SearchBar from './root-network-search-bar'; import { RootNetworkNodesSearchResults } from './root-network-nodes-search-results'; import { useRootNetworkNodeSearch } from './use-root-network-node-search'; import { useRootNetworkModificationSearch } from './use-root-network-modification-search'; +import { setHighlightModification } from 'redux/actions'; +import { useDispatch } from 'react-redux'; enum TAB_VALUES { modifications = 'MODIFICATIONS', @@ -71,11 +73,12 @@ const RootNetworkSearchPanel: React.FC = ({ setIsSe const isLoading = isNodeTab(tabValue) ? nodesSearch.isLoading : modificationsSearch.isLoading; const searchTerm = isNodeTab(tabValue) ? nodesSearch.searchTerm : modificationsSearch.searchTerm; - + const dispatch = useDispatch(); const leaveSearch = () => { nodesSearch.reset(); modificationsSearch.reset(); setIsSearchActive(false); + dispatch(setHighlightModification(null)); }; const handleOnChange = (e: React.ChangeEvent) => { diff --git a/src/components/graph/menus/root-network/use-root-network-search-notifications.ts b/src/components/graph/menus/root-network/use-root-network-search-notifications.ts index 01c4682b33..77a509fbc5 100644 --- a/src/components/graph/menus/root-network/use-root-network-search-notifications.ts +++ b/src/components/graph/menus/root-network/use-root-network-search-notifications.ts @@ -46,12 +46,9 @@ export const useRootNetworkSearchNotifications = ({ const nodeSubTreeCreated = isNodSubTreeCreatedNotification(eventData); if (nodesStatus || rootNetworksStatus || networkModificationsStatus) { - console.log(' inside thez modification node'); - resetModificationsSearch(); } if (nodeDeleted || nodeCreated || nodeEdited || nodeSubTreeCreated) { - console.log(' inside thez delete node'); resetNodesSearch(); } }, diff --git a/src/components/network-modification-tree.jsx b/src/components/network-modification-tree.jsx index e78241190e..329d6c3360 100644 --- a/src/components/network-modification-tree.jsx +++ b/src/components/network-modification-tree.jsx @@ -62,6 +62,8 @@ const NetworkModificationTree = ({ onNodeContextMenu, studyUuid, onTreePanelResi const toggleOptions = useSelector((state) => state.toggleOptions); + const centeredNode = useSelector((state) => state.centeredNode); + const [isMinimapOpen, setIsMinimapOpen] = useState(false); const { fitView, setCenter, getZoom } = useReactFlow(); @@ -330,6 +332,12 @@ const NetworkModificationTree = ({ onNodeContextMenu, studyUuid, onTreePanelResi setCenter(centerX, centerY, { zoom: getZoom() }); }, [currentNode, nodes, setCenter, getZoom]); + useEffect(() => { + if (centeredNode) { + handleFocusNode(); + } + }, [centeredNode, handleFocusNode]); + useEffect(() => { if (onTreePanelResize) { onTreePanelResize.current = handleFocusNode; diff --git a/src/module-mui.d.ts b/src/module-mui.d.ts index ebeaabf0df..7c2ed7257d 100644 --- a/src/module-mui.d.ts +++ b/src/module-mui.d.ts @@ -25,6 +25,7 @@ declare module '@mui/material/styles' { overlay: { background: string; }; + highlightColor: string; }; networkModificationPanel: { backgroundColor: string; diff --git a/src/redux/actions.ts b/src/redux/actions.ts index d83cfc613a..a3eb3e482a 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -628,6 +628,30 @@ export function setCurrentTreeNode(currentTreeNode: CurrentTreeNode): CurrentTre }; } +export const HIGHLIGHT_MODIFICATION = 'HIGHLIGHT_MODIFICATION'; +export type HighlightModificationAction = Readonly> & { + hightlightedModificationUuid: UUID | null; +}; + +export function setHighlightModification(modifficationUuid: UUID | null): HighlightModificationAction { + return { + type: HIGHLIGHT_MODIFICATION, + hightlightedModificationUuid: modifficationUuid, + }; +} + +export const CENTER_NODE = 'CENTER_NODE'; +export type CenterNodeAction = Readonly> & { + centeredNode: CurrentTreeNode; +}; + +export function setCentedNode(centeredNode: CurrentTreeNode): CenterNodeAction { + return { + type: CENTER_NODE, + centeredNode: centeredNode, + }; +} + export const CURRENT_ROOT_NETWORK_UUID = 'CURRENT_ROOT_NETWORK_UUID'; export type CurrentRootNetworkUuidAction = Readonly> & { currentRootNetworkUuid: UUID; diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 95f203079e..60671ad02b 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -1,3 +1,4 @@ +import { CENTER_NODE, CenterNodeAction } from './actions'; /** * Copyright (c) 2022, RTE (http://www.rte-france.com) * This Source Code Form is subject to the terms of the Mozilla Public @@ -62,6 +63,8 @@ import { type EnableDeveloperModeAction, FAVORITE_CONTINGENCY_LISTS, type FavoriteContingencyListsAction, + HIGHLIGHT_MODIFICATION, + HighlightModificationAction, INIT_TABLE_DEFINITIONS, type InitTableDefinitionsAction, LOAD_EQUIPMENTS, @@ -513,6 +516,8 @@ export interface AppState extends CommonStoreState, AppConfigState { deletedOrRenamedNodes: UUID[]; diagramGridLayout: DiagramGridLayoutConfig; toggleOptions: StudyDisplayMode[]; + hightlightedModificationUuid: UUID | null; + centeredNode: CurrentTreeNode | null; mapOpen: boolean; } @@ -621,6 +626,8 @@ const initialState: AppState = { params: [], }, toggleOptions: [StudyDisplayMode.TREE], + hightlightedModificationUuid: null, + centeredNode: null, computingStatus: { [ComputingType.LOAD_FLOW]: RunningStatus.IDLE, [ComputingType.SECURITY_ANALYSIS]: RunningStatus.IDLE, @@ -1137,6 +1144,13 @@ export const reducer = createReducer(initialState, (builder) => { state.reloadMapNeeded = true; }); + builder.addCase(HIGHLIGHT_MODIFICATION, (state, action: HighlightModificationAction) => { + state.hightlightedModificationUuid = action.hightlightedModificationUuid; + }); + builder.addCase(CENTER_NODE, (state, action: CenterNodeAction) => { + state.centeredNode = action.centeredNode; + }); + builder.addCase(CURRENT_ROOT_NETWORK_UUID, (state, action: CurrentRootNetworkUuidAction) => { if (state.currentRootNetworkUuid !== action.currentRootNetworkUuid) { state.currentRootNetworkUuid = action.currentRootNetworkUuid;