diff --git a/package-lock.json b/package-lock.json index ca80a277..6f3f5f4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@material-symbols/svg-400": "^0.31.9", "@react-querybuilder/dnd": "^8.11.0", "@react-querybuilder/material": "^8.11.0", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.18", "autosuggest-highlight": "^3.3.4", "clsx": "^2.1.1", "jwt-decode": "^4.0.0", @@ -6051,6 +6053,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.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "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.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", diff --git a/package.json b/package.json index 83ee2b93..1ce0974c 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "react-resizable-panels": "^3.0.6", "reconnecting-websocket": "^4.4.0", "type-fest": "^4.41.0", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.18", "uuid": "^13.0.0" }, "peerDependencies": { diff --git a/src/components/index.ts b/src/components/index.ts index a60c49a3..e0a874b1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -30,3 +30,4 @@ export * from './menus'; export * from './muiTable'; export * from './resizablePanels'; export * from './network-modifications'; +export * from './network-modification-table'; diff --git a/src/components/network-modification-table/columns-definition.tsx b/src/components/network-modification-table/columns-definition.tsx new file mode 100644 index 00000000..d861096f --- /dev/null +++ b/src/components/network-modification-table/columns-definition.tsx @@ -0,0 +1,49 @@ +/* + * 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 { + NetworkModificationEditorNameHeaderProps, +} from './renderers'; + +const CHIP_PADDING_PX = 24; +const CHAR_WIDTH_PX = 8; +const COLUMN_PADDING_PX = 12; +const MIN_COLUMN_SIZE = 40; + +export const computeTagMinSize = (tag: string): number => { + const chipContentWidth = tag.length * CHAR_WIDTH_PX + CHIP_PADDING_PX; + return Math.max(chipContentWidth + COLUMN_PADDING_PX, MIN_COLUMN_SIZE); +}; + +export const BASE_MODIFICATION_TABLE_COLUMNS = { + DRAG_HANDLE: { + id: 'dragHandle', + autoExtensible: false, + }, + SELECT: { + id: 'select', + autoExtensible: false, + }, + NAME: { + id: 'modificationName', + autoExtensible: true, + }, + DESCRIPTION: { + id: 'modificationDescription', + autoExtensible: false, + }, + SWITCH: { + id: 'switch', + autoExtensible: false, + }, +}; + +export const AUTO_EXTENSIBLE_COLUMNS = Object.values(BASE_MODIFICATION_TABLE_COLUMNS) + .filter((column) => column.autoExtensible) + .map((column) => column.id); + +export type NameHeaderProps = Omit; diff --git a/src/components/network-modification-table/index.ts b/src/components/network-modification-table/index.ts new file mode 100644 index 00000000..3ad6acff --- /dev/null +++ b/src/components/network-modification-table/index.ts @@ -0,0 +1,14 @@ +/** +import NetworkModificationsTable from './network-modification-table/network-modifications-table'; + * 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/. + */ + +export * from './network-table-styles'; +export * from './columns-definition'; +export * from './network-modifications-table'; +export * from './use-modifications-drag-and-drop'; +export * from './renderers'; +export * from './row'; diff --git a/src/components/network-modification-table/network-modifications-table.tsx b/src/components/network-modification-table/network-modifications-table.tsx new file mode 100644 index 00000000..43e474c9 --- /dev/null +++ b/src/components/network-modification-table/network-modifications-table.tsx @@ -0,0 +1,158 @@ +/** + * 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 React, { Dispatch, SetStateAction, useEffect, useMemo, useRef } from 'react'; +import { Box, Table, TableBody, TableCell, TableHead, TableRow, useTheme } from '@mui/material'; +import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { DragDropContext, DragStart, Droppable, DroppableProvided, DropResult } from '@hello-pangea/dnd'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { UUID } from 'node:crypto'; +import { NetworkModificationEditorNameHeaderProps } from './renderers'; +import { AUTO_EXTENSIBLE_COLUMNS, NameHeaderProps } from './columns-definition'; +import { useModificationsDragAndDrop } from './use-modifications-drag-and-drop'; +import { NetworkModificationMetadata } from '../../hooks'; +import { createHeaderCellStyle, MODIFICATION_ROW_HEIGHT, networkTableStyles } from './network-table-styles'; +import { ModificationRow } from './row'; + +interface NetworkModificationsTableProps extends Omit { + modifications: NetworkModificationMetadata[]; + setModifications: Dispatch>; + handleCellClick: (modification: NetworkModificationMetadata) => void; + isRowDragDisabled?: boolean; + onRowDragStart: (event: DragStart) => void; + onRowDragEnd: (event: DropResult) => void; + onRowSelected: (selectedRows: NetworkModificationMetadata[]) => void; + createAllColumns: (isRowDragDisabled: boolean, + modificationsCount: number, + nameHeaderProps: NameHeaderProps, + setModifications: React.Dispatch> + ) => ColumnDef[]; + highlightedModificationUuid: UUID | null; +} + +export function NetworkModificationsTable({ + modifications, + setModifications, + handleCellClick, + isRowDragDisabled = false, + onRowDragStart, + onRowDragEnd, + onRowSelected, + createAllColumns, + highlightedModificationUuid, + ...nameHeaderProps + }: Readonly) { + const theme = useTheme(); + + const containerRef = useRef(null); + const lastClickedIndex = useRef(null); + + const columns = useMemo[]>(() => + createAllColumns(isRowDragDisabled ?? false, + modifications.length, + nameHeaderProps, + setModifications) + , [ + isRowDragDisabled, + modifications, + nameHeaderProps, + setModifications, +]); + + const table = useReactTable({ + data: modifications, + columns, + getCoreRowModel: getCoreRowModel(), + getRowId: (row:any) => row.uuid, + enableRowSelection: true, + meta: { lastClickedIndex, onRowSelected }, + }); + + const { rows } = table.getRowModel(); + + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => containerRef.current, + overscan: 5, + estimateSize: () => MODIFICATION_ROW_HEIGHT, + }); + const virtualItems = virtualizer.getVirtualItems(); + + const { handleDragUpdate, handleDragEnd, renderClone } = useModificationsDragAndDrop({ + rows, + containerRef, + onRowDragEnd, + }); + + useEffect(() => { + table.resetRowSelection(); + lastClickedIndex.current = null; + }, [table]); + + useEffect(() => { + if (highlightedModificationUuid && containerRef.current) { + const rowIndex = rows.findIndex((row) => row.original.uuid === highlightedModificationUuid); + if (rowIndex !== -1) { + virtualizer.scrollToIndex(rowIndex, { align: 'start', behavior: 'auto' }); + } + } + }, [highlightedModificationUuid, rows, virtualizer]); + + return ( + + + + {(provided: DroppableProvided) => ( + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {virtualItems.map((virtualRow) => { + const row = rows[virtualRow.index]; + return ( + + ); + })} + +
+
+ )} +
+
+
+ ); +} diff --git a/src/components/network-modification-table/network-table-styles.ts b/src/components/network-modification-table/network-table-styles.ts new file mode 100644 index 00000000..3251647b --- /dev/null +++ b/src/components/network-modification-table/network-table-styles.ts @@ -0,0 +1,216 @@ +/* + * 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 { VirtualItem } from '@tanstack/react-virtual'; +import { SxProps, Theme } from '@mui/material'; +import { alpha } from '@mui/material/styles'; +import { CSSProperties } from 'react'; +import { MuiStyles } from '../../utils'; + +const HIGHLIGHT_COLOR_BASE = 'rgba(144, 202, 249, 0.16)'; +const HIGHLIGHT_COLOR_HOVER = 'rgba(144, 202, 249, 0.24)'; +const ROW_HOVER_COLOR = 'rgba(144, 202, 249, 0.08)'; +const DRAG_OPACITY = 0.5; +const DEACTIVATED_OPACITY = 0.4; + +export const MODIFICATION_ROW_HEIGHT = 41; + +// Static styles + +export const networkTableStyles = { + tableWrapper: (theme) => ({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + margin: theme.spacing(1), + border: `1px solid ${theme.palette.divider}`, + overflow: 'hidden', + minHeight: 0, + }), + container: { + position: 'relative', + flexGrow: 1, + overflow: 'auto', + height: '100%', + }, + table: (theme) => ({ + width: '100%', + tableLayout: 'fixed', + borderCollapse: 'collapse', + backgroundColor: theme.palette.background.paper, + }), + thead: (theme) => ({ + backgroundColor: theme.palette.background.paper, + position: 'sticky', + top: 0, + zIndex: 1, + width: '100%', + '& tr:hover': { + backgroundColor: 'transparent', + }, + }), + tableRow: { + display: 'flex', + alignItems: 'center', + transition: 'none', + opacity: 1, + }, + tableBody: { + position: 'relative', + }, + tableCell: { + fontSize: 'small', + minWidth: 0, + display: 'flex', + }, + dragRowClone: (theme) => ({ + backgroundColor: 'background.paper', + boxShadow: 4, + opacity: 1, + border: '1px solid #f5f5f5', + display: 'flex', + width: 'fit-content', + paddingRight: theme.spacing(1), + }), + overflow: { + whiteSpace: 'pre', + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + selectCheckBox: (theme) => ({ + padding: theme.spacing(0.8), + }), + dragHandle: (theme) => ({ + display: 'flex', + alignItems: 'center', + cursor: 'grab', + opacity: 0, + padding: theme.spacing(0.5), + 'tr:hover &': { opacity: 1 }, + }), + dragIndicatorIcon: { + width: '16px', + height: '16px', + }, + modificationLabel: { + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }, + rootNetworkHeader: { + width: '100%', + display: 'flex', + justifyContent: 'center', + }, + columnCell: { + select: { padding: 2, justifyContent: 'center' }, + modificationName: { cursor: 'pointer', minWidth: 0, overflow: 'hidden', flex: 1 }, + rootNetworkChip: { textAlign: 'center' }, + }, + modificationNameHeader: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + minWidth: 0, + gap: 2, + '& .MuiTypography-root': { + fontSize: 'inherit', + }, + }, + icon: (theme) => ({ + width: theme.spacing(1), + }), + modificationCircularProgress: (theme) => ({ + marginRight: theme.spacing(2), + color: theme.palette.primary.main, + }), +} as const satisfies MuiStyles; + +// Dynamic styles + +export const DROP_INDICATOR_TOP = 'inset 0 2px 0 #90caf9'; +export const DROP_INDICATOR_BOTTOM = 'inset 0 -2px 0 #90caf9'; + +export const createRowSx = (isHighlighted: boolean, isDragging: boolean, virtualRow: VirtualItem): SxProps => ({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + width: '100%', + height: `${virtualRow.size}px`, + transform: `translateY(${virtualRow.start}px)`, + backgroundColor: isHighlighted ? HIGHLIGHT_COLOR_BASE : 'transparent', + opacity: isDragging ? DRAG_OPACITY : 1, + '&:hover': { + backgroundColor: isHighlighted ? HIGHLIGHT_COLOR_HOVER : ROW_HOVER_COLOR, + }, + ...(isDragging && { zIndex: 1, transform: 'none' }), +}); + +export const createModificationNameCellStyle = (activated: boolean): CSSProperties => ({ + opacity: activated ? 1 : DEACTIVATED_OPACITY, + paddingLeft: '0.8vw', +}); + +export const createRootNetworkChipCellSx = (activated: boolean): SxProps => ({ + width: '100%', + display: 'flex', + justifyContent: 'center', + opacity: activated ? 1 : DEACTIVATED_OPACITY, +}); + +export const createEditDescriptionStyle = (description: string | undefined): SxProps => ({ + opacity: description ? 1 : 0, + cursor: description ? 'pointer' : 'default', + 'tr:hover &': { opacity: 1 }, +}); + +export const createCellStyle = (cell: any, isAutoExtensible: boolean) => { + const size = cell.column.getSize(); + const { minSize } = cell.column.columnDef; + + return { + ...cell.column.columnDef.meta?.cellStyle, + padding: 0, + flex: isAutoExtensible ? `1 1 ${size}px` : `0 1 ${size}px`, + minWidth: minSize ? `${minSize}px` : undefined, + height: `${MODIFICATION_ROW_HEIGHT}px`, + display: 'flex', + alignItems: 'center', + }; +}; + +export const createHeaderCellStyle = ( + header: any, + theme: Theme, + isFirst: boolean, + isLast: boolean, + isAutoExtensible: boolean +) => { + const darkBorder = `1px solid ${alpha(theme.palette.text.secondary, 0.4)}`; + const size = header.column.getSize(); + const { minSize } = header.column.columnDef; + + return { + ...header.column.columnDef.meta?.cellStyle, + flex: isAutoExtensible ? `1 1 ${size}px` : `0 1 ${size}px`, + minWidth: minSize ? `${minSize}px` : undefined, + height: `${MODIFICATION_ROW_HEIGHT}px`, + padding: '2px', + textAlign: 'left', + fontWeight: 600, + display: 'flex', + alignItems: 'center', + paddingTop: '1.5vh', + paddingBottom: '1.5vh', + backgroundColor: theme.palette.background.paper, + borderBottom: darkBorder, + borderTop: darkBorder, + ...(isFirst && { borderLeft: darkBorder }), + ...(isLast && { borderRight: darkBorder }), + }; +}; diff --git a/src/components/network-modification-table/renderers/drag-handle-cell.tsx b/src/components/network-modification-table/renderers/drag-handle-cell.tsx new file mode 100644 index 00000000..967a4fee --- /dev/null +++ b/src/components/network-modification-table/renderers/drag-handle-cell.tsx @@ -0,0 +1,25 @@ +/* + * 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 } from '@mui/material'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { networkTableStyles } from '../network-table-styles'; + +interface DragHandleCellProps { + isRowDragDisabled: boolean; +} + +export const DragHandleCell = ({ + isRowDragDisabled, + }:Readonly) => { + return ( + isRowDragDisabled ? undefined: + + + + ); +}; diff --git a/src/components/network-modification-table/renderers/index.ts b/src/components/network-modification-table/renderers/index.ts new file mode 100644 index 00000000..6bf32044 --- /dev/null +++ b/src/components/network-modification-table/renderers/index.ts @@ -0,0 +1,12 @@ +/** + * 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/. + */ + +export * from './drag-handle-cell'; +export * from './name-cell'; +export * from './network-modification-node-editor-name-header'; +export * from './select-cell'; +export * from './select-header-cell'; diff --git a/src/components/network-modification-table/renderers/name-cell.tsx b/src/components/network-modification-table/renderers/name-cell.tsx new file mode 100644 index 00000000..a5d621d4 --- /dev/null +++ b/src/components/network-modification-table/renderers/name-cell.tsx @@ -0,0 +1,39 @@ +/* + * 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 React, { FunctionComponent, useCallback, useMemo } from 'react'; +import { Row } from '@tanstack/react-table'; +import { useIntl } from 'react-intl'; +import { Box, Tooltip } from '@mui/material'; +import { createModificationNameCellStyle, networkTableStyles } from '../network-table-styles'; +import { NetworkModificationMetadata, useModificationLabelComputer } from '../../../hooks'; +import { mergeSx } from '../../../utils'; + +export const NameCell: FunctionComponent<{ row: Row }> = ({ row }) => { + const intl = useIntl(); + const { computeLabel } = useModificationLabelComputer(); + + const getModificationLabel = useCallback( + (modification: NetworkModificationMetadata, formatBold: boolean = true) => { + return intl.formatMessage( + { id: `network_modifications.${modification.messageType}` }, + { ...modification, ...computeLabel(modification, formatBold) } + ); + }, + [computeLabel, intl] + ); + + const label = useMemo(() => getModificationLabel(row.original), [getModificationLabel, row.original]); + + return ( + + + {label} + + + ); +}; diff --git a/src/components/network-modification-table/renderers/network-modification-node-editor-name-header.tsx b/src/components/network-modification-table/renderers/network-modification-node-editor-name-header.tsx new file mode 100644 index 00000000..a66bd988 --- /dev/null +++ b/src/components/network-modification-table/renderers/network-modification-node-editor-name-header.tsx @@ -0,0 +1,76 @@ +/** + * 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 { Box, CircularProgress, Typography } from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { FunctionComponent } from 'react'; +import { networkTableStyles } from '../network-table-styles'; + +export interface NetworkModificationEditorNameHeaderProps { + modificationCount?: number; + notificationMessageId?: string; + isFetchingModifications: boolean; + isImpactedByNotification: () => boolean; + pendingState: boolean; +} + +export const NetworkModificationEditorNameHeader: FunctionComponent = ( + props +) => { + const { + modificationCount, + isFetchingModifications, + isImpactedByNotification, + notificationMessageId, + pendingState, + } = props; + + if (isImpactedByNotification() && notificationMessageId) { + return ( + + + + + + + + + ); + } + + if (isFetchingModifications) { + return ( + + + + + + + + + ); + } + + return ( + + {pendingState && ( + + + + )} + + + + + ); +}; diff --git a/src/components/network-modification-table/renderers/select-cell.tsx b/src/components/network-modification-table/renderers/select-cell.tsx new file mode 100644 index 00000000..88776a65 --- /dev/null +++ b/src/components/network-modification-table/renderers/select-cell.tsx @@ -0,0 +1,78 @@ +/* + * 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 React, { FunctionComponent, useCallback } from 'react'; +import { Checkbox } from '@mui/material'; +import { Row, Table } from '@tanstack/react-table'; +import { networkTableStyles } from '../network-table-styles'; +import { NetworkModificationMetadata } from '../../../hooks'; + +interface SelectCellRendererProps { + row: Row; + table: Table; +} + +export const SelectCell: FunctionComponent = ({ row, table }) => { + const meta: any = table.options.meta; + + const handleChange = useCallback( + (event: React.MouseEvent) => { + const rows = table.getRowModel().rows; + const currentIndex = row.index; + const nextSelection = { ...table.getState().rowSelection }; + + // When shift is held and a previous click exists, select or deselect the contiguous range between + // the two clicks instead of toggling a single row. + if ( + event.shiftKey && + meta?.lastClickedIndex.current !== null && + meta?.lastClickedIndex.current !== undefined + ) { + const lastIndex = meta.lastClickedIndex.current; + const [from, to] = lastIndex < currentIndex ? [lastIndex, currentIndex] : [currentIndex, lastIndex]; + const isRowSelected = row.getIsSelected(); + + rows.slice(from, to + 1).forEach((r) => { + if (r.getCanSelect()) { + r.toggleSelected(!isRowSelected); + if (isRowSelected) { + delete nextSelection[r.id]; + } else { + nextSelection[r.id] = true; + } + } + }); + } else { + row.toggleSelected(); + if (row.getIsSelected()) { + // was selected, now toggled off + delete nextSelection[row.id]; + } else { + // was unselected, now toggled on + nextSelection[row.id] = true; + } + } + + if (meta) { + meta.lastClickedIndex.current = currentIndex; + const selectedRows = rows.filter((r) => nextSelection[r.id]).map((r) => r.original); + meta.onRowSelected?.(selectedRows); + } + }, + [table, row, meta] + ); + + return ( + + ); +}; diff --git a/src/components/network-modification-table/renderers/select-header-cell.tsx b/src/components/network-modification-table/renderers/select-header-cell.tsx new file mode 100644 index 00000000..74b5cd18 --- /dev/null +++ b/src/components/network-modification-table/renderers/select-header-cell.tsx @@ -0,0 +1,38 @@ +/* + * 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 React, { FunctionComponent, useCallback } from 'react'; +import { Checkbox } from '@mui/material'; +import { Table } from '@tanstack/react-table'; +import { NetworkModificationMetadata } from '../../../hooks'; + +interface SelectHeaderCellProps { + table: Table; +} + +export const SelectHeaderCell: FunctionComponent = ({ table }) => { + const handleClick = useCallback(() => { + const meta: any = table.options.meta; + if (meta) { + const nextSelectedRows = table.getIsAllRowsSelected() + ? [] + : table.getCoreRowModel().rows.map((r) => r.original); + meta.onRowSelected?.(nextSelectedRows); + meta.lastClickedIndex.current = null; + } + table.toggleAllRowsSelected(); + }, [table]); + + return ( + + ); +}; diff --git a/src/components/network-modification-table/row/drag-row-clone.tsx b/src/components/network-modification-table/row/drag-row-clone.tsx new file mode 100644 index 00000000..3ef0a62f --- /dev/null +++ b/src/components/network-modification-table/row/drag-row-clone.tsx @@ -0,0 +1,33 @@ +/* + * 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 } from '@mui/material'; +import { flexRender, Row } from '@tanstack/react-table'; +import { createCellStyle, networkTableStyles } from '../network-table-styles'; +import { AUTO_EXTENSIBLE_COLUMNS, BASE_MODIFICATION_TABLE_COLUMNS } from '../columns-definition'; +import { NetworkModificationMetadata } from '../../../hooks'; + +function DragCloneRow({ row }: { row: Row }) { + return ( + + {row + .getVisibleCells() + .filter((cell) => + [BASE_MODIFICATION_TABLE_COLUMNS.DRAG_HANDLE.id, BASE_MODIFICATION_TABLE_COLUMNS.NAME.id].includes( + cell.column.columnDef.id! + ) + ) + .map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); +} + +export default DragCloneRow; diff --git a/src/components/network-modification-table/row/index.ts b/src/components/network-modification-table/row/index.ts new file mode 100644 index 00000000..be94a859 --- /dev/null +++ b/src/components/network-modification-table/row/index.ts @@ -0,0 +1,9 @@ +/** + * 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/. + */ + +export * from './drag-row-clone'; +export * from './modification-row'; \ No newline at end of file diff --git a/src/components/network-modification-table/row/modification-row.tsx b/src/components/network-modification-table/row/modification-row.tsx new file mode 100644 index 00000000..6713ec73 --- /dev/null +++ b/src/components/network-modification-table/row/modification-row.tsx @@ -0,0 +1,68 @@ +/* + * 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 React, { memo, useCallback } from 'react'; +import { flexRender, Row } from '@tanstack/react-table'; +import { TableCell, TableRow } from '@mui/material'; +import { Draggable, DraggableProvided, DraggableStateSnapshot } from '@hello-pangea/dnd'; +import { VirtualItem } from '@tanstack/react-virtual'; +import { createCellStyle, createRowSx, networkTableStyles } from '../network-table-styles'; +import { AUTO_EXTENSIBLE_COLUMNS, BASE_MODIFICATION_TABLE_COLUMNS } from '../columns-definition'; +import { NetworkModificationMetadata } from '../../../hooks'; +import { mergeSx } from '../../../utils'; + +interface ModificationRowProps { + virtualRow: VirtualItem; + row: Row; + handleCellClick?: (modification: NetworkModificationMetadata) => void; + isRowDragDisabled: boolean; + highlightedModificationUuid: string | null; +} + +export const ModificationRow = memo( + ({ virtualRow, row, handleCellClick, isRowDragDisabled, highlightedModificationUuid }) => { + const isHighlighted = row.original.uuid === highlightedModificationUuid; + + const handleCellClickCallback = useCallback( + (columnId: string) => { + if (columnId === BASE_MODIFICATION_TABLE_COLUMNS.NAME.id) { + handleCellClick?.(row.original); + } + }, + [handleCellClick, row.original] + ); + + return ( + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { + const { style, ...draggablePropsWithoutStyle } = provided.draggableProps; + return ( + + {row.getVisibleCells().map((cell) => ( + handleCellClickCallback(cell.column.id)} + {...(cell.column.id === BASE_MODIFICATION_TABLE_COLUMNS.DRAG_HANDLE.id + ? provided.dragHandleProps + : undefined)} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); + }} + + ); + } +); diff --git a/src/components/network-modification-table/use-modifications-drag-and-drop.tsx b/src/components/network-modification-table/use-modifications-drag-and-drop.tsx new file mode 100644 index 00000000..5e7fb18e --- /dev/null +++ b/src/components/network-modification-table/use-modifications-drag-and-drop.tsx @@ -0,0 +1,81 @@ +/* + * 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 { JSX, RefObject, useCallback } from 'react'; +import { Row } from '@tanstack/react-table'; +import { DraggableProvided, DraggableRubric, DraggableStateSnapshot, DragUpdate, DropResult } from '@hello-pangea/dnd'; +import DragCloneRow from './row/drag-row-clone'; +import { DROP_INDICATOR_BOTTOM, DROP_INDICATOR_TOP } from './network-table-styles'; +import { NetworkModificationMetadata } from '../../hooks'; + +interface UseModificationsDragAndDropParams { + rows: Row[]; + containerRef: RefObject; + onRowDragEnd?: (result: DropResult) => void; +} + +interface UseModificationsDragAndDropReturn { + handleDragUpdate: (update: DragUpdate) => void; + handleDragEnd: (result: DropResult) => void; + renderClone: ( + provided: DraggableProvided, + snapshot: DraggableStateSnapshot, + rubric: DraggableRubric + ) => JSX.Element; +} + +const clearRowDragIndicators = (container: HTMLDivElement | null): void => { + container?.querySelectorAll('[data-row-id]').forEach((el) => { + el.style.boxShadow = ''; + }); +}; + +export const useModificationsDragAndDrop = ({ + rows, + containerRef, + onRowDragEnd, +}: UseModificationsDragAndDropParams): UseModificationsDragAndDropReturn => { + const handleDragUpdate = useCallback( + (update: DragUpdate) => { + clearRowDragIndicators(containerRef.current); + + const { source, destination } = update; + if (!destination || source.index === destination.index) { + return; + } + + const targetUuid = rows[destination.index]?.original.uuid; + const el = containerRef.current?.querySelector(`[data-row-id="${targetUuid}"]`); + if (el) { + el.style.boxShadow = destination.index > source.index ? DROP_INDICATOR_BOTTOM : DROP_INDICATOR_TOP; + } + }, + [rows, containerRef] + ); + + const handleDragEnd = useCallback( + (result: DropResult) => { + clearRowDragIndicators(containerRef.current); + + if (result.destination && result.source.index !== result.destination.index) { + onRowDragEnd?.(result); + } + }, + [containerRef, onRowDragEnd] + ); + + const renderClone = useCallback( + (provided: DraggableProvided, _snapshot: DraggableStateSnapshot, rubric: DraggableRubric) => ( +
+ +
+ ), + [rows] + ); + + return { handleDragUpdate, handleDragEnd, renderClone }; +}; diff --git a/src/utils/types/network-modification-types.ts b/src/utils/types/network-modification-types.ts index a9490cda..0fc0c776 100644 --- a/src/utils/types/network-modification-types.ts +++ b/src/utils/types/network-modification-types.ts @@ -1,3 +1,5 @@ +import type { UUID } from 'node:crypto'; + /** * Copyright (c) 2026, RTE (http://www.rte-france.com) * This Source Code Form is subject to the terms of the Mozilla Public @@ -13,3 +15,8 @@ export type AttributeModification = { value?: T; op: OperationType; }; + +export interface ExcludedNetworkModifications { + rootNetworkUuid: UUID; + modificationUuidsToExclude: UUID[]; +}