From cbe394de85660990ff7660523e08c3893fd7c1f0 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:21:35 +0100 Subject: [PATCH 1/7] refactor(ui): disable TS null assertion and update the code that use it (#2312) --- webapp/eslint.config.js | 1 + .../App/Singlestudy/FreezeStudy.tsx | 11 +++++--- .../App/Singlestudy/explore/Debug/index.tsx | 27 +++++++++---------- .../src/components/App/Singlestudy/index.tsx | 2 +- .../common/buttons/DownloadButton.tsx | 5 +++- webapp/src/services/api/tasks/index.ts | 8 +++--- webapp/src/services/api/tasks/types.ts | 22 ++++++++++----- 7 files changed, 47 insertions(+), 29 deletions(-) diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index fc16942269..1d327970c5 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -59,6 +59,7 @@ export default [ rules: { ...reactHookPlugin.configs.recommended.rules, "@typescript-eslint/array-type": ["error", { default: "array-simple" }], + "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-restricted-imports": [ "error", { diff --git a/webapp/src/components/App/Singlestudy/FreezeStudy.tsx b/webapp/src/components/App/Singlestudy/FreezeStudy.tsx index b5a3c02a92..1ee2b26216 100644 --- a/webapp/src/components/App/Singlestudy/FreezeStudy.tsx +++ b/webapp/src/components/App/Singlestudy/FreezeStudy.tsx @@ -75,7 +75,7 @@ function FreezeStudy({ studyId }: FreezeStudyProps) { setBlockingTasks( tasks.map((task) => ({ id: task.id, - type: task.type!, + type: task.type, })), ); @@ -143,10 +143,15 @@ function FreezeStudy({ studyId }: FreezeStudyProps) { function forceUpdate(taskId: BlockingTask["id"]) { getTask({ id: taskId }).then((task) => { + // Normally all blocking tasks have a type + if (!task.type) { + return; + } + const payload = { id: task.id, message: task.result?.message || "", - type: task.type!, + type: task.type, }; if (task.status === TaskStatus.Running) { if (typeof task.progress === "number") { @@ -179,7 +184,7 @@ function FreezeStudy({ studyId }: FreezeStudyProps) { unsubscribeWsChannels(); window.clearInterval(intervalId); }; - }, [studyId]); + }, [blockingTasksRef, studyId]); return ( 0} sx={{ position: "absolute" }}> diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/index.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/index.tsx index deae3f19a2..a20dcef975 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/index.tsx @@ -35,7 +35,7 @@ function Debug() { // Allow to keep expanded items when the tree is reloaded with `reloadTreeData` const [expandedItems, setExpandedItems] = useState([]); const [searchParams, setSearchParams] = useSearchParams(); - const pathInUrl = searchParams.get("path"); + const path = searchParams.get("path"); const res = usePromiseWithSnackbarError( async () => { @@ -60,17 +60,18 @@ function Debug() { const firstChildName = Object.keys(res.data ?? {})[0]; const firstChildTreeData = R.path([firstChildName], res.data); - const pathInUrlParts = pathInUrl?.split("/"); - const urlPathTreeData = pathInUrlParts ? R.path(pathInUrlParts, res.data) : null; + const pathSegments = path?.split("/"); + const filename = pathSegments ? R.last(pathSegments) : null; + const treeData = pathSegments ? R.path(pathSegments, res.data) : null; let fileInfo: FileInfo | null = null; - if (urlPathTreeData) { + if (path && filename && treeData) { fileInfo = { - fileType: getFileType(urlPathTreeData), - treeData: urlPathTreeData, - filename: R.last(pathInUrlParts!)!, - filePath: pathInUrl!, + fileType: getFileType(treeData), + treeData, + filename, + filePath: path, }; } else if (firstChildTreeData) { fileInfo = { @@ -81,15 +82,11 @@ function Debug() { }; } - if (fileInfo) { - setSelectedFile(fileInfo); - } else { - setSelectedFile(null); - } - }, [res.data, pathInUrl]); + setSelectedFile(fileInfo); + }, [res.data, path]); useUpdateEffect(() => { - if (selectedFile?.filePath !== pathInUrl) { + if (selectedFile?.filePath !== path) { setSearchParams({ path: selectedFile?.filePath || "" }); } }, [selectedFile?.filePath]); diff --git a/webapp/src/components/App/Singlestudy/index.tsx b/webapp/src/components/App/Singlestudy/index.tsx index 1cb727698c..6bb6c7fe79 100644 --- a/webapp/src/components/App/Singlestudy/index.tsx +++ b/webapp/src/components/App/Singlestudy/index.tsx @@ -186,7 +186,7 @@ function SingleStudy(props: Props) { ) : ( )} - + {studyId && } {openCommands && studyId && ( (props: DownloadButtonProps("/v1/tasks", { +export async function getTasks( + params: GetTasksParams, +) { + const { data } = await client.post>>("/v1/tasks", { status: params.status, type: params.type, name: params.name, diff --git a/webapp/src/services/api/tasks/types.ts b/webapp/src/services/api/tasks/types.ts index dd0e8ffa6d..c2b44c9bcb 100644 --- a/webapp/src/services/api/tasks/types.ts +++ b/webapp/src/services/api/tasks/types.ts @@ -20,9 +20,12 @@ export type TTaskStatus = O.UnionOf; export type TTaskType = O.UnionOf; -export interface TaskDTO extends IdentityDTO { - status: TTaskStatus; - type?: TTaskType; +interface BaseTaskDTO< + TStatus extends TTaskStatus = TTaskStatus, + TType extends TTaskType = TTaskType, +> extends IdentityDTO { + status: TStatus; + type?: TType; owner?: number; ref_id?: string; creation_date_utc: string; @@ -39,9 +42,16 @@ export interface TaskDTO extends IdentityDTO { }>; } -export interface GetTasksParams { - status?: TTaskStatus[]; - type?: TTaskType[]; +export type TaskDTO< + TStatus extends TTaskStatus = TTaskStatus, + TType extends TTaskType | undefined = undefined, +> = TType extends TTaskType + ? O.Required, "type"> + : BaseTaskDTO; + +export interface GetTasksParams { + status?: TStatus[]; + type?: TType[]; name?: string; studyId?: StudyMetadata["id"]; fromCreationDateUtc?: number; From f213a26d79446403f46cef31a6dcb02323c555ae Mon Sep 17 00:00:00 2001 From: Theo Pascoli <48944759+TheoPascoli@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:57:44 +0100 Subject: [PATCH 2/7] fix: fix a bug that would occurs after deleting multiple bc (#2317) --- .../model/command/remove_multiple_binding_constraints.py | 5 ++++- .../study_data_blueprint/test_binding_constraints.py | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py b/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py index 7747f4945f..0415276eb9 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py +++ b/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py @@ -61,8 +61,11 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = if binding_constraints[key].get("id") in self.ids: deleted_binding_constraints.append(binding_constraints.pop(key)) + # BC dict should start at index 0 + new_binding_constraints = {str(i): value for i, value in enumerate(binding_constraints.values())} + study_data.tree.save( - binding_constraints, + new_binding_constraints, ["input", "bindingconstraints", "bindingconstraints"], ) diff --git a/tests/integration/study_data_blueprint/test_binding_constraints.py b/tests/integration/study_data_blueprint/test_binding_constraints.py index 84b27ded12..192f1d27b2 100644 --- a/tests/integration/study_data_blueprint/test_binding_constraints.py +++ b/tests/integration/study_data_blueprint/test_binding_constraints.py @@ -987,10 +987,15 @@ def test_for_version_870(self, client: TestClient, user_access_token: str, study assert res.status_code == 200, res.json() # Asserts that the deletion worked - binding_constraints_list = preparer.get_binding_constraints(study_id) + binding_constraints_list = client.get( + f"/v1/studies/{study_id}/raw", + params={"path": f"input/bindingconstraints/bindingconstraints"}, # type: ignore + ).json() assert len(binding_constraints_list) == 2 - actual_ids = [constraint["id"] for constraint in binding_constraints_list] + actual_ids = [constraint["id"] for constraint in binding_constraints_list.values()] assert actual_ids == ["binding_constraint_1", "binding_constraint_3"] + keys = sorted(int(k) for k in binding_constraints_list.keys()) + assert keys == list(range(len(keys))) # ============================= # CONSTRAINT DUPLICATION From e494f20ffbb49b95d7aaaec6ceb989c54eaec97c Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:57:23 +0100 Subject: [PATCH 3/7] feat(commitlint): add new rules for scope (#2319) --- commitlint.config.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 50b8b23bf4..f70916e553 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -4,12 +4,18 @@ const RuleConfigSeverity = { Disabled: 0, Warning: 1, Error: 2, -} +}; module.exports = { extends: ["@commitlint/config-conventional"], // Rules: https://commitlint.js.org/reference/rules.html rules: { - "header-max-length": [RuleConfigSeverity.Error, "always", 150], + "scope-case": [ + RuleConfigSeverity.Error, + "always", + ["lower-case", "kebab-case"], + ], + "scope-empty": [RuleConfigSeverity.Error, "never"], + "header-max-length": [RuleConfigSeverity.Error, "always", 150], }, }; From f5a4d9e43a2dd2765560da55e54cac4ea3b52190 Mon Sep 17 00:00:00 2001 From: Hatim Dinia <33469289+hdinia@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:10:14 +0100 Subject: [PATCH 4/7] feat(ui-matrix): add matrix selection stats (#2313) --- webapp/eslint.config.js | 2 +- webapp/src/components/common/DataGrid.tsx | 18 ++-- .../Matrix/components/MatrixGrid/index.tsx | 65 +++++++----- .../Matrix/components/MatrixStats/index.tsx | 97 ++++++++++++++++++ .../Matrix/hooks/useSelectionStats/index.ts | 99 +++++++++++++++++++ 5 files changed, 247 insertions(+), 34 deletions(-) create mode 100644 webapp/src/components/common/Matrix/components/MatrixStats/index.tsx create mode 100644 webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index 1d327970c5..64ab173c74 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -130,7 +130,7 @@ export default [ { // Includes hooks from 'react-use' additionalHooks: - "(useSafeMemo|useUpdateEffectOnce|useDeepCompareEffect|useShallowCompareEffect|useCustomCompareEffect)", + "(useSafeMemo|useUpdateEffect|useUpdateEffectOnce|useDeepCompareEffect|useShallowCompareEffect|useCustomCompareEffect)", }, ], "require-await": "warn", // TODO: switch to "error" when the quantity of warning will be low diff --git a/webapp/src/components/common/DataGrid.tsx b/webapp/src/components/common/DataGrid.tsx index 2f95b302d6..06db713aea 100644 --- a/webapp/src/components/common/DataGrid.tsx +++ b/webapp/src/components/common/DataGrid.tsx @@ -37,8 +37,7 @@ type RowMarkers = type RowMarkersOptions = Exclude; -export interface DataGridProps - extends Omit { +export interface DataGridProps extends Omit { rowMarkers?: RowMarkers; enableColumnResize?: boolean; } @@ -61,6 +60,7 @@ function DataGrid(props: DataGridProps) { onColumnResizeEnd, enableColumnResize = true, freezeColumns, + onGridSelectionChange, ...rest } = props; @@ -70,9 +70,9 @@ function DataGrid(props: DataGridProps) { const adjustedFreezeColumns = isStringRowMarkers ? (freezeColumns || 0) + 1 : freezeColumns; const [columns, setColumns] = useState(columnsFromProps); - const [selection, setSelection] = useState({ - columns: CompactSelection.empty(), + const [gridSelection, setGridSelection] = useState({ rows: CompactSelection.empty(), + columns: CompactSelection.empty(), }); // Add a column for the "string" row markers if needed @@ -223,7 +223,7 @@ function DataGrid(props: DataGridProps) { if (newSelection.current) { // Select the whole row when clicking on a row marker cell if (rowMarkersOptions.kind === "clickable-string" && newSelection.current.cell[0] === 0) { - setSelection({ + setGridSelection({ ...newSelection, current: undefined, rows: CompactSelection.fromSingleSelection(newSelection.current.cell[1]), @@ -249,8 +249,7 @@ function DataGrid(props: DataGridProps) { // // rowsLength // ]), // }); - - setSelection({ + setGridSelection({ ...newSelection, columns: newSelection.columns.remove(0), }); @@ -259,7 +258,8 @@ function DataGrid(props: DataGridProps) { } } - setSelection(newSelection); + setGridSelection(newSelection); + onGridSelectionChange?.(newSelection); } }; @@ -287,7 +287,7 @@ function DataGrid(props: DataGridProps) { onColumnResize={handleColumnResize} onColumnResizeStart={handleColumnResizeStart} onColumnResizeEnd={handleColumnResizeEnd} - gridSelection={selection} + gridSelection={gridSelection} onGridSelectionChange={handleGridSelectionChange} freezeColumns={adjustedFreezeColumns} /> diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx index 572ce68883..2831b4d507 100644 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx @@ -13,21 +13,21 @@ */ import { + CompactSelection, GridCellKind, + type GridSelection, type EditableGridCell, type EditListItem, type Item, } from "@glideapps/glide-data-grid"; import { useGridCellContent } from "../../hooks/useGridCellContent"; -import { useMemo } from "react"; -import { - type EnhancedGridColumn, - type GridUpdate, - type MatrixAggregates, -} from "../../shared/types"; +import { useMemo, useState } from "react"; +import DataGrid from "@/components/common/DataGrid"; import { useColumnMapping } from "../../hooks/useColumnMapping"; +import type { EnhancedGridColumn, MatrixAggregates, GridUpdate } from "../../shared/types"; import { darkTheme, readOnlyDarkTheme } from "../../styles"; -import DataGrid from "@/components/common/DataGrid"; +import MatrixStats from "../MatrixStats"; +import { useSelectionStats } from "../../hooks/useSelectionStats"; export interface MatrixGridProps { data: number[][]; @@ -42,6 +42,7 @@ export interface MatrixGridProps { onMultipleCellsEdit?: (updates: GridUpdate[]) => void; readOnly?: boolean; showPercent?: boolean; + showStats?: boolean; } function MatrixGrid({ @@ -57,9 +58,21 @@ function MatrixGrid({ onMultipleCellsEdit, readOnly, showPercent, + showStats = true, }: MatrixGridProps) { + const [gridSelection, setGridSelection] = useState({ + rows: CompactSelection.empty(), + columns: CompactSelection.empty(), + }); + const { gridToData } = useColumnMapping(columns); + const selectionStats = useSelectionStats({ + data, + selection: gridSelection, + gridToData, + }); + const theme = useMemo(() => { if (readOnly) { return { @@ -140,23 +153,27 @@ function MatrixGrid({ //////////////////////////////////////////////////////////////// return ( - + <> + + {showStats && } + ); } diff --git a/webapp/src/components/common/Matrix/components/MatrixStats/index.tsx b/webapp/src/components/common/Matrix/components/MatrixStats/index.tsx new file mode 100644 index 0000000000..6770242a7b --- /dev/null +++ b/webapp/src/components/common/Matrix/components/MatrixStats/index.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2025, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * 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/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import { Box, Paper, Typography, Divider } from "@mui/material"; +import { formatGridNumber } from "../../shared/utils"; + +interface MatrixStatsProps { + stats: { + count: number; + sum: number; + average: number; + min: number; + max: number; + } | null; +} + +function MatrixStats({ stats }: MatrixStatsProps) { + if (!stats) { + return null; + } + + const statItems = [ + { label: "Nb", value: stats.count }, + { label: "Total", value: formatGridNumber({ value: stats.sum }) }, + { + label: "Avg", + value: formatGridNumber({ value: stats.average, maxDecimals: 2 }), + }, + { label: "Min", value: formatGridNumber({ value: stats.min }) }, + { label: "Max", value: formatGridNumber({ value: stats.max }) }, + ]; + + return ( + + {statItems.map((item, index) => ( + + + {item.label} + + + {item.value} + + {index < statItems.length - 1 && ( + + )} + + ))} + + ); +} + +export default MatrixStats; diff --git a/webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts b/webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts new file mode 100644 index 0000000000..d6c38ca79f --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useSelectionStats/index.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2025, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * 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/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +import type { GridSelection, Item } from "@glideapps/glide-data-grid"; +import { useState } from "react"; +import { useUpdateEffect } from "react-use"; + +interface SelectionStats { + sum: number; + average: number; + min: number; + max: number; + count: number; +} + +interface UseSelectionStatsProps { + data: number[][]; + selection: GridSelection; + gridToData: (coordinates: Item) => Item | null; +} + +export function useSelectionStats({ data, selection, gridToData }: UseSelectionStatsProps) { + const [stats, setStats] = useState(null); + + useUpdateEffect(() => { + let sum = 0; + let min = Infinity; + let max = -Infinity; + let count = 0; + const numRows = data.length; + const numCols = data[0]?.length ?? 0; + + const processValue = (value: number) => { + sum += value; + min = Math.min(min, value); + max = Math.max(max, value); + count++; + }; + + if (selection.current?.range) { + const { x, y, width, height } = selection.current.range; + + for (let col = x; col < x + width; ++col) { + for (let row = y; row < y + height; ++row) { + const coordinates = gridToData([col, row]); + + if (coordinates) { + const [dataCol, dataRow] = coordinates; + const value = data[dataRow]?.[dataCol]; + + if (value !== undefined) { + processValue(value); + } + } + } + } + } + + for (const row of selection.rows) { + for (let col = 0; col < numCols; ++col) { + const value = data[row]?.[col]; + + if (value !== undefined) { + processValue(value); + } + } + } + + for (const col of selection.columns) { + for (let row = 0; row < numRows; ++row) { + const coordinates = gridToData([col, row]); + + if (coordinates) { + const [dataCol, dataRow] = coordinates; + const value = data[dataRow]?.[dataCol]; + + if (value !== undefined) { + processValue(value); + } + } + } + } + + setStats(count ? { sum, min, max, count, average: sum / count } : null); + }, [data, selection]); + + return stats; +} From 07a0c1f83a6eb6c47691955b31d955098c471e92 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:05:08 +0100 Subject: [PATCH 5/7] fix(ui-tablemode): adjust the size of the column with the initial data (#2320) --- webapp/src/components/common/DataGrid.tsx | 86 +++++++++---------- webapp/src/components/common/DataGridForm.tsx | 64 ++++++++++---- webapp/src/components/common/TableMode.tsx | 1 - webapp/src/utils/domUtils.ts | 65 ++++++++++++++ 4 files changed, 154 insertions(+), 62 deletions(-) create mode 100644 webapp/src/utils/domUtils.ts diff --git a/webapp/src/components/common/DataGrid.tsx b/webapp/src/components/common/DataGrid.tsx index 06db713aea..4ef299cd8e 100644 --- a/webapp/src/components/common/DataGrid.tsx +++ b/webapp/src/components/common/DataGrid.tsx @@ -19,18 +19,21 @@ import { type EditListItem, type GridSelection, type DataEditorProps, + type GridCell, } from "@glideapps/glide-data-grid"; import "@glideapps/glide-data-grid/dist/index.css"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { voidFn } from "@/utils/fnUtils"; import { darkTheme } from "./Matrix/styles"; +import { useUpdateEffect } from "react-use"; interface StringRowMarkerOptions { kind: "string" | "clickable-string"; getTitle?: (rowIndex: number) => string; + width?: number; } -type RowMarkers = +export type RowMarkers = | NonNullable | StringRowMarkerOptions["kind"] | StringRowMarkerOptions; @@ -48,54 +51,57 @@ function isStringRowMarkerOptions( return rowMarkerOptions.kind === "string" || rowMarkerOptions.kind === "clickable-string"; } -function DataGrid(props: DataGridProps) { - const { - rowMarkers = { kind: "none" }, - getCellContent, - columns: columnsFromProps, - onCellEdited, - onCellsEdited, - onColumnResize, - onColumnResizeStart, - onColumnResizeEnd, - enableColumnResize = true, - freezeColumns, - onGridSelectionChange, - ...rest - } = props; - +function DataGrid({ + rowMarkers = { kind: "none" }, + getCellContent, + columns: columnsFromProps, + onCellEdited, + onCellsEdited, + onColumnResize, + onColumnResizeStart, + onColumnResizeEnd, + onGridSelectionChange, + enableColumnResize = true, + freezeColumns, + rows, + ...rest +}: DataGridProps) { const rowMarkersOptions: RowMarkersOptions = typeof rowMarkers === "string" ? { kind: rowMarkers } : rowMarkers; + const isStringRowMarkers = isStringRowMarkerOptions(rowMarkersOptions); const adjustedFreezeColumns = isStringRowMarkers ? (freezeColumns || 0) + 1 : freezeColumns; - const [columns, setColumns] = useState(columnsFromProps); + const [columns, setColumns] = useState(initColumns); const [gridSelection, setGridSelection] = useState({ rows: CompactSelection.empty(), columns: CompactSelection.empty(), }); - // Add a column for the "string" row markers if needed - useEffect(() => { - setColumns( - isStringRowMarkers ? [{ id: "", title: "" }, ...columnsFromProps] : columnsFromProps, - ); + useUpdateEffect(() => { + setColumns(initColumns()); }, [columnsFromProps, isStringRowMarkers]); //////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////// + function initColumns() { + return isStringRowMarkers + ? [{ id: "", title: "", width: rowMarkersOptions.width }, ...columnsFromProps] + : columnsFromProps; + } + const ifElseStringRowMarkers = ( colIndex: number, - onTrue: () => R1, + onTrue: (options: StringRowMarkerOptions) => R1, onFalse: (colIndex: number) => R2, ) => { let adjustedColIndex = colIndex; if (isStringRowMarkers) { if (colIndex === 0) { - return onTrue(); + return onTrue(rowMarkersOptions); } adjustedColIndex = colIndex - 1; @@ -118,11 +124,8 @@ function DataGrid(props: DataGridProps) { return ifElseStringRowMarkers( colIndex, - () => { - const title = - isStringRowMarkers && rowMarkersOptions.getTitle - ? rowMarkersOptions.getTitle(rowIndex) - : `Row ${rowIndex + 1}`; + ({ getTitle }) => { + const title = getTitle ? getTitle(rowIndex) : `Row ${rowIndex + 1}`; return { kind: GridCellKind.Text, @@ -133,7 +136,7 @@ function DataGrid(props: DataGridProps) { themeOverride: { bgCell: darkTheme.bgHeader, }, - }; + } satisfies GridCell; }, (adjustedColIndex) => { return getCellContent([adjustedColIndex, rowIndex]); @@ -238,20 +241,16 @@ function DataGrid(props: DataGridProps) { } } - // Prevent selecting the row marker column + // Select/Deselect all the rows like others row markers when selecting the column if (newSelection.columns.hasIndex(0)) { - // TODO find a way to have the rows length to select all the rows like other row markers - // setSelection({ - // ...newSelection, - // columns: CompactSelection.empty(), - // rows: CompactSelection.fromSingleSelection([ - // 0, - // // rowsLength - // ]), - // }); + const isSelectedAll = gridSelection.rows.length === rows; + setGridSelection({ ...newSelection, - columns: newSelection.columns.remove(0), + columns: CompactSelection.empty(), + rows: isSelectedAll + ? CompactSelection.empty() + : CompactSelection.fromSingleSelection([0, rows]), }); return; @@ -279,6 +278,7 @@ function DataGrid(props: DataGridProps) { width="100%" theme={darkTheme} {...rest} + rows={rows} columns={columns} rowMarkers={isStringRowMarkers ? "none" : rowMarkersOptions} getCellContent={getCellContentWrapper} diff --git a/webapp/src/components/common/DataGridForm.tsx b/webapp/src/components/common/DataGridForm.tsx index 0a00e813e9..9f322b8e3b 100644 --- a/webapp/src/components/common/DataGridForm.tsx +++ b/webapp/src/components/common/DataGridForm.tsx @@ -21,7 +21,7 @@ import { } from "@glideapps/glide-data-grid"; import type { DeepPartial } from "react-hook-form"; import { useCallback, useEffect, useMemo, useState } from "react"; -import DataGrid, { type DataGridProps } from "./DataGrid"; +import DataGrid, { type DataGridProps, type RowMarkers } from "./DataGrid"; import { Box, Divider, IconButton, setRef, Tooltip, type SxProps, type Theme } from "@mui/material"; import useUndo, { type Actions } from "use-undo"; import UndoIcon from "@mui/icons-material/Undo"; @@ -36,6 +36,8 @@ import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; import useFormCloseProtection from "@/hooks/useCloseFormSecurity"; import { useUpdateEffect } from "react-use"; import { toError } from "@/utils/fnUtils"; +import { measureTextWidth } from "@/utils/domUtils"; +import useSafeMemo from "@/hooks/useSafeMemo"; type Data = Record>; @@ -72,7 +74,7 @@ function DataGridForm({ columns, allowedFillDirections = "vertical", enableColumnResize, - rowMarkers, + rowMarkers: rowMarkersFromProps, onSubmit, onSubmitSuccessful, onStateChange, @@ -91,8 +93,6 @@ function DataGridForm({ // Deep comparison fix the issue but with big data it can be slow. const isDirty = savedData !== data; - const rowNames = Object.keys(data); - const formState = useMemo( () => ({ isDirty, @@ -107,16 +107,49 @@ function DataGridForm({ useEffect(() => setRef(apiRef, { data, setData, formState }), [apiRef, data, setData, formState]); + // Rows cannot be added or removed, so no dependencies are needed + const rowNames = useSafeMemo(() => Object.keys(defaultData), []); + + const columnsWithAdjustedSize = useMemo( + () => + columns.map((column) => ({ + ...column, + width: + "width" in column + ? column.width + : Math.max( + measureTextWidth(column.title), + ...rowNames.map((rowName) => + measureTextWidth(defaultData[rowName][column.id].toString()), + ), + ), + })), + [columns, defaultData, rowNames], + ); + + const columnIds = useMemo(() => columns.map((column) => column.id), [columns]); + + const rowMarkers = useMemo( + () => + rowMarkersFromProps || { + kind: "clickable-string", + getTitle: (index) => rowNames[index], + width: Math.max(...rowNames.map((name) => measureTextWidth(name))), + }, + [rowMarkersFromProps, rowNames], + ); + //////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////// - const getRowAndColumnNames = (location: Item) => { - const [colIndex, rowIndex] = location; - const columnIds = columns.map((column) => column.id); - - return [rowNames[rowIndex], columnIds[colIndex]]; - }; + const getRowAndColumnNames = useCallback( + (location: Item) => { + const [colIndex, rowIndex] = location; + return [rowNames[rowIndex], columnIds[colIndex]]; + }, + [rowNames, columnIds], + ); const getDirtyValues = () => { return rowNames.reduce((acc, rowName) => { @@ -190,7 +223,7 @@ function DataGridForm({ readonly: true, }; }, - [data, columns], + [data, getRowAndColumnNames], ); //////////////////////////////////////////////////////////////// @@ -261,15 +294,10 @@ function DataGridForm({ > rowNames[index], - } - } + rowMarkers={rowMarkers} fillHandle allowedFillDirections={allowedFillDirections} enableColumnResize={enableColumnResize} diff --git a/webapp/src/components/common/TableMode.tsx b/webapp/src/components/common/TableMode.tsx index a30bfe79fe..138b194919 100644 --- a/webapp/src/components/common/TableMode.tsx +++ b/webapp/src/components/common/TableMode.tsx @@ -73,7 +73,6 @@ function TableMode({ return { title, id: col, - width: title.length * 10, } satisfies GridColumn; }), ); diff --git a/webapp/src/utils/domUtils.ts b/webapp/src/utils/domUtils.ts new file mode 100644 index 0000000000..cc1c85edb3 --- /dev/null +++ b/webapp/src/utils/domUtils.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2025, RTE (https://www.rte-france.com) + * + * See AUTHORS.txt + * + * 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/. + * + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of the Antares project. + */ + +const CANVAS_CONTEXT = document.createElement("canvas").getContext("2d"); +const BODY_FONT = getCssFont(); + +/** + * Gets the computed style of the given element. + * + * @see https://stackoverflow.com/a/21015393 + * + * @param element - The element to get the style from. + * @param prop - The property to get the value of. + * @returns The computed style of the given element. + */ +export function getCssStyle(element: HTMLElement, prop: string) { + return window.getComputedStyle(element, null).getPropertyValue(prop); +} + +/** + * Gets the font of the given element, or the `body` element if none is provided. + * The returned value follows the CSS `font` shorthand property format. + * + * @see https://stackoverflow.com/a/21015393 + * + * @param element - The element to get the font from. + * @returns The font of the given element. + */ +export function getCssFont(element = document.body) { + const fontWeight = getCssStyle(element, "font-weight") || "normal"; + const fontSize = getCssStyle(element, "font-size") || "16px"; + const fontFamily = getCssStyle(element, "font-family") || "Arial"; + + return `${fontWeight} ${fontSize} ${fontFamily}`; +} + +/** + * Uses `canvas.measureText` to compute and return the width of the specified text, + * using the specified canvas font if defined (use font from the "body" otherwise). + * + * @see https://stackoverflow.com/a/21015393 + * + * @param text - The text to be rendered. + * @param [font] - The CSS font that text is to be rendered with (e.g. "bold 14px Arial"). + * If not provided, the font of the `body` element is used. + * @returns The width of the text in pixels. + */ +export function measureTextWidth(text: string, font?: string) { + if (CANVAS_CONTEXT) { + CANVAS_CONTEXT.font = font || BODY_FONT; + return CANVAS_CONTEXT.measureText(text).width; + } + return 0; +} From be08fb8e49a63d2e44fce9c9cac1354e8d8b669a Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:20:45 +0100 Subject: [PATCH 6/7] fix(ui-tablemode): prevent small values from being cut off and remove empty space at the bottom (#2321) --- webapp/src/components/common/DataGrid.tsx | 19 +++++++--- webapp/src/components/common/DataGridForm.tsx | 17 +++------ webapp/src/utils/dataGridUtils.ts | 38 +++++++++++++++++++ 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 webapp/src/utils/dataGridUtils.ts diff --git a/webapp/src/components/common/DataGrid.tsx b/webapp/src/components/common/DataGrid.tsx index 4ef299cd8e..39659f4e24 100644 --- a/webapp/src/components/common/DataGrid.tsx +++ b/webapp/src/components/common/DataGrid.tsx @@ -45,6 +45,9 @@ export interface DataGridProps extends Omit({ rows: CompactSelection.empty(), @@ -268,14 +276,15 @@ function DataGrid({ return ( >; @@ -114,15 +114,9 @@ function DataGridForm({ () => columns.map((column) => ({ ...column, - width: - "width" in column - ? column.width - : Math.max( - measureTextWidth(column.title), - ...rowNames.map((rowName) => - measureTextWidth(defaultData[rowName][column.id].toString()), - ), - ), + width: getColumnWidth(column, () => + rowNames.map((rowName) => defaultData[rowName][column.id].toString()), + ), })), [columns, defaultData, rowNames], ); @@ -134,7 +128,7 @@ function DataGridForm({ rowMarkersFromProps || { kind: "clickable-string", getTitle: (index) => rowNames[index], - width: Math.max(...rowNames.map((name) => measureTextWidth(name))), + width: getColumnWidth({ title: "", id: "" }, () => rowNames), }, [rowMarkersFromProps, rowNames], ); @@ -311,7 +305,6 @@ function DataGridForm({ mt: 1.5, }} > - string[]) { + if ("width" in column) { + return column.width; + } + + const width = Math.max( + measureTextWidth(column.title), + // `values` is a function to prevent unnecessary computation if the column already has a width + ...values().map((value) => measureTextWidth(value)), + ); + + // Under 110px, add a margin to the width to prevent text from being cut off + return width < 110 ? width + 15 : width; +} From 1263f5aea0b79f3fe4c4c10fefa9cc978d69cffa Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:29:16 +0100 Subject: [PATCH 7/7] fix(ui-commons): keep auto size for DataGrid (#2324) --- webapp/src/components/common/DataGrid.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/webapp/src/components/common/DataGrid.tsx b/webapp/src/components/common/DataGrid.tsx index 39659f4e24..40eb3ad8c0 100644 --- a/webapp/src/components/common/DataGrid.tsx +++ b/webapp/src/components/common/DataGrid.tsx @@ -46,7 +46,6 @@ export interface DataGridProps extends Omit({ rows: CompactSelection.empty(), @@ -281,12 +275,8 @@ function DataGrid({ rowHeight={ROW_HEIGHT} smoothScrollX smoothScrollY - overscrollX={OVERSCROLL} - overscrollY={OVERSCROLL} width="100%" - height={height} theme={darkTheme} - {...rest} rows={rows} columns={columns} rowMarkers={isStringRowMarkers ? "none" : rowMarkersOptions} @@ -299,6 +289,7 @@ function DataGrid({ gridSelection={gridSelection} onGridSelectionChange={handleGridSelectionChange} freezeColumns={adjustedFreezeColumns} + {...rest} /> ); }