diff --git a/package-lock.json b/package-lock.json index 1f014bd94d..5ff87f964c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "@powsybl/network-viewer": "3.1.0", "@reduxjs/toolkit": "^2.9.0", "@svgdotjs/svg.js": "^3.2.4", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.18", "@xyflow/react": "^12.8.4", "ag-grid-community": "^33.1.0", "ag-grid-react": "^33.3.2", @@ -6091,6 +6093,66 @@ "@svgr/core": "*" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", + "integrity": "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", + "integrity": "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@ts-jison/common": { "version": "0.4.1-alpha.1", "resolved": "https://registry.npmjs.org/@ts-jison/common/-/common-0.4.1-alpha.1.tgz", diff --git a/package.json b/package.json index a61f137fb7..ad1f3c550e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@powsybl/network-viewer": "3.1.0", "@reduxjs/toolkit": "^2.9.0", "@svgdotjs/svg.js": "^3.2.4", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.18", "@xyflow/react": "^12.8.4", "ag-grid-community": "^33.1.0", "ag-grid-react": "^33.3.2", 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 35bda6c944..b28455f89d 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 @@ -12,7 +12,6 @@ import { IElementUpdateDialog, MODIFICATION_TYPES, ModificationType, - NetworkModificationMetadata, NotificationsUrlKeys, snackWithFallback, useNotificationsListener, @@ -95,8 +94,7 @@ import ByFormulaDialog from '../../../dialogs/network-modifications/by-filter/by import ByFilterDeletionDialog from '../../../dialogs/network-modifications/by-filter/by-filter-deletion/by-filter-deletion-dialog'; import { LccCreationDialog } from '../../../dialogs/network-modifications/hvdc-line/lcc/creation/lcc-creation-dialog'; import { styles } from './network-modification-node-editor-utils'; -import NetworkModificationsTable from './network-modifications-table'; -import { CellClickedEvent, RowDragEndEvent, RowDragEnterEvent } from 'ag-grid-community'; +import NetworkModificationsTable from './tanstack-poc/network-modifications-table'; import { isModificationsDeleteFinishedNotification, isModificationsUpdateFinishedNotification, @@ -122,6 +120,8 @@ import { EQUIPMENT_TYPES } from '../../../utils/equipment-types'; 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'; import { useCopiedNetworkModifications } from 'hooks/copy-paste/use-copied-network-modifications'; +import { DragStart, DropResult } from '@hello-pangea/dnd'; +import { NetworkModificationMetadata } from './tanstack-poc/network-modifications-table'; const nonEditableModificationTypes = new Set([ 'EQUIPMENT_ATTRIBUTE_MODIFICATION', @@ -174,6 +174,37 @@ const NetworkModificationNodeEditor = () => { const [isUpdate, setIsUpdate] = useState(false); const buttonAddRef = useRef(null); + const alteredModifications = useMemo(() => { + const MAX_DEPTH = 5; + const MAX_CHILDREN = 3; + const createChildren = (base: NetworkModificationMetadata, depth: number): NetworkModificationMetadata[] => { + if (depth >= MAX_DEPTH) return []; + const childCount = Math.floor(Math.random() * (MAX_CHILDREN + 1)); + return Array.from({ length: childCount }).map(() => { + const child: NetworkModificationMetadata = { + ...base, + uuid: crypto.randomUUID(), + subModifications: [], + }; + + child.subModifications = createChildren(child, depth + 1); + + return child; + }); + }; + + return modifications.map((modification) => { + const { subModifications, ...rest } = modification; + const root: NetworkModificationMetadata = { + ...rest, + uuid: modification.uuid, + subModifications: [], + }; + root.subModifications = createChildren(root, 1); + return root; + }); + }, [modifications]); + const { networkModificationsToCopy, copyInfos, copyNetworkModifications, cutNetworkModifications, cleanClipboard } = useCopiedNetworkModifications(); @@ -1052,10 +1083,9 @@ const NetworkModificationNodeEditor = () => { setEditDialogOpen(id); setIsUpdate(false); }; - const handleRowSelected = (event: any) => { - const selectedRows = event.api.getSelectedRows(); // Get selected rows + const handleRowSelected = useCallback((selectedRows: NetworkModificationMetadata[]) => { setSelectedNetworkModifications(selectedRows); - }; + }, []); const renderDialog = () => { const menuItem = subMenuItemsList.find( @@ -1095,12 +1125,11 @@ const NetworkModificationNodeEditor = () => { return ( { }; const handleCellClick = useCallback( - (event: CellClickedEvent) => { - const { colDef, data } = event; - if (colDef.colId === 'modificationName' && isModificationClickable(data)) { + (modification: NetworkModificationMetadata) => { + if (isModificationClickable(modification)) { // Check if the clicked column is the 'modificationName' column - doEditModification(data.uuid, data.type); + doEditModification(modification.uuid, modification.type as ModificationType); } }, [doEditModification, isModificationClickable] ); - const onRowDragStart = (event: RowDragEnterEvent) => { + const onRowDragStart = (event: DragStart) => { setIsDragging(true); - setInitialPosition(event.overIndex); + setInitialPosition(event.source.index); }; - const onRowDragEnd = (event: RowDragEndEvent) => { - let newPosition = event.overIndex; + + const onRowDragEnd = (event: DropResult) => { + if (!event.destination) { + setIsDragging(false); + return; + } + + const newPosition = event.destination.index; const oldPosition = initialPosition; + if (!currentNode?.id || newPosition === undefined || oldPosition === undefined || newPosition === oldPosition) { setIsDragging(false); return; } - if (newPosition === -1) { - newPosition = modifications.length; - } const previousModifications = [...modifications]; const updatedModifications = [...modifications]; const [movedItem] = updatedModifications.splice(oldPosition, 1); - updatedModifications.splice(newPosition, 0, movedItem); setModifications(updatedModifications); @@ -1185,7 +1216,9 @@ const NetworkModificationNodeEditor = () => { snackWithFallback(snackError, error, { headerId: 'errReorderModificationMsg' }); setModifications(previousModifications); }) - .finally(() => setIsDragging(false)); + .finally(() => { + setIsDragging(false); + }); }; const isPasteButtonDisabled = useMemo(() => { diff --git a/src/components/graph/menus/network-modifications/tanstack-poc/description-renderer-tanstack.tsx b/src/components/graph/menus/network-modifications/tanstack-poc/description-renderer-tanstack.tsx new file mode 100644 index 0000000000..387fcc78a3 --- /dev/null +++ b/src/components/graph/menus/network-modifications/tanstack-poc/description-renderer-tanstack.tsx @@ -0,0 +1,97 @@ +/** + * 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 { + DescriptionModificationDialog, + EditNoteIcon, + MuiStyles, + NetworkModificationMetadata, +} from '@gridsuite/commons-ui'; +import { useCallback, useState } from 'react'; +import { Tooltip } from '@mui/material'; +import { useIsAnyNodeBuilding } from '../../../../utils/is-any-node-building-hook'; +import { useSelector } from 'react-redux'; +import { AppState } from '../../../../../redux/reducer'; +import IconButton from '@mui/material/IconButton'; +import type { UUID } from 'node:crypto'; +import { setModificationMetadata } from '../../../../../services/study/network-modifications'; + +const styles = { + coloredButton: (theme) => ({ + color: theme.palette.text.primary, + }), +} as const satisfies MuiStyles; + +export interface DescriptionRendererProps { + data: NetworkModificationMetadata; +} + +const DescriptionRenderer = (props: DescriptionRendererProps) => { + const { data } = props; + const studyUuid = useSelector((state: AppState) => state.studyUuid); + const currentNode = useSelector((state: AppState) => state.currentTreeNode); + const [isLoading, setIsLoading] = useState(false); + const isAnyNodeBuilding = useIsAnyNodeBuilding(); + const mapDataLoading = useSelector((state: AppState) => state.mapDataLoading); + const [openDescModificationDialog, setOpenDescModificationDialog] = useState(false); + + const modificationUuid = data?.uuid; + const description = data?.description; + const empty = !description; + + const updateModification = useCallback( + async (uuid: UUID, descriptionRecord: Record) => { + setIsLoading(true); + + return setModificationMetadata(studyUuid, currentNode?.id, uuid, { + description: descriptionRecord.description, + type: data?.type, + }).finally(() => { + setIsLoading(false); + }); + }, + [studyUuid, currentNode?.id, data?.type] + ); + + const handleDescDialogClose = useCallback(() => { + setOpenDescModificationDialog(false); + }, []); + + const handleModifyDescription = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); // Prevent row click from firing + setOpenDescModificationDialog(true); + }, []); + + return ( + <> + {openDescModificationDialog && modificationUuid && ( + + )} + + + + + + + ); +}; + +export default DescriptionRenderer; diff --git a/src/components/graph/menus/network-modifications/tanstack-poc/network-modifications-table.tsx b/src/components/graph/menus/network-modifications/tanstack-poc/network-modifications-table.tsx new file mode 100644 index 0000000000..f171aa953a --- /dev/null +++ b/src/components/graph/menus/network-modifications/tanstack-poc/network-modifications-table.tsx @@ -0,0 +1,573 @@ +/** + * 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 React, { memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { mergeSx, type MuiStyles, useModificationLabelComputer } from '@gridsuite/commons-ui'; +import { RemoveRedEye as RemoveRedEyeIcon } from '@mui/icons-material'; +import { + Badge, + Box, + Checkbox, + IconButton, + SxProps, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Theme, + Tooltip, +} from '@mui/material'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { useSelector } from 'react-redux'; +import { AppState } from 'redux/reducer'; +import { useIntl } from 'react-intl'; +import { + ColumnDef, + ExpandedState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + Row, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import { + DragDropContext, + Draggable, + DraggableProvided, + DraggableStateSnapshot, + DragStart, + Droppable, + DroppableProvided, + DropResult, +} from '@hello-pangea/dnd'; +import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'; +import type { UUID } from 'node:crypto'; + +import { + NetworkModificationEditorNameHeader, + NetworkModificationEditorNameHeaderProps, +} from '../network-modification-node-editor-name-header'; +import RootNetworkChipCellRenderer from '../root-network-chip-cell-renderer'; +import { ExcludedNetworkModifications } from '../network-modification-menu.type'; +import DescriptionRenderer from './description-renderer-tanstack'; +import SwitchCellRenderer from './switch-cell-renderer-tanstack'; + +export interface NetworkModificationMetadata { + uuid: UUID; + type: string; + date: Date; + stashed: boolean; + activated: boolean; + description: string; + messageType: string; + messageValues: string; + subModifications: NetworkModificationMetadata[]; +} + +interface NetworkModificationsTableProps extends Omit { + modifications: NetworkModificationMetadata[]; + setModifications: React.Dispatch>; + handleCellClick?: (modification: NetworkModificationMetadata) => void; + isRowDragDisabled?: boolean; + onRowDragStart?: (event: DragStart) => void; + onRowDragEnd?: (event: DropResult) => void; + onRowSelected?: (selectedRows: NetworkModificationMetadata[]) => void; + modificationsToExclude: ExcludedNetworkModifications[]; + setModificationsToExclude: React.Dispatch>; +} + +interface ModificationRowProps { + virtualRow: VirtualItem; + row: Row; + handleCellClick?: (modification: NetworkModificationMetadata) => void; + isRowDragDisabled: boolean; + highlightedModificationUuid: string; +} + +const styles = { + container: (theme) => ({ + position: 'relative', + flexGrow: 1, + marginTop: theme.spacing(1), + overflow: 'auto', + height: '100%', + }), + table: (theme) => ({ + width: '100%', + borderCollapse: 'collapse', + backgroundColor: theme.palette.background.paper, + }), + thead: (theme) => ({ + backgroundColor: theme.palette.background.paper, + position: 'sticky', + borderTop: `2px solid ${theme.palette.divider}`, + borderBottom: `2px solid ${theme.palette.divider}`, + top: 0, + zIndex: 1, + }), + th: { + padding: 0, + textAlign: 'left', + fontWeight: 600, + }, + tbody: (theme) => ({ + backgroundColor: theme.palette.background.paper, + position: 'relative', + }), + tr: { + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + '& .edit-description-button': { + opacity: 1, + pointerEvents: 'auto', + }, + }, + }, + td: { + padding: 0, + }, + tableCell: (theme) => ({ + fontSize: 'small', + cursor: 'inherit', + display: 'flex', + '&:before': { + content: '""', + position: 'absolute', + left: theme.spacing(0.5), + right: theme.spacing(0.5), + bottom: 0, + }, + }), + overflow: { + whiteSpace: 'pre', + textOverflow: 'ellipsis', + overflow: 'hidden', + }, +} as const satisfies MuiStyles; + +const createIndentedCellStyle = (depth: number): SxProps => ({ + paddingLeft: depth * 4, + display: 'flex', + alignItems: 'center', + gap: 1, +}); + +const createRowStyle = (provided: DraggableProvided, snapshot: DraggableStateSnapshot, virtualRow: VirtualItem) => ({ + ...provided.draggableProps.style, + position: 'absolute' as const, + top: 0, + left: 0, + right: 0, + width: '100%', + height: `${virtualRow.size}px`, + transform: snapshot.isDragging ? provided.draggableProps.style?.transform : `translateY(${virtualRow.start}px)`, + transition: snapshot.isDragging ? 'none' : 'transform 0.2s ease', +}); + +const createCellStyle = (cell: any, styles: any) => ({ + ...styles.td, + ...(cell.column.columnDef.meta as any)?.cellStyle, + width: cell.column.getSize(), + minWidth: cell.column.columnDef.minSize, + maxWidth: cell.column.getSize(), +}); + +const NetworkModificationNameCell = memo(({ row }: { row: Row }) => { + const intl = useIntl(); + const { computeLabel } = useModificationLabelComputer(); + + const hasSubModifications = row.original.subModifications?.length > 0; + const depth = row.depth; + + const getModificationLabel = useCallback( + (modif?: NetworkModificationMetadata, formatBold: boolean = true) => { + if (!modif) return ''; + return intl.formatMessage( + { id: `network_modifications.${modif.messageType}` }, + { ...modif, ...(computeLabel(modif, formatBold) as any) } + ); + }, + [computeLabel, intl] + ); + + const label = getModificationLabel(row.original); + + return ( + + {hasSubModifications ? ( + { + e.stopPropagation(); + row.getToggleExpandedHandler()(); + }} + sx={{ padding: '4px' }} + aria-label={row.getIsExpanded() ? 'Collapse' : 'Expand'} + > + {row.getIsExpanded() ? ( + + ) : ( + + )} + + ) : ( + + )} + + {label} + + + ); +}); + +const DragHandleCell = memo(({ isRowDragDisabled }: { isRowDragDisabled: boolean }) => { + if (isRowDragDisabled) { + return ; + } + return ( + + + + ); +}); + +const createStaticColumns = ( + isRowDragDisabled: boolean, + modifications: NetworkModificationMetadata[], + nameHeaderProps: any, + setModifications: React.Dispatch> +): ColumnDef[] => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + e.stopPropagation()} + /> + ), + size: 50, + }, + { + id: 'dragHandle', + header: '', + cell: () => , + size: 40, + }, + { + id: 'modificationName', + header: () => ( + + ), + cell: ({ row }) => , + minSize: 200, + size: Number.MAX_SAFE_INTEGER, + meta: { + cellStyle: { cursor: 'pointer' }, + }, + }, + { + id: 'modificationDescription', + header: '', + cell: ({ row }) => , + size: 30, + }, + { + id: 'switch', + header: '', + cell: ({ row }) => , + size: 60, + }, +]; + +const createDynamicColumns = ( + rootNetworks: any[], + currentRootNetworkUuid: string, + modificationsCount: number, + modificationsToExclude: ExcludedNetworkModifications[], + setModificationsToExclude: React.Dispatch> +): ColumnDef[] => { + return rootNetworks.map((rootNetwork) => { + const rootNetworkUuid = rootNetwork.rootNetworkUuid; + const isCurrentRootNetwork = rootNetworkUuid === currentRootNetworkUuid; + + return { + id: rootNetworkUuid, + header: () => + isCurrentRootNetwork && modificationsCount >= 1 ? ( + + + + + + ) : null, + cell: ({ row }) => ( + + ), + size: 72, + meta: { + cellStyle: { textAlign: 'center' }, + }, + }; + }); +}; + +const ModificationRow = memo( + ({ virtualRow, row, handleCellClick, isRowDragDisabled, highlightedModificationUuid }) => { + const handleCellClickCallback = useCallback( + (columnId: string) => { + if (columnId === 'modificationName') { + handleCellClick?.(row.original); + } + }, + [handleCellClick, row.original] + ); + + return ( + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + + {row.getVisibleCells().map((cell) => ( + handleCellClickCallback(cell.column.id)} + {...(cell.column.id === 'dragHandle' ? provided.dragHandleProps : undefined)} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )} + + ); + } +); + +const DragCloneRow = memo(({ row }: { row: Row }) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + +)); + +const NetworkModificationsTable: React.FC = ({ + modifications, + setModifications, + handleCellClick, + isRowDragDisabled = false, + onRowDragStart, + onRowDragEnd, + onRowSelected, + modificationsToExclude, + setModificationsToExclude, + ...nameHeaderProps +}) => { + const rootNetworks = useSelector((state: AppState) => state.rootNetworks); + const isMonoRootStudy = useSelector((state: AppState) => state.isMonoRootStudy); + const highlightedModificationUuid = useSelector((state: AppState) => state.highlightedModificationUuid); + const currentRootNetworkUuid = useSelector((state: AppState) => state.currentRootNetworkUuid); + + const [rowSelection, setRowSelection] = useState({}); + const [expanded, setExpanded] = useState({}); + + const parentRef = useRef(null); + + const columns = useMemo[]>(() => { + const staticColumns = createStaticColumns(isRowDragDisabled, modifications, nameHeaderProps, setModifications); + const dynamicColumns = !isMonoRootStudy + ? createDynamicColumns( + rootNetworks, + currentRootNetworkUuid!, + modifications.length, + modificationsToExclude, + setModificationsToExclude + ) + : []; + + return [...staticColumns, ...dynamicColumns]; + }, [ + isRowDragDisabled, + modifications, + nameHeaderProps, + setModifications, + isMonoRootStudy, + rootNetworks, + currentRootNetworkUuid, + modificationsToExclude, + setModificationsToExclude, + ]); + + const table = useReactTable({ + data: modifications, + columns, + state: { rowSelection, expanded }, + onRowSelectionChange: setRowSelection, + onExpandedChange: setExpanded, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getRowId: (row, index, parent) => (parent ? `${parent.id}.${row.uuid}` : row.uuid), + getSubRows: (row) => row.subModifications, + getRowCanExpand: (row) => !!row.original.subModifications?.length, + enableRowSelection: true, + enableExpanding: true, + }); + + const { rows } = table.getRowModel(); + + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => parentRef.current, + overscan: 5, + estimateSize: () => 48, + }); + const virtualItems = virtualizer.getVirtualItems(); + + useEffect(() => { + if (onRowSelected) { + const selectedRows = table.getSelectedRowModel().rows.map((row) => row.original); + onRowSelected(selectedRows); + } + }, [rowSelection, onRowSelected, table]); + + useEffect(() => { + if (highlightedModificationUuid && parentRef.current) { + const rowIndex = rows.findIndex((row) => row.original.uuid === highlightedModificationUuid); + if (rowIndex !== -1) { + virtualizer.scrollToIndex(rowIndex, { align: 'center', behavior: 'smooth' }); + } + } + }, [highlightedModificationUuid, rows, virtualizer]); + + // Event handlers + const handleDragStart = useCallback( + (event: DragStart) => { + onRowDragStart?.(event); + }, + [onRowDragStart] + ); + + const handleDragEnd = useCallback( + (result: DropResult) => { + if (!result.destination || result.source.index === result.destination.index) { + return; + } + onRowDragEnd?.(result); + }, + [onRowDragEnd] + ); + + const renderClone = useCallback( + (provided: DraggableProvided, snapshot: DraggableStateSnapshot, rubric: any) => ( +
+ +
+ ), + [rows] + ); + + return ( + + + {(provided: DroppableProvided) => ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {virtualItems.map((virtualRow) => { + const row = rows[virtualRow.index]; + return ( + + ); + })} + +
+
+ )} +
+
+ ); +}; + +export default memo(NetworkModificationsTable); diff --git a/src/components/graph/menus/network-modifications/tanstack-poc/switch-cell-renderer-tanstack.tsx b/src/components/graph/menus/network-modifications/tanstack-poc/switch-cell-renderer-tanstack.tsx new file mode 100644 index 0000000000..0a9313ffa8 --- /dev/null +++ b/src/components/graph/menus/network-modifications/tanstack-poc/switch-cell-renderer-tanstack.tsx @@ -0,0 +1,93 @@ +/** + * 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 React, { useState, useCallback, SetStateAction } from 'react'; +import { Switch, Tooltip } from '@mui/material'; +import { snackWithFallback, useSnackMessage } from '@gridsuite/commons-ui'; +import { setModificationMetadata } from 'services/study/network-modifications'; +import { useSelector } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { AppState } from 'redux/reducer'; +import { useIsAnyNodeBuilding } from 'components/utils/is-any-node-building-hook'; +import { NetworkModificationMetadata } from './network-modifications-table'; + +export interface SwitchCellRendererProps { + data: NetworkModificationMetadata; + setModifications: React.Dispatch>; +} + +const SwitchCellRenderer = (props: SwitchCellRendererProps) => { + const { data, setModifications } = props; + const studyUuid = useSelector((state: AppState) => state.studyUuid); + const currentNode = useSelector((state: AppState) => state.currentTreeNode); + const [isLoading, setIsLoading] = useState(false); + const isAnyNodeBuilding = useIsAnyNodeBuilding(); + const mapDataLoading = useSelector((state: AppState) => state.mapDataLoading); + + const { snackError } = useSnackMessage(); + + const modificationUuid = data?.uuid; + const modificationActivated = data?.activated; + + const updateModification = useCallback( + (activated: boolean) => { + if (!modificationUuid) { + return; + } + setModificationMetadata(studyUuid, currentNode?.id, modificationUuid, { + activated: activated, + type: data?.type, + }) + .catch((error) => { + snackWithFallback(snackError, error, { headerId: 'networkModificationActivationError' }); + }) + .finally(() => { + setIsLoading(false); + }); + }, + [modificationUuid, studyUuid, currentNode?.id, data?.type, snackError] + ); + + const toggleModificationActive = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent row click from firing + setIsLoading(true); + setModifications((oldModifications) => { + const modificationToUpdateIndex = oldModifications.findIndex((m) => m.uuid === modificationUuid); + if (modificationToUpdateIndex === -1) { + return oldModifications; + } + const newModifications = [...oldModifications]; + const newStatus = !newModifications[modificationToUpdateIndex].activated; + + newModifications[modificationToUpdateIndex] = { + ...newModifications[modificationToUpdateIndex], + activated: newStatus, + }; + + updateModification(newStatus); + return newModifications; + }); + }, + [modificationUuid, updateModification, setModifications] + ); + + return ( + } arrow> + + + + + ); +}; + +export default SwitchCellRenderer; diff --git a/src/services/study/network-modifications.ts b/src/services/study/network-modifications.ts index dc28b1f600..dd9201ed4c 100644 --- a/src/services/study/network-modifications.ts +++ b/src/services/study/network-modifications.ts @@ -13,7 +13,6 @@ import { EquipmentType, MODIFICATION_TYPES, ModificationType, - NetworkModificationMetadata, } from '@gridsuite/commons-ui'; import { toModificationOperation } from '../../components/utils/utils'; import { getStudyUrlWithNodeUuid, getStudyUrlWithNodeUuidAndRootNetworkUuid, safeEncodeURIComponent } from './index'; @@ -68,6 +67,7 @@ import { OLGS_MODIFICATION_TYPE, OPERATIONAL_LIMITS_GROUPS_MODIFICATION_TYPE, } from '../../components/utils/field-constants'; +import { NetworkModificationMetadata } from '../../components/graph/menus/network-modifications/tanstack-poc/network-modifications-table'; function getNetworkModificationUrl(studyUuid: string | null | undefined, nodeUuid: string | undefined) { return getStudyUrlWithNodeUuid(studyUuid, nodeUuid) + '/network-modifications';