diff --git a/src/components/app-top-bar.jsx b/src/components/app-top-bar.jsx index 791e32247e..14f7b29e95 100644 --- a/src/components/app-top-bar.jsx +++ b/src/components/app-top-bar.jsx @@ -22,14 +22,10 @@ import { PARAM_USE_NAME } from '../utils/config-params'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import AppPackage from '../../package.json'; -import { isNodeBuilt, isNodeReadOnly } from './graph/util/model-functions'; import { getServersInfos } from '../services/study'; import { fetchVersion } from '../services/utils'; -import { RunButtonContainer } from './run-button-container'; import { useParameterState } from './dialogs/parameters/use-parameters-state'; -import StudyNavigationSyncToggle from './study-navigation-sync-toggle'; import { WorkspaceToolbar } from './workspace/core/workspace-toolbar'; -import { WorkspaceSwitcher } from './workspace/core/workspace-switcher'; const styles = { boxContent: (theme) => ({ @@ -39,23 +35,12 @@ const styles = { width: '100%', marginLeft: theme.spacing(1), }), - runButtonContainer: { - display: 'flex', - alignItems: 'center', - marginRight: 1.5, - }, - syncToggleContainer: { - display: 'flex', - alignItems: 'center', - marginRight: 1.5, - }, }; const AppTopBar = ({ userProfile, userManager }) => { const dispatch = useDispatch(); const theme = useSelector((state) => state[PARAM_THEME]); const studyUuid = useSelector((state) => state.studyUuid); - const currentNode = useSelector((state) => state.currentTreeNode); const currentRootNetworkUuid = useSelector((state) => state.currentRootNetworkUuid); const [appsAndUrls, setAppsAndUrls] = useState([]); @@ -97,23 +82,7 @@ const AppTopBar = ({ userProfile, userManager }) => { > {userProfile && studyUuid && currentRootNetworkUuid && ( - - - - - - - - - - - - + )} diff --git a/src/components/breadcrumbs/current-selection.tsx b/src/components/breadcrumbs/current-selection.tsx new file mode 100644 index 0000000000..8ffe36bc2d --- /dev/null +++ b/src/components/breadcrumbs/current-selection.tsx @@ -0,0 +1,96 @@ +/** + * 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 { Box, Paper, Tooltip } from '@mui/material'; +import { BuildStatusChip, type MuiStyles } from '@gridsuite/commons-ui'; +import type { UUID } from 'node:crypto'; +import { useIntl } from 'react-intl'; +import { useSelector } from 'react-redux'; +import { CurrentTreeNode, NodeType } from '../graph/tree-node.type'; +import { RootNetworkMetadata } from '../graph/menus/network-modifications/network-modification-menu.type'; +import { AppState } from '../../redux/reducer.type'; +import { BlockedByActivityIndicator, NodeActivityChip } from 'components/node-activity/node-activity-display'; +import { useNodeActivity } from 'components/node-activity/hooks/use-node-activity'; +import RootNetworkSelect from './root-network-select'; + +const styles = { + container: { + display: 'flex', + alignItems: 'center', + borderRadius: '8px', + minHeight: 32, + px: 1, + }, + label: (theme) => ({ + display: { xs: 'none', lg: 'block' }, + mr: 1, + fontSize: theme.typography.fontSize, + }), + rootNetworkSlot: { + display: 'flex', + alignItems: 'center', + mr: 1, + }, + nodeLabel: (theme) => ({ + maxWidth: 500, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: theme.typography.fontSize, + }), + chipSlot: { + display: 'flex', + alignItems: 'center', + ml: 1, + }, + blockedSpinner: { + ml: 1, + }, +} as const satisfies MuiStyles; + +export default function CurrentSelection() { + const intl = useIntl(); + const currentNode: CurrentTreeNode | null = useSelector((state: AppState) => state.currentTreeNode); + const currentRootNetworkUuid: UUID | null = useSelector((state: AppState) => state.currentRootNetworkUuid); + const rootNetworks: RootNetworkMetadata[] = useSelector((state: AppState) => state.rootNetworks); + const isRootNode = currentNode?.type === NodeType.ROOT; + const nodeLabel = isRootNode ? intl.formatMessage({ id: 'root' }) : currentNode?.data.label; + const activity = useNodeActivity(currentNode?.id); + + return ( + + {rootNetworks.length > 1 && ( + <> + {intl.formatMessage({ id: 'root' })} + + + + + )} + {intl.formatMessage({ id: 'node' })} + + {nodeLabel} + + + {activity && } + {!activity && currentNode && !isRootNode && ( + + )} + + + + ); +} diff --git a/src/components/breadcrumbs/root-network-select.tsx b/src/components/breadcrumbs/root-network-select.tsx index d2eb6f77c0..e6b7b2244b 100644 --- a/src/components/breadcrumbs/root-network-select.tsx +++ b/src/components/breadcrumbs/root-network-select.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Box, ListItemText, MenuItem, Select } from '@mui/material'; +import { Box, MenuItem, Select } from '@mui/material'; import type { UUID } from 'node:crypto'; import { RemoveRedEye, VisibilityOff } from '@mui/icons-material'; import { RootNetworkMetadata } from '../graph/menus/network-modifications/network-modification-menu.type'; @@ -15,12 +15,22 @@ import { mergeSx, type MuiStyles } from '@gridsuite/commons-ui'; const styles = { selectRoot: (theme) => ({ height: theme.spacing(4), - width: theme.spacing(15), + width: 'fit-content', paddingTop: theme.spacing(1), paddingBottom: theme.spacing(1), + '& .MuiOutlinedInput-notchedOutline': { border: 'none' }, + '& .MuiSelect-select.MuiSelect-select': { paddingLeft: 0, paddingRight: theme.spacing(3.5) }, + }), + selectInput: (theme) => ({ + display: 'flex', + gap: 1, + alignItems: 'center', + fontSize: theme.typography.fontSize, + }), + selectItem: (theme) => ({ + gap: 1, + fontSize: theme.typography.fontSize, }), - selectInput: { display: 'flex', gap: 1, alignItems: 'center' }, - selectItem: { gap: 1 }, hiddenItem: { display: 'none' }, } as const satisfies MuiStyles; @@ -45,8 +55,8 @@ export default function RootNetworkSelect({ currentRootNetworkUuid, rootNetworks const tag = rootNetworks.find((item) => item.rootNetworkUuid === value)?.tag; return ( - - + {tag} + ); }} @@ -60,8 +70,8 @@ export default function RootNetworkSelect({ currentRootNetworkUuid, rootNetworks item.rootNetworkUuid === currentRootNetworkUuid ? styles.hiddenItem : undefined )} > - - + {item.tag} + ))} diff --git a/src/components/breadcrumbs/study-path-breadcrumbs.tsx b/src/components/breadcrumbs/study-path-breadcrumbs.tsx index a8bf026a34..ccacc2a66e 100644 --- a/src/components/breadcrumbs/study-path-breadcrumbs.tsx +++ b/src/components/breadcrumbs/study-path-breadcrumbs.tsx @@ -6,16 +6,10 @@ */ import { MoreHoriz } from '@mui/icons-material'; -import { Box, Breadcrumbs as MuiBreadcrumbs, Tooltip } from '@mui/material'; +import { Box, Breadcrumbs as MuiBreadcrumbs, type Theme, Tooltip, useMediaQuery } from '@mui/material'; import { type MuiStyles } from '@gridsuite/commons-ui'; -import { CurrentTreeNode, NodeType } from '../graph/tree-node.type'; -import { useSelector } from 'react-redux'; -import { AppState } from '../../redux/reducer.type'; -import { RootNetworkMetadata } from '../graph/menus/network-modifications/network-modification-menu.type'; -import type { UUID } from 'node:crypto'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; -import RootNetworkSelect from './root-network-select'; -import { useIntl } from 'react-intl'; +import CurrentSelection from './current-selection'; const styles = { tooltipItem: { @@ -29,6 +23,12 @@ const styles = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, + parentDirectoryItem: { + maxWidth: 200, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, } as const satisfies MuiStyles; export interface StudyPathBreadcrumbsProps { @@ -40,13 +40,8 @@ export default function StudyPathBreadcrumbs({ studyName, parentDirectoriesNames, }: Readonly) { - const intl = useIntl(); - const currentNode: CurrentTreeNode | null = useSelector((state: AppState) => state.currentTreeNode); - const currentRootNetworkUuid: UUID | null = useSelector((state: AppState) => state.currentRootNetworkUuid); - const rootNetworks: RootNetworkMetadata[] = useSelector((state: AppState) => state.rootNetworks); - const currentRootNetworkTag = rootNetworks.find((item) => item.rootNetworkUuid === currentRootNetworkUuid)?.tag; - const isRootNode = currentNode?.type === NodeType.ROOT; - const nodeLabel = isRootNode ? intl.formatMessage({ id: 'root' }) : currentNode?.data.label; + const nearestParentDirectoryName = parentDirectoriesNames?.at(-1); + const showParentDirectory = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); return ( ))} - - {studyName} - - - {nodeLabel} - {rootNetworks?.length > 1 && ( - - - {currentRootNetworkTag} - - )} + {studyName} } slotProps={{ @@ -87,17 +72,15 @@ export default function StudyPathBreadcrumbs({ > + {showParentDirectory && nearestParentDirectoryName && ( + + {nearestParentDirectoryName} + + )} {studyName} - - {nodeLabel} - - {rootNetworks && rootNetworks.length > 1 && ( - - - - )} + ); } diff --git a/src/components/graph/menus/create-node-menu.tsx b/src/components/graph/menus/create-node-menu.tsx index 00ba2e5833..9c0c8f1ea1 100644 --- a/src/components/graph/menus/create-node-menu.tsx +++ b/src/components/graph/menus/create-node-menu.tsx @@ -8,11 +8,10 @@ import { useCallback, useState } from 'react'; import Menu from '@mui/material/Menu'; import { useIntl } from 'react-intl'; -import { useIsAnyNodeBuilding } from '../../utils/is-any-node-building-hook'; import { useSelector } from 'react-redux'; import ChildMenuItem from './create-child-menu-item'; import { CustomDialog } from '../../utils/custom-dialog'; -import { CustomNestedMenuItem, PARAM_DEVELOPER_MODE, BuildStatus } from '@gridsuite/commons-ui'; +import { CustomNestedMenuItem, PARAM_DEVELOPER_MODE } from '@gridsuite/commons-ui'; import { type AppState, type NodeSelectionForCopy } from 'redux/reducer.type'; import type { UUID } from 'node:crypto'; import NetworkModificationTreeModel from '../network-modification-tree-model'; @@ -21,6 +20,8 @@ import { CurrentTreeNode, isSecurityModificationNode, NetworkModificationNodeTyp import { NodeInsertModes } from 'types/notification-types'; import { Divider } from '@mui/material'; import { useParameterState } from 'components/dialogs/parameters/use-parameters-state'; +import { useIsBuildBlocked, useIsEditBlocked } from 'components/node-activity/hooks/use-node-activity'; +import { isStatusBuilt } from '../util/model-functions'; type SubMenuItem = { onRoot: boolean; @@ -133,7 +134,9 @@ const CreateNodeMenu: React.FC = ({ disableRestoreNodes, }) => { const intl = useIntl(); - const isAnyNodeBuilding = useIsAnyNodeBuilding(); + const isActiveNodeEditBlocked = useIsEditBlocked(activeNode?.id); + const isClipboardSourceEditBlocked = useIsEditBlocked(nodeSelectionForCopy?.nodeId); + const isActiveNodeBuildBlocked = useIsBuildBlocked(activeNode?.id, activeNode?.data); const mapDataLoading = useSelector((state: AppState) => state.mapDataLoading); const treeModel = useSelector((state: AppState) => state.networkModificationTreeModel); const [isDeveloperMode] = useParameterState(PARAM_DEVELOPER_MODE); @@ -294,16 +297,16 @@ const CreateNodeMenu: React.FC = ({ return isConstructionInsertionForbidden || isSecurityInsertionForbidden; } - function isNodeRemovingAllowed() { - return !isAnyNodeBuilding && !mapDataLoading; + function isNodeRemovalDisabled() { + return isActiveNodeEditBlocked || mapDataLoading; } - function isNodeUnbuildingAllowed() { - return !isAnyNodeBuilding && !mapDataLoading && activeNode?.data?.globalBuildStatus?.startsWith('BUILT'); + function isNodeUnbuildDisabled() { + return isActiveNodeBuildBlocked || mapDataLoading || !isStatusBuilt(activeNode?.data?.globalBuildStatus); } - function isNodeRestorationAllowed() { - return !isAnyNodeBuilding && !disableRestoreNodes; + function isNodeRestorationDisabled() { + return disableRestoreNodes || isActiveNodeEditBlocked; } function isNodeAlreadySelectedForCut() { @@ -315,12 +318,39 @@ const CreateNodeMenu: React.FC = ({ nodeSelectionForCopy?.nodeId === activeNode.id && nodeSelectionForCopy?.copyType === CopyType.SUBTREE_CUT ); } + function isCutDisabled(isAlreadySelectedForCut: boolean) { + // cancelling a cut only clears the clipboard, so it stays available while the node is busy + return !isAlreadySelectedForCut && isActiveNodeEditBlocked; + } + + function isInsertionDisabled(insertMode: NodeInsertModes) { + // a new branch only appends a child : it leaves the active node and its subtree untouched + return insertMode !== NodeInsertModes.NewBranch && isActiveNodeEditBlocked; + } + + function isNodePasteDisabled(insertMode: NodeInsertModes) { + return ( + !isNodePastingAllowed() || + isNodeInsertionForbidden(insertMode) || + isInsertionDisabled(insertMode) || + // a cut moves the source node, so it has to be free too + (nodeSelectionForCopy?.copyType === CopyType.NODE_CUT && isClipboardSourceEditBlocked) + ); + } + + function isSubtreePasteDisabled() { + return ( + !isSubtreePastingAllowed() || + !isSubtreeContentPasteable() || + (nodeSelectionForCopy?.copyType === CopyType.SUBTREE_CUT && isClipboardSourceEditBlocked) + ); + } + function isNodeHasChildren(node: CurrentTreeNode, treeModel: NetworkModificationTreeModel | null): boolean { return treeModel?.treeNodes.some((item) => item.parentId === node.id) ?? false; } - function isSubtreeRemovingAllowed() { - // check if the subtree has children - return !isAnyNodeBuilding && !mapDataLoading && isNodeHasChildren(activeNode, treeModel); + function isSubtreeRemovalDisabled() { + return isActiveNodeEditBlocked || mapDataLoading || !isNodeHasChildren(activeNode, treeModel); } const SUBTREE_SUBMENU_ITEMS: Record = { @@ -328,7 +358,7 @@ const CreateNodeMenu: React.FC = ({ onRoot: false, action: () => copySubtree(), id: 'copyNetworkModificationSubtree', - disabled: isAnyNodeBuilding || !isNodeHasChildren(activeNode, treeModel), + disabled: !isNodeHasChildren(activeNode, treeModel), }, CUT_SUBTREE: { onRoot: false, @@ -336,20 +366,20 @@ const CreateNodeMenu: React.FC = ({ id: isSubtreeAlreadySelectedForCut() ? 'cancelCutNetworkModificationSubtree' : 'cutNetworkModificationSubtree', - disabled: isAnyNodeBuilding || !isNodeHasChildren(activeNode, treeModel), + disabled: isCutDisabled(isSubtreeAlreadySelectedForCut()) || !isNodeHasChildren(activeNode, treeModel), }, PASTE_SUBTREE: { onRoot: true, action: () => pasteSubtree(), id: 'pasteNetworkModificationSubtree', - disabled: !isSubtreePastingAllowed() || !isSubtreeContentPasteable(), + disabled: isSubtreePasteDisabled(), withDivider: activeNode?.type !== NodeType.ROOT, }, REMOVE_SUBTREE: { onRoot: false, action: () => removeSubtree(), id: 'removeNetworkModificationSubtree', - disabled: !isSubtreeRemovingAllowed(), + disabled: isSubtreeRemovalDisabled(), }, }; @@ -358,15 +388,13 @@ const CreateNodeMenu: React.FC = ({ onRoot: false, action: () => buildNode(), id: 'buildNode', - disabled: - activeNode?.data?.globalBuildStatus?.startsWith('BUILT') || - activeNode?.data?.globalBuildStatus === BuildStatus.BUILDING, + disabled: isStatusBuilt(activeNode?.data?.globalBuildStatus) || isActiveNodeBuildBlocked, }, UNBUILD_NODE: { onRoot: false, action: () => unbuildNode(), id: 'unbuildNode', - disabled: !isNodeUnbuildingAllowed(), + disabled: isNodeUnbuildDisabled(), withDivider: true, }, RENAME_NODE: { @@ -385,6 +413,7 @@ const CreateNodeMenu: React.FC = ({ action: () => isNodeAlreadySelectedForCut() ? cancelCutNetworkModificationNode() : cutNetworkModificationNode(), id: isNodeAlreadySelectedForCut() ? 'cancelCutNetworkModificationNode' : 'cutNetworkModificationNode', + disabled: isCutDisabled(isNodeAlreadySelectedForCut()), }, PASTE_MODIFICATION_NODE: { onRoot: true, @@ -395,19 +424,19 @@ const CreateNodeMenu: React.FC = ({ onRoot: true, action: () => pasteNetworkModificationNode(NodeInsertModes.NewBranch), id: 'insertNodeInNewBranch', - disabled: !isNodePastingAllowed() || isNodeInsertionForbidden(NodeInsertModes.NewBranch), + disabled: isNodePasteDisabled(NodeInsertModes.NewBranch), }, PASTE_BEFORE: { onRoot: false, action: () => pasteNetworkModificationNode(NodeInsertModes.Before), id: 'insertNodeAbove', - disabled: !isNodePastingAllowed() || isNodeInsertionForbidden(NodeInsertModes.Before), + disabled: isNodePasteDisabled(NodeInsertModes.Before), }, PASTE_AFTER: { onRoot: true, action: () => pasteNetworkModificationNode(NodeInsertModes.After), id: 'insertNodeBelow', - disabled: !isNodePastingAllowed() || isNodeInsertionForbidden(NodeInsertModes.After), + disabled: isNodePasteDisabled(NodeInsertModes.After), }, }, }, @@ -415,7 +444,7 @@ const CreateNodeMenu: React.FC = ({ onRoot: false, action: () => removeNode(), id: 'removeNode', - disabled: !isNodeRemovingAllowed(), + disabled: isNodeRemovalDisabled(), sectionEnd: true, withDivider: isSecurityModificationNode(activeNode), }, @@ -424,7 +453,7 @@ const CreateNodeMenu: React.FC = ({ hidden: isSecurityModificationNode(activeNode), action: () => restoreNodes(), id: 'restoreNodes', - disabled: !isNodeRestorationAllowed(), + disabled: isNodeRestorationDisabled(), withDivider: !isSecurityModificationNode(activeNode), }, @@ -441,18 +470,21 @@ const CreateNodeMenu: React.FC = ({ NetworkModificationNodeType.CONSTRUCTION ), id: 'insertNodeInNewBranch', + disabled: isInsertionDisabled(NodeInsertModes.NewBranch), }, INSERT_NODE_BEFORE: { onRoot: false, action: () => createNetworkModificationNode(NodeInsertModes.Before, NetworkModificationNodeType.CONSTRUCTION), id: 'insertNodeAbove', + disabled: isInsertionDisabled(NodeInsertModes.Before), }, INSERT_NODE_AFTER: { onRoot: true, action: () => createNetworkModificationNode(NodeInsertModes.After, NetworkModificationNodeType.CONSTRUCTION), id: 'insertNodeBelow', + disabled: isInsertionDisabled(NodeInsertModes.After), }, }, }, @@ -465,17 +497,18 @@ const CreateNodeMenu: React.FC = ({ action: () => createNetworkModificationNode(NodeInsertModes.NewBranch, NetworkModificationNodeType.SECURITY), id: 'insertNodeInNewBranch', + disabled: isInsertionDisabled(NodeInsertModes.NewBranch), }, INSERT_NODE_BEFORE: { onRoot: false, - disabled: !isSecurityModificationNode(activeNode), + disabled: !isSecurityModificationNode(activeNode) || isInsertionDisabled(NodeInsertModes.Before), action: () => createNetworkModificationNode(NodeInsertModes.Before, NetworkModificationNodeType.SECURITY), id: 'insertNodeAbove', }, INSERT_NODE_AFTER: { onRoot: true, - disabled: !isSecurityModificationNode(activeNode), + disabled: !isSecurityModificationNode(activeNode) || isInsertionDisabled(NodeInsertModes.After), action: () => createNetworkModificationNode(NodeInsertModes.After, NetworkModificationNodeType.SECURITY), id: 'insertNodeBelow', @@ -507,7 +540,7 @@ const CreateNodeMenu: React.FC = ({ onRoot: true, action: () => exportCaseOnNode(), id: 'exportCaseOnNode', - disabled: activeNode?.type !== NodeType.ROOT && !activeNode?.data?.globalBuildStatus?.startsWith('BUILT'), + disabled: activeNode?.type !== NodeType.ROOT && !isStatusBuilt(activeNode?.data?.globalBuildStatus), }, }; diff --git a/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx b/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx index bf8a5da789..65ed1342b9 100644 --- a/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx +++ b/src/components/graph/menus/dynamic-simulation/event-modification-scenario-editor.tsx @@ -14,13 +14,12 @@ import { useNotificationsListener, useSnackMessage, } from '@gridsuite/commons-ui'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { Box, Checkbox, CircularProgress, Toolbar, Typography } from '@mui/material'; import { FormattedMessage, useIntl } from 'react-intl'; import DeleteIcon from '@mui/icons-material/Delete'; import IconButton from '@mui/material/IconButton'; -import { useIsAnyNodeBuilding } from '../../../utils/is-any-node-building-hook'; -import { addNotification, removeNotificationByNode, setModificationsInProgress } from '../../../../redux/actions'; +import { useIsEventEditBlocked, useIsNodeUpdating } from 'components/node-activity/hooks/use-node-activity'; import type { UUID } from 'node:crypto'; import { Event, EventType } from '../../../dialogs/dynamicsimulation/event/types/event.type'; import { DynamicSimulationEventDialog } from '../../../dialogs/dynamicsimulation/event/dynamic-simulation-event-dialog'; @@ -29,17 +28,7 @@ import { isChecked, isPartial, styles } from '../network-modifications/network-m import { EQUIPMENT_TYPE_LABEL_KEYS } from '../../util/model-constants'; import EditIcon from '@mui/icons-material/Edit'; import { AppState } from '../../../../redux/reducer.type'; -import { AppDispatch } from '../../../../redux/store'; -import { - EventCreatingInProgressEventData, - EventDeletingInProgressEventData, - EventUpdatingInProgressEventData, - isEventCrudFinishedNotification, - isEventNotification, - NotificationType, - parseEventData, - CommonStudyEventData, -} from 'types/notification-types'; +import { isEventCrudFinishedNotification, parseEventData, CommonStudyEventData } from 'types/notification-types'; import { deleteDynamicSimulationEvents, fetchDynamicSimulationEvents, @@ -56,14 +45,12 @@ const paperStyles = { const EventModificationScenarioEditor = memo(() => { const intl = useIntl(); - const notificationIdList = useSelector((state: AppState) => state.notificationIdList); const studyUuid = useSelector((state: AppState) => state.studyUuid); const { snackError } = useSnackMessage(); const [events, setEvents] = useState([]); const currentNode = useSelector((state: AppState) => state.currentTreeNode); const currentNodeIdRef = useRef(null); // initial empty to get first update - const [pendingState, setPendingState] = useState(false); const [selectedItems, setSelectedItems] = useState([]); @@ -76,47 +63,12 @@ const EventModificationScenarioEditor = memo(() => { | undefined >(); - const dispatch = useDispatch(); - const [messageId, setMessageId] = useState(''); const [launchLoader, setLaunchLoader] = useState(false); const handleCloseDialog = () => { setEditDialogOpen(undefined); }; - const fillNotification = useCallback( - ( - eventData: - EventCreatingInProgressEventData | EventUpdatingInProgressEventData | EventDeletingInProgressEventData, - messageId: string - ) => { - // (work for all users) - // specific message id for each action type - setMessageId(messageId); - - dispatch(addNotification([eventData.headers.parentNode, ...(eventData.headers.nodes ?? [])])); - }, - [dispatch] - ); - - const manageNotification = useCallback( - ( - eventData: - EventCreatingInProgressEventData | EventUpdatingInProgressEventData | EventDeletingInProgressEventData - ) => { - let messageId = ''; - if (eventData.headers.updateType === NotificationType.EVENT_CREATING_IN_PROGRESS) { - messageId = 'DynamicSimulationEventCreating'; - } else if (eventData.headers.updateType === NotificationType.EVENT_UPDATING_IN_PROGRESS) { - messageId = 'DynamicSimulationEventUpdating'; - } else if (eventData.headers.updateType === NotificationType.EVENT_DELETING_IN_PROGRESS) { - messageId = 'DynamicSimulationEventDeleting'; - } - fillNotification(eventData, messageId); - }, - [fillNotification] - ); - const updateSelectedItems = useCallback((events: Event[]) => { const toKeepIdsSet = new Set(events.map((e) => e.uuid)); setSelectedItems((oldselectedItems) => oldselectedItems.filter((s) => toKeepIdsSet.has(s.uuid))); @@ -143,11 +95,9 @@ const EventModificationScenarioEditor = memo(() => { snackWithFallback(snackError, error); }) .finally(() => { - setPendingState(false); setLaunchLoader(false); - dispatch(setModificationsInProgress(false)); }); - }, [currentNode?.type, currentNode?.id, studyUuid, updateSelectedItems, snackError, dispatch]); + }, [currentNode?.type, currentNode?.id, studyUuid, updateSelectedItems, snackError]); useEffect(() => { // first time with currentNode initialized then fetch events @@ -167,32 +117,21 @@ const EventModificationScenarioEditor = memo(() => { if (!eventData) { return; } - if (isEventNotification(eventData)) { - if (currentNodeIdRef.current !== eventData.headers.parentNode) { - return; - } - - dispatch(setModificationsInProgress(true)); - setPendingState(true); - manageNotification(eventData); - } else if (isEventCrudFinishedNotification(eventData)) { - // notify finished action (success or error => we remove the loader) - // error handling in dialog for each equipment (snackbar with specific error showed only for current user) - // fetch events because it must have changed - // Do not clear the events list, because currentNode is the concerned one - // this allows to append new events to the existing list. + // success or error, the events may have changed : the spinner is driven by the node activity + if (isEventCrudFinishedNotification(eventData)) { + // append to the existing list : currentNode is the concerned one, so it is not cleared doFetchEvents(); - dispatch(removeNotificationByNode([eventData.headers.parentNode, ...(eventData.headers.nodes ?? [])])); } }, - [dispatch, doFetchEvents, manageNotification] + [doFetchEvents] ); useNotificationsListener(NotificationsUrlKeys.STUDY, { listenerCallbackMessage: handleEvent, }); - const isAnyNodeBuilding = useIsAnyNodeBuilding(); + const isEventEditBlocked = useIsEventEditBlocked(currentNode?.id); + const isNodeUpdating = useIsNodeUpdating(currentNode?.id); const doDeleteEvent = useCallback(() => { if (!studyUuid || !currentNode?.id) { @@ -216,10 +155,6 @@ const EventModificationScenarioEditor = memo(() => { setSelectedItems((oldVals: Event[]) => (oldVals.length === 0 ? events : [])); }, [events]); - const isLoading = useCallback(() => { - return notificationIdList.filter((notification) => notification === currentNode?.id).length > 0; - }, [currentNode?.id, notificationIdList]); - const getItemLabel = (item: Event) => { if (!studyUuid || !currentNode || !item) { return ''; @@ -246,17 +181,12 @@ const EventModificationScenarioEditor = memo(() => { const handleSecondaryAction = useCallback( (item: Event, isItemHovered?: boolean) => - isItemHovered && !isAnyNodeBuilding ? ( - doEditEvent(item)} - size={'small'} - sx={styles.iconEdit} - disabled={isLoading()} - > + isItemHovered && !isEventEditBlocked ? ( + doEditEvent(item)} size={'small'} sx={styles.iconEdit}> ) : null, - [isAnyNodeBuilding, isLoading] + [isEventEditBlocked] ); const renderEventList = () => { @@ -277,25 +207,12 @@ const EventModificationScenarioEditor = memo(() => { getItemId={(v: Event) => v.equipmentId} getItemLabel={getItemLabel} secondaryAction={handleSecondaryAction} - isDisabled={() => isLoading()} + isDisabled={() => isEventEditBlocked} divider /> ); }; - const renderEventListTitleLoading = () => { - return ( - - - - - - - - - ); - }; - const renderEventListTitleUpdating = () => { return ( @@ -313,14 +230,14 @@ const EventModificationScenarioEditor = memo(() => { return ( - {pendingState && } + {isNodeUpdating && } @@ -329,9 +246,6 @@ const EventModificationScenarioEditor = memo(() => { }; const renderPaneSubtitle = () => { - if (isLoading() && messageId) { - return renderEventListTitleLoading(); - } if (launchLoader) { return renderEventListTitleUpdating(); } @@ -354,7 +268,7 @@ const EventModificationScenarioEditor = memo(() => { diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor-utils.ts b/src/components/graph/menus/network-modifications/network-modification-node-editor-utils.ts index 5b2a87f8fd..f32954b45c 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor-utils.ts +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor-utils.ts @@ -67,14 +67,6 @@ export const styles = { marginRight: theme.spacing(2), color: theme.palette.primary.main, }), - toolbarCircularProgress: (theme) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginLeft: theme.spacing(1.25), - marginRight: theme.spacing(2), - color: theme.palette.secondary.main, - }), notification: (theme) => ({ flex: 1, alignContent: 'center', diff --git a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx index f4ede0f30d..534dc98318 100644 --- a/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx +++ b/src/components/graph/menus/network-modifications/network-modification-node-editor.tsx @@ -35,7 +35,7 @@ import ContentCutIcon from '@mui/icons-material/ContentCut'; import ContentPasteIcon from '@mui/icons-material/ContentPaste'; import DeleteIcon from '@mui/icons-material/Delete'; import SaveIcon from '@mui/icons-material/Save'; -import { Alert, Box, CircularProgress, Toolbar, Tooltip } from '@mui/material'; +import { Alert, Box, Divider, Toolbar, Tooltip } from '@mui/material'; import IconButton from '@mui/material/IconButton'; import BatteryCreationDialog from 'components/dialogs/network-modifications/battery/creation/battery-creation-dialog'; @@ -73,14 +73,13 @@ import NetworkModificationsMenu from 'components/graph/menus/network-modificatio import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import { - addNotification, - removeNotificationByNode, - setHighlightModification, - setModificationsInProgress, -} from '../../../../redux/actions'; +import { setHighlightModification } from '../../../../redux/actions'; import TwoWindingsTransformerModificationDialog from '../../../dialogs/network-modifications/two-windings-transformer/modification/two-windings-transformer-modification-dialog'; -import { useIsAnyNodeBuilding } from '../../../utils/is-any-node-building-hook'; +import { + useIsBuildBlocked, + useIsEditBlocked, + useIsNodeUpdating, +} from 'components/node-activity/hooks/use-node-activity'; import { FileUpload, RestoreFromTrash } from '@mui/icons-material'; @@ -115,13 +114,6 @@ import { isModificationsDeleteFinishedNotification, isModificationsUpdateFinishedNotification, isNodeDeletedNotification, - isPendingModificationNotification, - ModificationsCreationInProgressEventData, - ModificationsDeletingInProgressEventData, - ModificationsRestoringInProgressEventData, - ModificationsStashingInProgressEventData, - ModificationsUpdatingInProgressEventData, - NotificationType, parseEventData, } from 'types/notification-types'; import { LccModificationDialog } from '../../../dialogs/network-modifications/hvdc-line/lcc/modification/lcc-modification-dialog'; @@ -130,6 +122,7 @@ import CreateCouplingDeviceDialog from '../../../dialogs/network-modifications/c import { BalancesAdjustmentDialog } from '../../../dialogs/network-modifications/balances-adjustment/balances-adjustment-dialog'; import CreateVoltageLevelTopologyDialog from '../../../dialogs/network-modifications/voltage-level/topology-creation/create-voltage-level-topology-dialog'; import { NodeType } from 'components/graph/tree-node.type'; +import { BuildButton } from 'components/graph/nodes/build-button'; import { LimitSetsModificationDialog } from '../../../dialogs/network-modifications/limit-sets/limit-sets-modification-dialog'; import CreateVoltageLevelSectionDialog from '../../../dialogs/network-modifications/voltage-level/section/create-voltage-level-section-dialog'; import MoveVoltageLevelFeederBaysDialog from '../../../dialogs/network-modifications/voltage-level/move-feeder-bays/move-voltage-level-feeder-bays-dialog'; @@ -146,6 +139,10 @@ const nonEditableModificationTypes = new Set([ 'MODIFICATION_REFERENCE', ]); +// commons-ui only spins on this when paired with notificationMessageId, which the node activity spinner +// replaced : the prop is required, so it stays pinned off until commons-ui drops it. +const NEVER_IMPACTED_BY_NOTIFICATION = () => false; + const isEditableModification = (modif: NetworkModificationMetadata) => { if (!modif) { return false; @@ -154,7 +151,6 @@ const isEditableModification = (modif: NetworkModificationMetadata) => { }; const NetworkModificationNodeEditor = () => { - const notificationIdList = useSelector((state: AppState) => state.notificationIdList); const studyUuid = useSelector((state: AppState) => state.studyUuid); const rootNetworks = useSelector((state: AppState) => state.rootNetworks); const createdRootNetworks = rootNetworks.filter((rn) => !rn.isCreating); @@ -165,7 +161,6 @@ const NetworkModificationNodeEditor = () => { const [modifications, setModifications] = useState([]); const [modificationsToExclude, setModificationsToExclude] = useState([]); const [saveInProgress, setSaveInProgress] = useState(false); - const [deleteInProgress, setDeleteInProgress] = useState(false); const [modificationsToRestore, setModificationsToRestore] = useState([]); const currentNode = useSelector((state: AppState) => state.currentTreeNode); const isRootNode = currentNode?.type === NodeType.ROOT; @@ -173,7 +168,6 @@ const NetworkModificationNodeEditor = () => { const isMonoRootStudy = useSelector((state: AppState) => state.isMonoRootStudy); const currentNodeIdRef = useRef(null); // initial empty to get first update - const [pendingState, setPendingState] = useState(false); const [selectedNetworkModifications, setSelectedNetworkModifications] = useState( [] @@ -197,7 +191,6 @@ const NetworkModificationNodeEditor = () => { const [importDialogOpen, setImportDialogOpen] = useState(false); const [createCompositeModificationDialogOpen, setCreateCompositeModificationDialogOpen] = useState(false); const dispatch = useDispatch(); - const [notificationMessageId, setNotificationMessageId] = useState(''); const [isFetchingModifications, setIsFetchingModifications] = useState(false); const [isUpdate, setIsUpdate] = useState(false); const buttonAddRef = useRef(null); @@ -668,56 +661,6 @@ const NetworkModificationNodeEditor = () => { ) .filter((item) => !('hide' in item && item.hide)); - const fillNotification = useCallback( - ( - eventData: - | ModificationsCreationInProgressEventData - | ModificationsUpdatingInProgressEventData - | ModificationsStashingInProgressEventData - | ModificationsRestoringInProgressEventData - | ModificationsDeletingInProgressEventData, - messageId: string - ) => { - // (work for all users) - // specific message id for each action type - - setNotificationMessageId(messageId); - dispatch(addNotification([eventData.headers.parentNode ?? []])); - }, - [dispatch] - ); - - const manageNotification = useCallback( - ( - eventData: - | ModificationsCreationInProgressEventData - | ModificationsUpdatingInProgressEventData - | ModificationsStashingInProgressEventData - | ModificationsRestoringInProgressEventData - | ModificationsDeletingInProgressEventData - ) => { - let messageId; - switch (eventData.headers.updateType) { - case NotificationType.MODIFICATIONS_CREATION_IN_PROGRESS: - messageId = 'network_modifications.creatingModification'; - break; - case NotificationType.MODIFICATIONS_UPDATING_IN_PROGRESS: - messageId = 'network_modifications.updatingModification'; - break; - case NotificationType.MODIFICATIONS_STASHING_IN_PROGRESS: - messageId = 'network_modifications.stashingModification'; - break; - case NotificationType.MODIFICATIONS_RESTORING_IN_PROGRESS: - messageId = 'network_modifications.restoringModification'; - break; - default: - messageId = ''; - } - fillNotification(eventData, messageId); - }, - [fillNotification] - ); - const dofetchNetworkModificationsToRestore = useCallback(() => { if (currentNode?.type !== NodeType.NETWORK_MODIFICATION) { return; @@ -733,11 +676,9 @@ const NetworkModificationNodeEditor = () => { snackWithFallback(snackError, error); }) .finally(() => { - setPendingState(false); setIsFetchingModifications(false); - dispatch(setModificationsInProgress(false)); }); - }, [studyUuid, currentNode?.id, currentNode?.type, snackError, dispatch]); + }, [studyUuid, currentNode?.id, currentNode?.type, snackError]); const updateSelectedItems = useCallback((modifications: NetworkModificationMetadata[]) => { const toKeepIdsSet = new Set(modifications.map((e) => e.uuid)); @@ -765,11 +706,9 @@ const NetworkModificationNodeEditor = () => { snackWithFallback(snackError, error); }) .finally(() => { - setPendingState(false); setIsFetchingModifications(false); - dispatch(setModificationsInProgress(false)); }); - }, [currentNode?.type, currentNode?.id, studyUuid, updateSelectedItems, snackError, dispatch]); + }, [currentNode?.type, currentNode?.id, studyUuid, updateSelectedItems, snackError]); const dofetchExcludedNetworkModifications = useCallback(() => { // Do not fetch modifications status on the root node @@ -789,11 +728,9 @@ const NetworkModificationNodeEditor = () => { snackWithFallback(snackError, error); }) .finally(() => { - setPendingState(false); setIsFetchingModifications(false); - dispatch(setModificationsInProgress(false)); }); - }, [currentNode?.type, currentNode?.id, studyUuid, snackError, dispatch]); + }, [currentNode?.type, currentNode?.id, studyUuid, snackError]); useEffect(() => { if (!currentNode) { @@ -852,41 +789,23 @@ const NetworkModificationNodeEditor = () => { } } - if (isPendingModificationNotification(eventData)) { - if (currentNodeIdRef.current !== eventData.headers.parentNode) { - return; - } - if (eventData.headers.updateType === NotificationType.MODIFICATIONS_DELETING_IN_PROGRESS) { - // deleting means removing from trashcan (stashed elements) so there is no network modification - setDeleteInProgress(true); - } else { - dispatch(setModificationsInProgress(true)); - setPendingState(true); - manageNotification(eventData); - } - } - // notify finished action (success or error => we remove the loader) - // error handling in dialog for each equipment (snackbar with specific error showed only for current user) + // success or error, the modifications may have changed : the spinner is driven by the node activity if (isModificationsUpdateFinishedNotification(eventData)) { if (currentNodeIdRef.current !== eventData.headers.parentNode) { return; } - // fetch modifications because it must have changed - // Do not clear the modifications list, because currentNode is the concerned one - // this allows to append new modifications to the existing list. + // append to the existing list : currentNode is the concerned one, so it is not cleared dofetchNetworkModifications(); dofetchExcludedNetworkModifications(); - dispatch(removeNotificationByNode([eventData.headers.parentNode, ...(eventData.headers.nodes ?? [])])); } if (isModificationsDeleteFinishedNotification(eventData)) { if (currentNodeIdRef.current !== eventData.headers.parentNode) { return; } - setDeleteInProgress(false); dofetchNetworkModifications(); } }, - [dispatch, dofetchNetworkModifications, manageNotification, cleanClipboard, dofetchExcludedNetworkModifications] + [dofetchNetworkModifications, cleanClipboard, dofetchExcludedNetworkModifications] ); useNotificationsListener(NotificationsUrlKeys.STUDY, { @@ -895,7 +814,9 @@ const NetworkModificationNodeEditor = () => { const [openNetworkModificationsMenu, setOpenNetworkModificationsMenu] = useState(false); - const isAnyNodeBuilding = useIsAnyNodeBuilding(); + const isEditBlocked = useIsEditBlocked(currentNode?.id); + const isBuildBlocked = useIsBuildBlocked(currentNode?.id, currentNode?.data); + const isNodeUpdating = useIsNodeUpdating(currentNode?.id); const mapDataLoading = useSelector((state: AppState) => state.mapDataLoading); @@ -1125,14 +1046,10 @@ const NetworkModificationNodeEditor = () => { return undefined; }; - const isImpactedByNotification = useCallback(() => { - return notificationIdList.filter((notification) => notification === currentNode?.id).length > 0; - }, [notificationIdList, currentNode?.id]); - const isModificationClickable = useCallback( (modification: ComposedModificationMetadata) => - !isAnyNodeBuilding && !mapDataLoading && !isDragging && isEditableModification(modification), - [isAnyNodeBuilding, mapDataLoading, isDragging] + !isEditBlocked && !mapDataLoading && !isDragging && isEditableModification(modification), + [isEditBlocked, mapDataLoading, isDragging] ); const columns = useMemo[]>( @@ -1167,11 +1084,10 @@ const NetworkModificationNodeEditor = () => { onRowDragStart={onRowDragStart} onRowDragEnd={onRowDragEnd} onSelectedRowsChange={handleRowSelected} - isRowDragDisabled={isImpactedByNotification() || isAnyNodeBuilding || mapDataLoading} - isImpactedByNotification={isImpactedByNotification} - notificationMessageId={notificationMessageId} + isRowDragDisabled={isEditBlocked || mapDataLoading} + isImpactedByNotification={NEVER_IMPACTED_BY_NOTIFICATION} isFetchingModifications={isFetchingModifications} - pendingState={pendingState} + pendingState={isNodeUpdating} columns={columns} highlightedModificationUuid={highlightedModificationUuid} modificationUuidsToReset={modificationUuidsToReset} @@ -1182,7 +1098,7 @@ const NetworkModificationNodeEditor = () => { rootNetworks={isMonoRootStudy ? undefined : rootNetworks} modificationsToExclude={modificationsToExclude} setModificationsToExclude={setModificationsToExclude} - isDisabled={isAnyNodeBuilding || mapDataLoading} + isDisabled={isEditBlocked || mapDataLoading} /> ); }; @@ -1238,12 +1154,12 @@ const NetworkModificationNodeEditor = () => { }, []); const isPasteButtonDisabled = useMemo(() => { - return networkModificationsToCopy.length <= 0 || isAnyNodeBuilding || mapDataLoading || !currentNode; - }, [networkModificationsToCopy.length, isAnyNodeBuilding, mapDataLoading, currentNode]); + return networkModificationsToCopy.length <= 0 || isEditBlocked || mapDataLoading || !currentNode; + }, [networkModificationsToCopy.length, isEditBlocked, mapDataLoading, currentNode]); const isRestoreButtonDisabled = useMemo(() => { - return modificationsToRestore.length === 0 || isAnyNodeBuilding || deleteInProgress; - }, [modificationsToRestore.length, isAnyNodeBuilding, deleteInProgress]); + return modificationsToRestore.length === 0 || isEditBlocked; + }, [modificationsToRestore.length, isEditBlocked]); const isCompositeNestingLimitReached = useMemo( () => selectedNetworkModifications.some((row) => (row.maxDepth ?? 0) >= MAX_COMPOSITE_NESTING_DEPTH), @@ -1251,8 +1167,14 @@ const NetworkModificationNodeEditor = () => { ); const disabledCompositeCreation: boolean = useMemo(() => { - return selectedNetworkModifications?.length === 0 || saveInProgress || isRootNode || isAssemblyDepthExceeded; - }, [selectedNetworkModifications, saveInProgress, isRootNode, isAssemblyDepthExceeded]); + return ( + selectedNetworkModifications?.length === 0 || + saveInProgress || + isRootNode || + isAssemblyDepthExceeded || + isEditBlocked + ); + }, [selectedNetworkModifications, saveInProgress, isRootNode, isAssemblyDepthExceeded, isEditBlocked]); const disabledCompositeExport: boolean = useMemo(() => { return ( @@ -1264,13 +1186,25 @@ const NetworkModificationNodeEditor = () => { <> + {currentNode?.type === NodeType.NETWORK_MODIFICATION && ( + <> + + + + )} }> @@ -1305,7 +1239,7 @@ const NetworkModificationNodeEditor = () => { @@ -1342,7 +1276,7 @@ const NetworkModificationNodeEditor = () => { size={'small'} disabled={ selectedNetworkModifications.length === 0 || - isAnyNodeBuilding || + isEditBlocked || mapDataLoading || !currentNode || isRootNode || @@ -1361,7 +1295,7 @@ const NetworkModificationNodeEditor = () => { size={'small'} disabled={ selectedNetworkModifications.length === 0 || - isAnyNodeBuilding || + isEditBlocked || mapDataLoading || isRootNode || selectionContainsShared @@ -1401,9 +1335,8 @@ const NetworkModificationNodeEditor = () => { size={'small'} disabled={ selectedNetworkModifications.length === 0 || - isAnyNodeBuilding || + isEditBlocked || mapDataLoading || - deleteInProgress || !currentNode || isRootNode } @@ -1413,36 +1346,28 @@ const NetworkModificationNodeEditor = () => { - {deleteInProgress ? ( - }> - - - - - ) : ( - 1 ? 's' : '', - }} - /> - } - > - - - - - - - )} + 1 ? 's' : '', + }} + /> + } + > + + + + + + {restoreDialogOpen && renderNetworkModificationsToRestoreDialog()} {importDialogOpen && renderImportNetworkModificationsDialog()} diff --git a/src/components/graph/menus/root-network/unbuild-all-nodes-button.tsx b/src/components/graph/menus/root-network/unbuild-all-nodes-button.tsx index 033f32ef23..367bd9cfe0 100644 --- a/src/components/graph/menus/root-network/unbuild-all-nodes-button.tsx +++ b/src/components/graph/menus/root-network/unbuild-all-nodes-button.tsx @@ -21,6 +21,7 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { AppState } from 'redux/reducer.type'; import { unbuildAllStudyNodes } from 'services/study/study'; +import { useIsUnbuildAllBlocked } from 'components/node-activity/hooks/use-node-activity'; import { NETWORK_MODIFICATION } from '../../../../utils/report/report.constant'; const styles = { @@ -41,6 +42,8 @@ export const UnbuildAllNodesButton = () => { const [isValidationDialogOpen, setIsValidationDialogOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const isUnbuildAllBlocked = useIsUnbuildAllBlocked(); + const handleCloseDialog = () => { setIsValidationDialogOpen(false); }; @@ -81,8 +84,15 @@ export const UnbuildAllNodesButton = () => { <> - diff --git a/src/components/graph/network-modification-tree-model.ts b/src/components/graph/network-modification-tree-model.ts index 87d7969170..5a60eb6532 100644 --- a/src/components/graph/network-modification-tree-model.ts +++ b/src/components/graph/network-modification-tree-model.ts @@ -6,7 +6,6 @@ */ import { convertNodetoReactFlowModelNode, getModificationNodeDataOrUndefined } from './util/model-functions'; -import { BuildStatus } from '@gridsuite/commons-ui'; import type { UUID } from 'node:crypto'; import { Edge } from '@xyflow/react'; import { AbstractNode, CurrentTreeNode, NetworkModificationNodeData, RootNodeData } from './tree-node.type'; @@ -28,8 +27,6 @@ export default class NetworkModificationTreeModel { treeNodes: CurrentTreeNode[] = []; treeEdges: Edge[] = []; - isAnyNodeBuilding = false; - // Will sort if columnPosition is defined, and not move the nodes if undefined childrenNodeSorter(a: AbstractNode, b: AbstractNode) { if (a.columnPosition !== undefined && b.columnPosition !== undefined) { @@ -272,16 +269,10 @@ export default class NetworkModificationTreeModel { elements.children.forEach((child) => { this.addChild(child, elements.id); }); - this.setBuildingStatus(); } newSharedForUpdate() { /* shallow clone of the network https://stackoverflow.com/a/44782052 */ return Object.assign(Object.create(Object.getPrototypeOf(this)), this); } - - setBuildingStatus() { - this.isAnyNodeBuilding = - this.treeNodes.find((node) => node?.data?.globalBuildStatus === BuildStatus.BUILDING) !== undefined; - } } diff --git a/src/components/graph/nodes/build-button.tsx b/src/components/graph/nodes/build-button.tsx index 402310d257..067a6c522a 100644 --- a/src/components/graph/nodes/build-button.tsx +++ b/src/components/graph/nodes/build-button.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useState } from 'react'; import { PlayCircleFilled, StopCircleOutlined } from '@mui/icons-material'; -import { Button, CircularProgress } from '@mui/material'; +import { Button } from '@mui/material'; import { buildNode, unbuildNode } from '../../../services/study'; import type { UUID } from 'node:crypto'; import { type MuiStyles, snackWithFallback, BuildStatus, useSnackMessage } from '@gridsuite/commons-ui'; @@ -18,6 +18,7 @@ type BuildButtonProps = { currentRootNetworkUuid: UUID | null; nodeUuid: UUID; onClick?: () => void; + disabled?: boolean; }; const styles = { @@ -35,6 +36,7 @@ export const BuildButton = ({ currentRootNetworkUuid, nodeUuid, onClick, + disabled, }: BuildButtonProps) => { const [isLoading, setIsLoading] = useState(false); const { snackError } = useSnackMessage(); @@ -69,9 +71,6 @@ export const BuildButton = ({ ); const getIcon = () => { - if (isLoading) { - return ; - } return !buildStatus || buildStatus === BuildStatus.NOT_BUILT ? ( ) : ( @@ -79,7 +78,7 @@ export const BuildButton = ({ ); }; - const isButtonDisabled = isLoading || !studyUuid || !currentRootNetworkUuid; + const isButtonDisabled = isLoading || !studyUuid || !currentRootNetworkUuid || disabled; return (