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] 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; +}