diff --git a/src/components/spreadsheet-view/hooks/use-built-nodes-ids.ts b/src/components/spreadsheet-view/hooks/use-built-nodes-ids.ts new file mode 100644 index 0000000000..c9883d6ad0 --- /dev/null +++ b/src/components/spreadsheet-view/hooks/use-built-nodes-ids.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2025, 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 { useSelector } from 'react-redux'; +import type { AppState } from '../../../redux/reducer'; +import { useStableComputedSet } from '../../../hooks/use-stable-computed-set'; +import type { UUID } from 'node:crypto'; +import { validAlias } from './use-node-aliases'; +import { NodeType } from '../../graph/tree-node.type'; +import { isStatusBuilt } from '../../graph/util/model-functions'; +import type { NodeAlias } from '../types/node-alias.type'; + +export function useBuiltNodesIds(nodeAliases: NodeAlias[] | undefined) { + const currentNode = useSelector((state: AppState) => state.currentTreeNode); + const treeNodes = useSelector((state: AppState) => state.networkModificationTreeModel?.treeNodes); + + return useStableComputedSet(() => { + const aliasedNodesIds = nodeAliases + ?.filter((nodeAlias) => validAlias(nodeAlias)) + .map((nodeAlias) => nodeAlias.id); + if (currentNode?.id) { + aliasedNodesIds?.push(currentNode.id); + } + + const ids = new Set(); + if (aliasedNodesIds && aliasedNodesIds.length > 0 && treeNodes) { + for (const treeNode of treeNodes) { + if ( + aliasedNodesIds.includes(treeNode.id) && + (treeNode.type === NodeType.ROOT || isStatusBuilt(treeNode.data.globalBuildStatus)) + ) { + ids.add(treeNode.id); + } + } + } + return ids; + }, [nodeAliases, treeNodes]); +} diff --git a/src/components/spreadsheet-view/hooks/use-update-equipments-on-notification.ts b/src/components/spreadsheet-view/hooks/use-update-equipments-on-notification.ts new file mode 100644 index 0000000000..f983c9074d --- /dev/null +++ b/src/components/spreadsheet-view/hooks/use-update-equipments-on-notification.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025, 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 { useCallback } from 'react'; +import type { UUID } from 'node:crypto'; +import { DeletedEquipment, isStudyNotification, type NetworkImpactsInfos } from '../../../types/notification-types'; +import { isSpreadsheetEquipmentType, SpreadsheetEquipmentType } from '../types/spreadsheet.type'; +import { + deleteEquipments, + type EquipmentToDelete, + resetEquipments, + resetEquipmentsByTypes, + updateEquipments, +} from '../../../redux/actions'; +import { fetchAllEquipments } from '../../../services/study/network-map'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'redux/reducer'; +import { NotificationsUrlKeys, useNotificationsListener } from '@gridsuite/commons-ui'; +import { NodeAlias } from '../types/node-alias.type'; +import { useBuiltNodesIds } from './use-built-nodes-ids'; + +const SPREADSHEET_EQUIPMENTS_LISTENER_ID = 'spreadsheet-equipments-listener'; + +export function useUpdateEquipmentsOnNotification(nodeAliases: NodeAlias[] | undefined) { + const dispatch = useDispatch(); + const allEquipments = useSelector((state: AppState) => state.spreadsheetNetwork); + const studyUuid = useSelector((state: AppState) => state.studyUuid); + const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); + + const builtNodesIds = useBuiltNodesIds(nodeAliases); + + const updateEquipmentsLocal = useCallback( + ( + nodeUuid: UUID, + impactedSubstationsIds: UUID[], + deletedEquipments: DeletedEquipment[], + impactedElementTypes: string[] + ) => { + // Handle updates and resets based on impacted element types + if (impactedElementTypes.length > 0) { + if (impactedElementTypes.includes(SpreadsheetEquipmentType.SUBSTATION)) { + dispatch(resetEquipments()); + return; + } + const impactedSpreadsheetEquipmentsTypes = impactedElementTypes.filter((type) => + Object.keys(allEquipments).includes(type) + ); + if (impactedSpreadsheetEquipmentsTypes.length > 0) { + dispatch( + resetEquipmentsByTypes(impactedSpreadsheetEquipmentsTypes.filter(isSpreadsheetEquipmentType)) + ); + } + return; + } + + if (impactedSubstationsIds.length > 0 && studyUuid && currentRootNetworkUuid) { + fetchAllEquipments(studyUuid, nodeUuid, currentRootNetworkUuid, impactedSubstationsIds).then( + (values) => { + dispatch(updateEquipments(values, nodeUuid)); + } + ); + } + + if (deletedEquipments.length > 0) { + const equipmentsToDelete = deletedEquipments + .filter(({ equipmentType, equipmentId }) => equipmentType && equipmentId) + .map(({ equipmentType, equipmentId }) => { + console.info( + 'removing equipment with id=', + equipmentId, + ' and type=', + equipmentType, + ' from the network' + ); + return { equipmentType, equipmentId }; + }); + + if (equipmentsToDelete.length > 0) { + const equipmentsToDeleteArray = equipmentsToDelete + .filter((e) => isSpreadsheetEquipmentType(e.equipmentType)) + .map((equipment) => ({ + equipmentType: equipment.equipmentType as unknown as SpreadsheetEquipmentType, + equipmentId: equipment.equipmentId, + })); + dispatch(deleteEquipments(equipmentsToDeleteArray, nodeUuid)); + } + } + }, + [studyUuid, currentRootNetworkUuid, dispatch, allEquipments] + ); + + const listenerUpdateEquipmentsLocal = useCallback( + (event: MessageEvent) => { + const eventData = JSON.parse(event.data); + if (isStudyNotification(eventData)) { + const eventStudyUuid = eventData.headers.studyUuid; + const eventNodeUuid = eventData.headers.node; + const eventRootNetworkUuid = eventData.headers.rootNetworkUuid; + if ( + studyUuid === eventStudyUuid && + currentRootNetworkUuid === eventRootNetworkUuid && + builtNodesIds.has(eventNodeUuid) + ) { + const networkImpacts = JSON.parse(eventData.payload) as NetworkImpactsInfos; + updateEquipmentsLocal( + eventNodeUuid, + networkImpacts.impactedSubstationsIds, + networkImpacts.deletedEquipments, + networkImpacts.impactedElementTypes ?? [] + ); + } + } + }, + [builtNodesIds, currentRootNetworkUuid, studyUuid, updateEquipmentsLocal] + ); + + useNotificationsListener(NotificationsUrlKeys.STUDY, { + listenerCallbackMessage: listenerUpdateEquipmentsLocal, + propsId: SPREADSHEET_EQUIPMENTS_LISTENER_ID, + }); +} diff --git a/src/components/spreadsheet-view/spreadsheet-view.tsx b/src/components/spreadsheet-view/spreadsheet-view.tsx index 474560833c..c65e6ddcac 100644 --- a/src/components/spreadsheet-view/spreadsheet-view.tsx +++ b/src/components/spreadsheet-view/spreadsheet-view.tsx @@ -23,6 +23,7 @@ import { initTableDefinitions, setActiveSpreadsheetTab } from 'redux/actions'; import { type MuiStyles, PopupConfirmationDialog, useSnackMessage } from '@gridsuite/commons-ui'; import { processSpreadsheetsCollectionData } from './add-spreadsheet/dialogs/add-spreadsheet-utils'; import { DiagramType } from 'components/grid-layout/cards/diagrams/diagram.type'; +import { useUpdateEquipmentsOnNotification } from './hooks/use-update-equipments-on-notification'; const styles = { invalidNode: { @@ -60,6 +61,8 @@ export const SpreadsheetView: FunctionComponent = ({ const studyUuid = useSelector((state: AppState) => state.studyUuid); const [resetConfirmationDialogOpen, setResetConfirmationDialogOpen] = useState(false); + useUpdateEquipmentsOnNotification(nodeAliases); + const handleSwitchTab = useCallback( (tabUuid: UUID) => { dispatch(setActiveSpreadsheetTab(tabUuid)); @@ -137,6 +140,7 @@ export const SpreadsheetView: FunctionComponent = ({ ) : ( + nodeAliases && tablesDefinitions.map((tabDef) => { const isActive = activeSpreadsheetTabUuid === tabDef.uuid; const equipmentIdToScrollTo = tabDef.type === equipmentType && isActive ? equipmentId : null; diff --git a/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-spreadsheet-equipments.ts b/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-spreadsheet-equipments.ts index ccd337d8eb..ee84d4f088 100644 --- a/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-spreadsheet-equipments.ts +++ b/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/hooks/use-spreadsheet-equipments.ts @@ -5,54 +5,29 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { NotificationsUrlKeys, useNotificationsListener, useSnackMessage } from '@gridsuite/commons-ui'; -import type { UUID } from 'crypto'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - cleanEquipments, - deleteEquipments, - type EquipmentToDelete, - removeNodeData, - resetEquipments, - resetEquipmentsByTypes, - updateEquipments, - type UpdateEquipmentsAction, -} from 'redux/actions'; +import { cleanEquipments, removeNodeData } from 'redux/actions'; import { type AppState } from 'redux/reducer'; -import { isSpreadsheetEquipmentType, SpreadsheetEquipmentType } from '../../../types/spreadsheet.type'; -import { fetchAllEquipments } from 'services/study/network-map'; import type { NodeAlias } from '../../../types/node-alias.type'; -import { isStatusBuilt } from '../../../../graph/util/model-functions'; -import { useFetchEquipment } from '../../../hooks/use-fetch-equipment'; -import { type DeletedEquipment, isStudyNotification, type NetworkImpactsInfos } from 'types/notification-types'; -import { NodeType } from '../../../../graph/tree-node.type'; -import { validAlias } from '../../../hooks/use-node-aliases'; -import { fetchNetworkElementInfos } from 'services/study/network'; -import { EQUIPMENT_INFOS_TYPES } from '../../../../utils/equipment-types'; import { useOptionalLoadingParametersForEquipments } from './use-optional-loading-parameters-for-equipments'; - -const SPREADSHEET_EQUIPMENTS_LISTENER_ID = 'spreadsheet-equipments-listener'; +import { SpreadsheetEquipmentType } from '../../../types/spreadsheet.type'; +import { useFetchEquipment } from 'components/spreadsheet-view/hooks/use-fetch-equipment'; +import { useBuiltNodesIds } from '../../../hooks/use-built-nodes-ids'; +import { useStableComputedSet } from '../../../../../hooks/use-stable-computed-set'; +import type { UUID } from 'crypto'; export const useSpreadsheetEquipments = ( type: SpreadsheetEquipmentType, - equipmentToUpdateId: string | null, - highlightUpdatedEquipment: () => void, - nodeAliases: NodeAlias[] | undefined, + nodeAliases: NodeAlias[], active: boolean = false ) => { - const { snackError } = useSnackMessage(); const dispatch = useDispatch(); - const allEquipments = useSelector((state: AppState) => state.spreadsheetNetwork); - const equipments = useSelector((state: AppState) => state.spreadsheetNetwork[type]); - const studyUuid = useSelector((state: AppState) => state.studyUuid); - const isNetworkModificationTreeModelUpToDate = useSelector( - (state: AppState) => state.isNetworkModificationTreeModelUpToDate - ); const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); const currentNode = useSelector((state: AppState) => state.currentTreeNode); const treeNodes = useSelector((state: AppState) => state.networkModificationTreeModel?.treeNodes); - const [builtAliasedNodesIds, setBuiltAliasedNodesIds] = useState(); + const equipments = useSelector((state: AppState) => state.spreadsheetNetwork[type]); + const loadedNodesIdsForType = useSelector((state: AppState) => state.spreadsheetNetwork[type].nodesId); const [isFetching, setIsFetching] = useState(false); const { fetchNodesEquipmentData } = useFetchEquipment(type); @@ -71,276 +46,51 @@ export const useSpreadsheetEquipments = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldCleanOptionalLoadingParameters, type]); - // effect to keep builtAliasedNodesIds up-to-date (when we add/remove an alias or build/unbuild an aliased node) - useEffect(() => { - if (!nodeAliases) { - return; - } - let computedIds: UUID[] = []; - const aliasedNodesIds = nodeAliases - .filter((nodeAlias) => validAlias(nodeAlias)) - .map((nodeAlias) => nodeAlias.id); - if (aliasedNodesIds.length > 0) { - treeNodes?.forEach((treeNode) => { - if ( - aliasedNodesIds.includes(treeNode.id) && - (treeNode.type === NodeType.ROOT || isStatusBuilt(treeNode.data.globalBuildStatus)) - ) { - computedIds.push(treeNode.id); - } - }); - } - // Because of treeNodes: update the state only on real values changes (to avoid multiple effects for the watchers) - setBuiltAliasedNodesIds((prevState) => { - const currentIds = prevState; - currentIds?.sort((a, b) => a.localeCompare(b)); - computedIds.sort((a, b) => a.localeCompare(b)); - if (JSON.stringify(currentIds) !== JSON.stringify(computedIds)) { - return computedIds; - } - return prevState; - }); - }, [nodeAliases, treeNodes]); + const builtNodesIds = useBuiltNodesIds(nodeAliases); - const nodesIdToFetch = useMemo(() => { - const nodesIdToFetch = new Set(); - if (!equipments.nodesId || !builtAliasedNodesIds || !currentNode?.id) { - return nodesIdToFetch; + const nodesIdsToRemove = useStableComputedSet(() => { + const unwantedFetchedNodes = new Set(loadedNodesIdsForType); + for (const nodeId of builtNodesIds) { + unwantedFetchedNodes.delete(nodeId); } + return unwantedFetchedNodes; + }, [builtNodesIds]); - // If optional loading parameters changed we should refetch all nodes + const nodesIdsToFetch = useStableComputedSet(() => { if (shouldLoadOptionalLoadingParameters) { - nodesIdToFetch.add(currentNode.id); - for (const builtAliasNodeId of builtAliasedNodesIds) { - nodesIdToFetch.add(builtAliasNodeId); - } - return nodesIdToFetch; - } - - // We check if we have the data for the currentNode and if we don't, we save the fact that we need to fetch it - if (equipments.nodesId.find((nodeId) => nodeId === currentNode.id) === undefined) { - nodesIdToFetch.add(currentNode.id); + return builtNodesIds; } - // Then we do the same for the other nodes we need the data of (the ones defined in aliases) - for (const builtAliasNodeId of builtAliasedNodesIds) { - if (equipments.nodesId.find((nodeId) => nodeId === builtAliasNodeId) === undefined) { - nodesIdToFetch.add(builtAliasNodeId); - } + const nodesIdToFetch = new Set(builtNodesIds); + for (const nodeId of loadedNodesIdsForType) { + nodesIdToFetch.delete(nodeId); } return nodesIdToFetch; - }, [currentNode?.id, equipments.nodesId, builtAliasedNodesIds, shouldLoadOptionalLoadingParameters]); + }, [currentNode?.id, loadedNodesIdsForType, nodeAliases, treeNodes]); // effect to unload equipment data when we remove an alias or unbuild an aliased node useEffect(() => { - if (!equipments || !builtAliasedNodesIds || !currentNode?.id) { - return; + if (active && nodesIdsToRemove.size > 0) { + dispatch(removeNodeData(Array.from(nodesIdsToRemove))); } - const currentNodeId = currentNode.id; - const unwantedFetchedNodes = new Set(equipments.nodesId); - const usedNodesId = new Set(builtAliasedNodesIds); - usedNodesId.add(currentNodeId); - usedNodesId.forEach((nodeId) => unwantedFetchedNodes.delete(nodeId)); - if (unwantedFetchedNodes.size !== 0) { - dispatch(removeNodeData(Array.from(unwantedFetchedNodes))); - } - }, [builtAliasedNodesIds, currentNode?.id, dispatch, equipments]); - - const updateEquipmentsLocal = useCallback( - (impactedSubstationsIds: UUID[], deletedEquipments: DeletedEquipment[], impactedElementTypes: string[]) => { - if (!type) { - return; - } - // updating data related to impacted elements - const nodeId = currentNode?.id as UUID; //TODO maybe do nothing if no current node? - - // Handle updates and resets based on impacted element types - if (impactedElementTypes.length > 0) { - if (impactedElementTypes.includes(SpreadsheetEquipmentType.SUBSTATION)) { - dispatch(resetEquipments()); - return; - } - const impactedSpreadsheetEquipmentsTypes = impactedElementTypes.filter((type) => - Object.keys(allEquipments).includes(type) - ); - if (impactedSpreadsheetEquipmentsTypes.length > 0) { - dispatch( - resetEquipmentsByTypes(impactedSpreadsheetEquipmentsTypes.filter(isSpreadsheetEquipmentType)) - ); - } - } - - if (impactedSubstationsIds.length > 0 && studyUuid && currentRootNetworkUuid && currentNode?.id) { - // The formatting of the fetched equipments is done in the reducer - if ( - type === SpreadsheetEquipmentType.SUBSTATION || - type === SpreadsheetEquipmentType.VOLTAGE_LEVEL || - !equipmentToUpdateId - ) { - // we must fetch data for all equipments, as substation data (country) and voltage level data(nominalV) - // can be displayed for all equipment types - fetchAllEquipments(studyUuid, nodeId, currentRootNetworkUuid, impactedSubstationsIds).then( - (values) => { - highlightUpdatedEquipment(); - dispatch(updateEquipments(values, nodeId)); - } - ); - } else { - // here, we can fetch only the data for the modified equipment - const promises = [ - fetchNetworkElementInfos( - studyUuid, - nodeId, - currentRootNetworkUuid, - type, - EQUIPMENT_INFOS_TYPES.TAB.type, - equipmentToUpdateId, - false - ), - ]; - if ( - type === SpreadsheetEquipmentType.LINE || - type === SpreadsheetEquipmentType.TWO_WINDINGS_TRANSFORMER - ) { - promises.push( - fetchNetworkElementInfos( - studyUuid, - nodeId, - currentRootNetworkUuid, - SpreadsheetEquipmentType.BRANCH, - EQUIPMENT_INFOS_TYPES.TAB.type, - equipmentToUpdateId, - false - ) - ); - } - Promise.allSettled(promises).then((results) => { - const updates: UpdateEquipmentsAction['equipments'] = {}; - if (results[0].status === 'rejected') { - console.error( - `(re)loading of spreadsheet data of type ${type} ${results[0].status}`, - results[0].reason - ); - snackError({ - headerId: 'spreadsheet/loading/error_fetching_type_title', - headerValues: { type }, - messageTxt: `Details: ${results[0].reason}`, - }); - } else { - updates[type] = results[0].value; - } - if (results.length > 1) { - if (results[1].status === 'rejected') { - console.error( - `(re)loading of spreadsheet data of type ${type} ${results[1].status}`, - results[1].reason - ); - snackError({ - headerId: 'spreadsheet/loading/error_fetching_type_title', - headerValues: { type: SpreadsheetEquipmentType.BRANCH }, - messageTxt: `Details: ${results[1].reason}`, - }); - } else { - updates[SpreadsheetEquipmentType.BRANCH] = results[1].value; - } - } - if (Object.keys(updates).length > 1) { - highlightUpdatedEquipment(); - dispatch(updateEquipments(updates, nodeId)); - } - }); - } - } - if (deletedEquipments.length > 0) { - const equipmentsToDelete = deletedEquipments - .filter(({ equipmentType, equipmentId }) => equipmentType && equipmentId) - .map(({ equipmentType, equipmentId }) => { - console.info( - 'removing equipment with id=', - equipmentId, - ' and type=', - equipmentType, - ' from the network' - ); - return { equipmentType, equipmentId }; - }); - - if (equipmentsToDelete.length > 0) { - const equipmentsToDeleteArray = equipmentsToDelete - .filter((e) => isSpreadsheetEquipmentType(e.equipmentType)) - .map((equipment) => ({ - equipmentType: equipment.equipmentType as unknown as SpreadsheetEquipmentType, - equipmentId: equipment.equipmentId, - })); - dispatch(deleteEquipments(equipmentsToDeleteArray, nodeId)); - } - } - }, - [ - type, - currentNode?.id, - studyUuid, - currentRootNetworkUuid, - dispatch, - allEquipments, - equipmentToUpdateId, - highlightUpdatedEquipment, - snackError, - ] - ); - - const listenerUpdateEquipmentsLocal = useCallback( - (event: MessageEvent) => { - const eventData = JSON.parse(event.data); - if (isStudyNotification(eventData)) { - const eventStudyUuid = eventData.headers.studyUuid; - const eventNodeUuid = eventData.headers.node; - const eventRootNetworkUuid = eventData.headers.rootNetworkUuid; - if ( - studyUuid === eventStudyUuid && - currentNode?.id === eventNodeUuid && - currentRootNetworkUuid === eventRootNetworkUuid - ) { - const payload = JSON.parse(eventData.payload) as NetworkImpactsInfos; - const impactedSubstationsIds = payload.impactedSubstationsIds; - const deletedEquipments = payload.deletedEquipments; - const impactedElementTypes = payload.impactedElementTypes ?? []; - updateEquipmentsLocal(impactedSubstationsIds, deletedEquipments, impactedElementTypes); - } - } - }, - [currentNode?.id, currentRootNetworkUuid, studyUuid, updateEquipmentsLocal] - ); - - useNotificationsListener(NotificationsUrlKeys.STUDY, { - listenerCallbackMessage: listenerUpdateEquipmentsLocal, - propsId: SPREADSHEET_EQUIPMENTS_LISTENER_ID, - }); + }, [active, dispatch, nodesIdsToRemove]); // Note: take care about the dependencies because any execution here implies equipment loading (large fetches). // For example, we have 3 currentNode properties in deps rather than currentNode object itself. useEffect(() => { - if ( - active && - currentNode?.id && - currentRootNetworkUuid && - nodesIdToFetch.size > 0 && - isNetworkModificationTreeModelUpToDate && - (currentNode?.type === NodeType.ROOT || isStatusBuilt(currentNode?.data.globalBuildStatus)) - ) { + if (active && currentNode?.id && currentRootNetworkUuid && nodesIdsToFetch.size > 0) { setIsFetching(true); equipmentsWithLoadingOptionsLoaded(); - fetchNodesEquipmentData(nodesIdToFetch, currentNode.id, currentRootNetworkUuid, () => setIsFetching(false)); + fetchNodesEquipmentData(nodesIdsToFetch, currentNode.id, currentRootNetworkUuid, () => + setIsFetching(false) + ); } }, [ active, - isNetworkModificationTreeModelUpToDate, currentNode?.id, - currentNode?.type, - currentNode?.data.globalBuildStatus, currentRootNetworkUuid, - fetchNodesEquipmentData, - nodesIdToFetch, equipmentsWithLoadingOptionsLoaded, + fetchNodesEquipmentData, + nodesIdsToFetch, ]); return { equipments, isFetching }; diff --git a/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/spreadsheet-content.tsx b/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/spreadsheet-content.tsx index aace3c58b1..a2067ab8f8 100644 --- a/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/spreadsheet-content.tsx +++ b/src/components/spreadsheet-view/spreadsheet/spreadsheet-content/spreadsheet-content.tsx @@ -6,7 +6,6 @@ */ import { memo, type RefObject, useCallback, useEffect, useMemo, useState } from 'react'; -import { useSpreadsheetEquipments } from './hooks/use-spreadsheet-equipments'; import { EquipmentTable } from './equipment-table'; import { type Identifiable, type MuiStyles } from '@gridsuite/commons-ui'; import { type CustomColDef } from 'components/custom-aggrid/custom-aggrid-filters/custom-aggrid-filter.type'; @@ -25,6 +24,7 @@ import { useGridCalculations } from 'components/spreadsheet-view/spreadsheet/spr import { useColumnManagement } from './hooks/use-column-management'; import { DiagramType } from 'components/grid-layout/cards/diagrams/diagram.type'; import { type RowDataUpdatedEvent } from 'ag-grid-community'; +import { useSpreadsheetEquipments } from './hooks/use-spreadsheet-equipments'; const styles = { table: (theme) => ({ @@ -48,7 +48,7 @@ interface SpreadsheetContentProps { currentNode: CurrentTreeNode; tableDefinition: SpreadsheetTabDefinition; columns: CustomColDef[]; - nodeAliases: NodeAlias[] | undefined; + nodeAliases: NodeAlias[]; disabled: boolean; equipmentId: string | null; onEquipmentScrolled: () => void; @@ -71,36 +71,10 @@ export const SpreadsheetContent = memo( openDiagram, active, }: SpreadsheetContentProps) => { - const [equipmentToUpdateId, setEquipmentToUpdateId] = useState(null); const [isGridReady, setIsGridReady] = useState(false); - const highlightUpdatedEquipment = useCallback(() => { - if (!equipmentToUpdateId) { - return; - } - - const api = gridRef.current?.api; - const rowNode = api?.getRowNode(equipmentToUpdateId); - - if (rowNode && api) { - api.flashCells({ - rowNodes: [rowNode], - flashDuration: 1000, - }); - api.setNodesSelected({ nodes: [rowNode], newValue: false }); - } - - setEquipmentToUpdateId(null); - }, [equipmentToUpdateId, gridRef]); - // Only fetch when active - const { equipments, isFetching } = useSpreadsheetEquipments( - tableDefinition?.type, - equipmentToUpdateId, - highlightUpdatedEquipment, - nodeAliases, - active - ); + const { equipments, isFetching } = useSpreadsheetEquipments(tableDefinition?.type, nodeAliases, active); const { onModelUpdated } = useGridCalculations(gridRef, tableDefinition.uuid, columns); @@ -207,7 +181,6 @@ export const SpreadsheetContent = memo( const handleModify = useCallback( (equipmentId: string) => { - setEquipmentToUpdateId(equipmentId); handleOpenModificationDialog(equipmentId); }, [handleOpenModificationDialog] diff --git a/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/nodes-config-button.tsx b/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/nodes-config-button.tsx index 7eb4e49512..19df1804ac 100644 --- a/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/nodes-config-button.tsx +++ b/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/nodes-config-button.tsx @@ -14,7 +14,6 @@ import { SpreadsheetEquipmentType } from '../../../types/spreadsheet.type'; import type { NodeAlias } from '../../../types/node-alias.type'; import NodesConfigDialog from './nodes-config-dialog'; import { PolylineOutlined } from '@mui/icons-material'; -import { useNodeConfigNotificationsListener } from './use-node-config-notifications-listener'; const styles = { badgeStyle: (theme) => ({ @@ -50,9 +49,6 @@ export default function NodesConfigButton({ [nodeAliases] ); - //Enables to automatically reload nodeAliases data upon receiving study notification related to node and rootNetwork update - useNodeConfigNotificationsListener(tableType, nodeAliases); - const badgeText = useMemo(() => { if (nodeAliases?.length && !showWarning) { return nodeAliases.length; diff --git a/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/use-node-config-notifications-listener.ts b/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/use-node-config-notifications-listener.ts deleted file mode 100644 index 1b268dc28f..0000000000 --- a/src/components/spreadsheet-view/spreadsheet/spreadsheet-toolbar/nodes-config/use-node-config-notifications-listener.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright © 2025, 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 { useCallback, useMemo } from 'react'; -import { validAlias } from '../../../hooks/use-node-aliases'; -import { ROOT_NODE_LABEL } from '../../../../../constants/node.constant'; -import { NotificationsUrlKeys, useNotificationsListener } from '@gridsuite/commons-ui'; -import { isStudyNotification } from '../../../../../types/notification-types'; -import { NodeType } from '../../../../graph/tree-node.type'; -import { isStatusBuilt } from '../../../../graph/util/model-functions'; -import { useSelector } from 'react-redux'; -import { AppState } from '../../../../../redux/reducer'; -import { useFetchEquipment } from '../../../hooks/use-fetch-equipment'; -import { SpreadsheetEquipmentType } from '../../../types/spreadsheet.type'; -import { NodeAlias } from '../../../types/node-alias.type'; - -export function useNodeConfigNotificationsListener( - tableType: SpreadsheetEquipmentType, - nodeAliases: NodeAlias[] | undefined -) { - const currentNode = useSelector((state: AppState) => state.currentTreeNode); - const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); - const treeNodes = useSelector((state: AppState) => state.networkModificationTreeModel?.treeNodes); - const { fetchNodesEquipmentData } = useFetchEquipment(tableType); - - const isBuilt = useCallback( - (nodeId: string | undefined) => - treeNodes?.find( - (node) => - node.id === nodeId && (node.type === NodeType.ROOT || isStatusBuilt(node.data?.globalBuildStatus)) - ) !== undefined, - [treeNodes] - ); - - const nodesToReload = useMemo(() => { - // Get all valid aliased nodes ids, except for Root and current node (both are always up-to-date), and only the built ones - return nodeAliases?.filter( - (nodeAlias) => - validAlias(nodeAlias) && - nodeAlias.id !== currentNode?.id && - nodeAlias.name !== ROOT_NODE_LABEL && - isBuilt(nodeAlias.id) - ); - }, [currentNode?.id, isBuilt, nodeAliases]); - - useNotificationsListener(NotificationsUrlKeys.STUDY, { - listenerCallbackMessage: (event: MessageEvent) => { - const eventData = JSON.parse(event.data); - if (!isStudyNotification(eventData) || !currentNode?.id) { - return; - } - const nodeId = eventData.headers.node; - if ( - currentRootNetworkUuid === eventData.headers.rootNetworkUuid && - nodesToReload?.find((alias) => alias.id === nodeId) - ) { - fetchNodesEquipmentData(new Set([nodeId]), currentNode.id, currentRootNetworkUuid); - } - }, - }); -} diff --git a/src/components/spreadsheet-view/spreadsheet/spreadsheet.tsx b/src/components/spreadsheet-view/spreadsheet/spreadsheet.tsx index f1dc34b42b..90783ec59b 100644 --- a/src/components/spreadsheet-view/spreadsheet/spreadsheet.tsx +++ b/src/components/spreadsheet-view/spreadsheet/spreadsheet.tsx @@ -22,7 +22,7 @@ interface SpreadsheetProps { currentNode: CurrentTreeNode; tableDefinition: SpreadsheetTabDefinition; disabled: boolean; - nodeAliases: NodeAlias[] | undefined; + nodeAliases: NodeAlias[]; equipmentId: string | null; onEquipmentScrolled: () => void; openDiagram?: (equipmentId: string, diagramType?: DiagramType.SUBSTATION | DiagramType.VOLTAGE_LEVEL) => void; diff --git a/src/components/study-container.jsx b/src/components/study-container.jsx index ea6a197e4c..eee9f4cf14 100644 --- a/src/components/study-container.jsx +++ b/src/components/study-container.jsx @@ -15,7 +15,6 @@ import { closeStudy, loadNetworkModificationTreeSuccess, openStudy, - resetEquipments, resetEquipmentsPostComputation, setCurrentRootNetworkUuid, setCurrentTreeNode, @@ -29,7 +28,7 @@ import { fetchRootNetworks } from 'services/root-network'; import WaitingLoader from './utils/waiting-loader'; import { NotificationsUrlKeys, useIntlRef, useNotificationsListener, useSnackMessage } from '@gridsuite/commons-ui'; import NetworkModificationTreeModel from './graph/network-modification-tree-model'; -import { getFirstNodeOfType, isNodeBuilt, isNodeEdited, isSameNode } from './graph/util/model-functions'; +import { getFirstNodeOfType } from './graph/util/model-functions'; import { BUILD_STATUS } from './network/constants'; import { useAllComputingStatus } from './computing-status/use-all-computing-status'; import { fetchNetworkModificationTree } from '../services/study/tree-subtree'; @@ -486,33 +485,6 @@ export function StudyContainer({ view, onChangeTab }) { } }, [studyUpdatedForce, dispatch]); - //handles map automatic mode network reload - useEffect(() => { - let previousCurrentNode = currentNodeRef.current; - currentNodeRef.current = currentNode; - let previousCurrentRootNetworkUuid = currentRootNetworkUuidRef.current; - // this is the last effect to compare currentRootNetworkUuid and currentRootNetworkUuidRef.current - // then we can update the currentRootNetworkUuidRef.current - currentRootNetworkUuidRef.current = currentRootNetworkUuid; - // if only node renaming, do not reload network - if (isNodeEdited(previousCurrentNode, currentNode)) { - return; - } - if (!isNodeBuilt(currentNode)) { - return; - } - // A modification has been added to the currentNode and this one has been built incrementally. - // No need to load the network because reloadImpactedSubstationsEquipments will be executed in the notification useEffect. - if ( - previousCurrentRootNetworkUuid === currentRootNetworkUuid && - isSameNode(previousCurrentNode, currentNode) && - isNodeBuilt(previousCurrentNode) - ) { - return; - } - dispatch(resetEquipments()); - }, [currentNode, currentRootNetworkUuid, dispatch]); - useEffect(() => { if (studyUuid) { websocketExpectedCloseRef.current = false; diff --git a/src/hooks/use-stable-computed-set.ts b/src/hooks/use-stable-computed-set.ts new file mode 100644 index 0000000000..2296326292 --- /dev/null +++ b/src/hooks/use-stable-computed-set.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, 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 { DependencyList, useMemo, useRef } from 'react'; + +export function useStableComputedSet(compute: () => Iterable, deps: DependencyList): Set { + const prevRef = useRef>(new Set()); + + return useMemo(() => { + const computed = compute(); + const newSet = new Set(computed); + const prevSet = prevRef.current; + + const hasChanged = newSet.size !== prevSet.size || [...newSet].some((v) => !prevSet.has(v)); + + if (hasChanged) { + prevRef.current = newSet; + } + + return prevRef.current; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [deps]); +} diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index b89b029ca9..7b922fc972 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -1533,11 +1533,10 @@ export const reducer = createReducer(initialState, (builder) => { const currentEquipment: Record | undefined = state.spreadsheetNetwork[equipmentType]?.equipmentsByNodeId[action.nodeId]; - // Format the updated equipments to match the table format - const formattedEquipments = mapSpreadsheetEquipments(equipmentType, updatedEquipments); - // if the equipments are not loaded into the store yet, we don't have to update them - if (currentEquipment !== undefined) { + if (currentEquipment) { + // Format the updated equipments to match the table format + const formattedEquipments = mapSpreadsheetEquipments(equipmentType, updatedEquipments); //since substations data contains voltage level ones, they have to be treated separately if (equipmentType === SpreadsheetEquipmentType.SUBSTATION) { const [updatedSubstations, updatedVoltageLevels] = updateSubstationsAndVoltageLevels( @@ -1568,24 +1567,27 @@ export const reducer = createReducer(initialState, (builder) => { builder.addCase(DELETE_EQUIPMENTS, (state, action: DeleteEquipmentsAction) => { action.equipments.forEach(({ equipmentType: equipmentToDeleteType, equipmentId: equipmentToDeleteId }) => { - const currentEquipments = - state.spreadsheetNetwork[equipmentToDeleteType]?.equipmentsByNodeId[action.nodeId]; - if (currentEquipments !== undefined) { - // in case of voltage level deletion, we need to update the linked substation which contains a list of its voltage levels - if (equipmentToDeleteType === SpreadsheetEquipmentType.VOLTAGE_LEVEL) { - const currentSubstations = state.spreadsheetNetwork[SpreadsheetEquipmentType.SUBSTATION] - .equipmentsByNodeId[action.nodeId] as Record | null; - if (currentSubstations != null) { - state.spreadsheetNetwork[SpreadsheetEquipmentType.SUBSTATION].equipmentsByNodeId[ - action.nodeId - ] = updateSubstationAfterVLDeletion(currentSubstations, equipmentToDeleteId); - } + if ( + // If we delete a line or a two windings transformer we also have to delete it from branch type + (equipmentToDeleteType === SpreadsheetEquipmentType.LINE || + equipmentToDeleteType === SpreadsheetEquipmentType.TWO_WINDINGS_TRANSFORMER) && + state.spreadsheetNetwork[SpreadsheetEquipmentType.BRANCH]?.equipmentsByNodeId[action.nodeId] + ) { + delete state.spreadsheetNetwork[SpreadsheetEquipmentType.BRANCH].equipmentsByNodeId[action.nodeId][ + equipmentToDeleteId + ]; + } else if (equipmentToDeleteType === SpreadsheetEquipmentType.VOLTAGE_LEVEL) { + const currentSubstations = state.spreadsheetNetwork[SpreadsheetEquipmentType.SUBSTATION] + .equipmentsByNodeId[action.nodeId] as Record | null; + if (currentSubstations != null) { + state.spreadsheetNetwork[SpreadsheetEquipmentType.SUBSTATION].equipmentsByNodeId[action.nodeId] = + updateSubstationAfterVLDeletion(currentSubstations, equipmentToDeleteId); } - - state.spreadsheetNetwork[equipmentToDeleteType].equipmentsByNodeId[action.nodeId] = deleteEquipment( - currentEquipments, + } + if (state.spreadsheetNetwork[equipmentToDeleteType]?.equipmentsByNodeId[action.nodeId]) { + delete state.spreadsheetNetwork[equipmentToDeleteType].equipmentsByNodeId[action.nodeId][ equipmentToDeleteId - ); + ]; } }); }); @@ -1951,11 +1953,6 @@ function updateSubstationAfterVLDeletion( return currentSubstations; } -function deleteEquipment(currentEquipments: Record, equipmentToDeleteId: string) { - delete currentEquipments[equipmentToDeleteId]; - return currentEquipments; -} - export type Substation = Identifiable & { voltageLevels: Identifiable[]; };