From 2b6b6b1a82577684d0fd2517dbf9406324a27eb1 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 11 Sep 2024 18:28:57 +0200 Subject: [PATCH 01/43] feat(ui): add `customColumns` prop to `Matrix` --- .../components/common/MatrixGrid/Matrix.tsx | 10 ++- .../src/components/common/MatrixGrid/types.ts | 14 ++++ .../components/common/MatrixGrid/useMatrix.ts | 12 ++-- .../src/components/common/MatrixGrid/utils.ts | 65 +++++++++++++++---- 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 128bf480cc..c7b6004417 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -29,6 +29,7 @@ interface MatrixProps { title?: string; enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; + customColumns?: string[]; } function Matrix({ @@ -36,6 +37,7 @@ function Matrix({ title = "global.timeSeries", enableTimeSeriesColumns = true, enableAggregateColumns = false, + customColumns, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -57,7 +59,13 @@ function Matrix({ redo, canUndo, canRedo, - } = useMatrix(study.id, url, enableTimeSeriesColumns, enableAggregateColumns); + } = useMatrix( + study.id, + url, + enableTimeSeriesColumns, + enableAggregateColumns, + customColumns, + ); //////////////////////////////////////////////////////////////// // JSX diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index af94b98d97..016c0c28b9 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -46,6 +46,20 @@ export const Operations = { export type ColumnType = (typeof ColumnTypes)[keyof typeof ColumnTypes]; export type Operation = (typeof Operations)[keyof typeof Operations]; +export interface TimeSeriesColumnOptions { + count: number; + startIndex?: number; + prefix?: string; + width?: number; + editable?: boolean; + style?: BaseGridColumn["style"]; +} + +export interface CustomColumnOptions { + titles: string[]; + width?: number; +} + export interface EnhancedGridColumn extends BaseGridColumn { id: string; width?: number; diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 3332bcfa6e..e5ef022676 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -30,7 +30,7 @@ import { GridUpdate, MatrixUpdateDTO, } from "./types"; -import { generateDateTime, generateTimeSeriesColumns } from "./utils"; +import { generateDataColumns, generateDateTime } from "./utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; import { importFile } from "../../../services/api/studies/raw"; @@ -45,6 +45,7 @@ export function useMatrix( url: string, enableTimeSeriesColumns: boolean, enableAggregateColumns: boolean, + customColumns?: string[], ) { const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [columnCount, setColumnCount] = useState(0); @@ -97,9 +98,11 @@ export function useMatrix( }, ]; - const dataColumns = enableTimeSeriesColumns - ? generateTimeSeriesColumns({ count: columnCount }) - : []; + const dataColumns = generateDataColumns( + enableTimeSeriesColumns, + columnCount, + customColumns, + ); const aggregateColumns = enableAggregateColumns ? [ @@ -130,6 +133,7 @@ export function useMatrix( return [...baseColumns, ...dataColumns, ...aggregateColumns]; }, [ currentState.data, + customColumns, enableTimeSeriesColumns, columnCount, enableAggregateColumns, diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index ed3e1094ee..97e6720d6a 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -17,6 +17,8 @@ import { DateIncrementStrategy, EnhancedGridColumn, ColumnTypes, + TimeSeriesColumnOptions, + CustomColumnOptions, } from "./types"; import { getCurrentLanguage } from "../../../utils/i18nUtils"; import { Theme } from "@glideapps/glide-data-grid"; @@ -195,20 +197,61 @@ export function generateTimeSeriesColumns({ width = 50, editable = true, style = "normal", -}: { - count: number; - startIndex?: number; - prefix?: string; - width?: number; - editable?: boolean; - style?: "normal" | "highlight"; -}): EnhancedGridColumn[] { +}: TimeSeriesColumnOptions): EnhancedGridColumn[] { return Array.from({ length: count }, (_, index) => ({ id: `data${startIndex + index}`, title: `${prefix} ${startIndex + index}`, type: ColumnTypes.Number, - style: style, - width: width, - editable: editable, + style, + width, + editable, })); } + +/** + * Generates custom columns for a matrix grid. + * + * @param customColumns - An array of strings representing the custom column titles. + * @param customColumns.titles - The titles of the custom columns. + * @param customColumns.width - The width of each custom column. + * @returns An array of EnhancedGridColumn objects representing the generated custom columns. + */ +export function generateCustomColumns({ + titles, + width = 100, +}: CustomColumnOptions): EnhancedGridColumn[] { + return titles.map((title, index) => ({ + id: `custom${index + 1}`, + title, + type: ColumnTypes.Number, + style: "normal", + width, + editable: true, + })); +} + +/** + * Generates an array of data columns for a matrix grid. + * + * @param enableTimeSeriesColumns - A boolean indicating whether to enable time series columns. + * @param columnCount - The number of columns to generate. + * @param customColumns - An optional array of custom column titles. + * @returns An array of EnhancedGridColumn objects representing the generated data columns. + */ +export function generateDataColumns( + enableTimeSeriesColumns: boolean, + columnCount: number, + customColumns?: string[], +): EnhancedGridColumn[] { + // If custom columns are provided, use them + if (customColumns) { + return generateCustomColumns({ titles: customColumns }); + } + + // Else, generate time series columns if enabled + if (enableTimeSeriesColumns) { + return generateTimeSeriesColumns({ count: columnCount }); + } + + return []; +} From 5a33677354710013e46b37acf2fc2b4e9617f6d3 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 12 Sep 2024 14:55:02 +0200 Subject: [PATCH 02/43] refactor(ui): update matrix views replacing `MatrixInput` with `Matrix` refactor(ui): update `MiscGen` matrix refactor(ui): update `Reserves` matrix refactor(ui): update `Solar` matrix refactor(ui): update `Wind` matrix refactor(ui): update `Thermal` matrices refactor(ui): update `Renewables` matrix refactor(ui): update `Storages` matrices --- .../explore/Modelization/Areas/MiscGen.tsx | 18 +- .../Modelization/Areas/Renewables/Form.tsx | 13 +- .../Modelization/Areas/Renewables/Matrix.tsx | 38 +-- .../explore/Modelization/Areas/Reserve.tsx | 22 +- .../explore/Modelization/Areas/Solar.tsx | 11 +- .../Modelization/Areas/Storages/Form.tsx | 7 +- .../Modelization/Areas/Storages/Matrix.tsx | 220 +++++++++--------- .../Modelization/Areas/Thermal/Matrix.tsx | 18 +- .../explore/Modelization/Areas/Wind.tsx | 11 +- .../components/common/MatrixGrid/Matrix.tsx | 3 +- .../src/components/common/MatrixGrid/types.ts | 2 +- .../components/common/MatrixGrid/useMatrix.ts | 6 +- .../src/components/common/MatrixGrid/utils.ts | 5 +- 13 files changed, 156 insertions(+), 218 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx index 46451653a9..dde3f72734 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx @@ -15,15 +15,12 @@ import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; -import { Root } from "./style"; +import Matrix from "../../../../../common/MatrixGrid/Matrix"; function MiscGen() { - const { study } = useOutletContext<{ study: StudyMetadata }>(); const currentArea = useAppSelector(getCurrentAreaId); const url = `input/misc-gen/miscgen-${currentArea}`; - const colmunsNames = [ + const columns = [ "CHP", "Bio Mass", "Bio Gaz", @@ -38,16 +35,7 @@ function MiscGen() { // JSX //////////////////////////////////////////////////////////////// - return ( - - - - ); + return ; } export default MiscGen; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx index 0d3b21cf17..a180b0fe05 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx @@ -81,23 +81,16 @@ function Renewables() { config={{ defaultValues }} onSubmit={handleSubmit} enableUndoRedo + sx={{ height: "50%" }} > - + diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx index 4de557f4e5..2948c33584 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx @@ -25,43 +25,17 @@ import { import MatrixInput from "../../../../../../common/MatrixInput"; interface Props { - study: StudyMetadata; areaId: string; clusterId: Cluster["id"]; } -function Matrix({ study, areaId, clusterId }: Props) { - const [t] = useTranslation(); - const [value, setValue] = React.useState(0); - +function RenewablesMatrix({ areaId, clusterId }: Props) { return ( - - setValue(v)} sx={{ width: 1 }}> - - - - - - + ); } -export default Matrix; +export default RenewablesMatrix; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx index 0aae3af44d..279ddebfde 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx @@ -15,31 +15,23 @@ import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; -import { Root } from "./style"; +import Matrix from "../../../../../common/MatrixGrid/Matrix"; function Reserve() { - const { study } = useOutletContext<{ study: StudyMetadata }>(); const currentArea = useAppSelector(getCurrentAreaId); const url = `input/reserves/${currentArea}`; - const colmunsNames = [ + const columns = [ "Primary Res. (draft)", "Strategic Res. (draft)", "DSM", "Day Ahead", ]; - return ( - - - - ); + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ; } export default Reserve; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx index 2aeddedd2d..029146ddc0 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx @@ -15,12 +15,9 @@ import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; -import { Root } from "./style"; +import Matrix from "../../../../../common/MatrixGrid/Matrix"; function Solar() { - const { study } = useOutletContext<{ study: StudyMetadata }>(); const currentArea = useAppSelector(getCurrentAreaId); const url = `input/solar/series/solar_${currentArea}`; @@ -28,11 +25,7 @@ function Solar() { // JSX //////////////////////////////////////////////////////////////// - return ( - - - - ); + return ; } export default Solar; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx index daa773bf9d..22899502df 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx @@ -95,16 +95,13 @@ function Storages() { }} onSubmit={handleSubmit} enableUndoRedo + sx={{ height: "50%" }} > { + setActiveTab(newValue); + }; + + const MATRICES: MatrixItem[] = useMemo( + () => [ + { + titleKey: "modulation", + content: { + type: "split", + matrices: [ + { + url: `input/st-storage/series/${areaId}/${storageId}/pmax_injection`, + titleKey: "injectionModulation", + }, + { + url: `input/st-storage/series/${areaId}/${storageId}/pmax_withdrawal`, + titleKey: "withdrawalModulation", + }, + ], + }, + }, + { + titleKey: "ruleCurves", + content: { + type: "split", + matrices: [ + { + url: `input/st-storage/series/${areaId}/${storageId}/lower_rule_curve`, + titleKey: "lowerRuleCurve", + }, + { + url: `input/st-storage/series/${areaId}/${storageId}/upper_rule_curve`, + titleKey: "upperRuleCurve", + }, + ], + }, + }, + { + titleKey: "inflows", + content: { + type: "single", + matrix: { + url: `input/st-storage/series/${areaId}/${storageId}/inflows`, + titleKey: "inflows", + }, + }, + }, + ], + [areaId, storageId], + ); //////////////////////////////////////////////////////////////// // JSX //////////////////////////////////////////////////////////////// return ( - - setValue(v)}> - - - + + + {MATRICES.map(({ titleKey }) => ( + + ))} - - {R.cond([ - [ - () => value === 0, - () => ( - - } - right={ - + {MATRICES.map( + ({ titleKey, content }) => + activeTab === titleKey && ( + + {content.type === "split" ? ( + + {content.matrices.map(({ url, titleKey }) => ( + + + + ))} + + ) : ( + - } - sx={{ - mt: 1, - ".SplitLayoutView__Left": { - width: "50%", - }, - ".SplitLayoutView__Right": { - height: 1, - width: "50%", - }, - }} - /> - ), - ], - [ - () => value === 1, - () => ( - - } - right={ - - } - sx={{ - mt: 1, - ".SplitLayoutView__Left": { - width: "50%", - }, - ".SplitLayoutView__Right": { - height: 1, - width: "50%", - }, - }} - /> + )} + ), - ], - [ - R.T, - () => ( - - ), - ], - ])()} + )} ); } -export default Matrix; +export default StorageMatrices; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx index e735f20402..60a15a02c6 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx @@ -17,13 +17,9 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; -import { - Cluster, - MatrixStats, - StudyMetadata, -} from "../../../../../../../common/types"; -import MatrixInput from "../../../../../../common/MatrixInput"; +import { Cluster, StudyMetadata } from "../../../../../../../common/types"; import { COMMON_MATRIX_COLS, TS_GEN_MATRIX_COLS } from "./utils"; +import Matrix from "../../../../../../common/MatrixGrid/Matrix"; interface Props { study: StudyMetadata; @@ -31,7 +27,7 @@ interface Props { clusterId: Cluster["id"]; } -function Matrix({ study, areaId, clusterId }: Props) { +function ThermalMatrices({ study, areaId, clusterId }: Props) { const [t] = useTranslation(); const [value, setValue] = useState("common"); const studyVersion = Number(study.version); @@ -113,13 +109,11 @@ function Matrix({ study, areaId, clusterId }: Props) { {filteredMatrices.map( ({ url, titleKey, columns }) => value === titleKey && ( - ), )} @@ -128,4 +122,4 @@ function Matrix({ study, areaId, clusterId }: Props) { ); } -export default Matrix; +export default ThermalMatrices; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx index 39339779c4..af966811f2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx @@ -15,12 +15,9 @@ import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import { MatrixStats, StudyMetadata } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; -import { Root } from "./style"; +import Matrix from "../../../../../common/MatrixGrid/Matrix"; function Wind() { - const { study } = useOutletContext<{ study: StudyMetadata }>(); const currentArea = useAppSelector(getCurrentAreaId); const url = `input/wind/series/wind_${currentArea}`; @@ -28,11 +25,7 @@ function Wind() { // JSX //////////////////////////////////////////////////////////////// - return ( - - - - ); + return ; } export default Wind; diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index c7b6004417..88fb2499c7 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -29,7 +29,8 @@ interface MatrixProps { title?: string; enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; - customColumns?: string[]; + customColumns?: string[] | readonly string[]; + colWidth?: number; } function Matrix({ diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index 016c0c28b9..5168f1e819 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -56,7 +56,7 @@ export interface TimeSeriesColumnOptions { } export interface CustomColumnOptions { - titles: string[]; + titles: string[] | readonly string[]; width?: number; } diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index e5ef022676..083b75c629 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -45,7 +45,8 @@ export function useMatrix( url: string, enableTimeSeriesColumns: boolean, enableAggregateColumns: boolean, - customColumns?: string[], + customColumns?: string[] | readonly string[], + colWidth?: number, ) { const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [columnCount, setColumnCount] = useState(0); @@ -133,9 +134,10 @@ export function useMatrix( return [...baseColumns, ...dataColumns, ...aggregateColumns]; }, [ currentState.data, - customColumns, enableTimeSeriesColumns, columnCount, + customColumns, + colWidth, enableAggregateColumns, ]); diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 97e6720d6a..11c589a615 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -218,7 +218,7 @@ export function generateTimeSeriesColumns({ */ export function generateCustomColumns({ titles, - width = 100, + width, }: CustomColumnOptions): EnhancedGridColumn[] { return titles.map((title, index) => ({ id: `custom${index + 1}`, @@ -241,7 +241,8 @@ export function generateCustomColumns({ export function generateDataColumns( enableTimeSeriesColumns: boolean, columnCount: number, - customColumns?: string[], + customColumns?: string[] | readonly string[], + colWidth?: number, ): EnhancedGridColumn[] { // If custom columns are provided, use them if (customColumns) { From 9f26788042498531c387dffc8ebbb66595810b7a Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 11 Sep 2024 18:38:08 +0200 Subject: [PATCH 03/43] feat(ui): add `colWidth` prop to `Matrix` for custom columns width --- webapp/src/components/common/MatrixGrid/Matrix.tsx | 2 ++ webapp/src/components/common/MatrixGrid/useMatrix.ts | 1 + webapp/src/components/common/MatrixGrid/utils.ts | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 88fb2499c7..79c8fac9cb 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -39,6 +39,7 @@ function Matrix({ enableTimeSeriesColumns = true, enableAggregateColumns = false, customColumns, + colWidth, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -66,6 +67,7 @@ function Matrix({ enableTimeSeriesColumns, enableAggregateColumns, customColumns, + colWidth, ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 083b75c629..94931457c1 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -103,6 +103,7 @@ export function useMatrix( enableTimeSeriesColumns, columnCount, customColumns, + colWidth, ); const aggregateColumns = enableAggregateColumns diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 11c589a615..731e5d2b21 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -236,17 +236,18 @@ export function generateCustomColumns({ * @param enableTimeSeriesColumns - A boolean indicating whether to enable time series columns. * @param columnCount - The number of columns to generate. * @param customColumns - An optional array of custom column titles. + * @param colWidth - The width of each column. * @returns An array of EnhancedGridColumn objects representing the generated data columns. */ export function generateDataColumns( enableTimeSeriesColumns: boolean, columnCount: number, - customColumns?: string[] | readonly string[], + customColumns?: string[], colWidth?: number, ): EnhancedGridColumn[] { // If custom columns are provided, use them if (customColumns) { - return generateCustomColumns({ titles: customColumns }); + return generateCustomColumns({ titles: customColumns, width: colWidth }); } // Else, generate time series columns if enabled From 1357bf8fba9609463f371d5df9974f663f5b9d8c Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 12 Sep 2024 18:07:32 +0200 Subject: [PATCH 04/43] feat(ui): add custom `rowHeaders` prop on `Matrix` --- .../src/components/common/MatrixGrid/Matrix.tsx | 6 ++++++ .../components/common/MatrixGrid/useMatrix.ts | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 79c8fac9cb..0a08e668ac 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -29,8 +29,10 @@ interface MatrixProps { title?: string; enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; + enableRowHeaders?: boolean; customColumns?: string[] | readonly string[]; colWidth?: number; + rowHeaders?: string[]; } function Matrix({ @@ -38,8 +40,10 @@ function Matrix({ title = "global.timeSeries", enableTimeSeriesColumns = true, enableAggregateColumns = false, + enableRowHeaders = false, customColumns, colWidth, + rowHeaders, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -66,6 +70,7 @@ function Matrix({ url, enableTimeSeriesColumns, enableAggregateColumns, + enableRowHeaders, customColumns, colWidth, ); @@ -109,6 +114,7 @@ function Matrix({ data={data} columns={columns} rows={data.length} + rowHeaders={rowHeaders} dateTime={dateTime} onCellEdit={handleCellEdit} onMultipleCellsEdit={handleMultipleCellsEdit} diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 94931457c1..c626521fbe 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -45,6 +45,7 @@ export function useMatrix( url: string, enableTimeSeriesColumns: boolean, enableAggregateColumns: boolean, + enableRowHeaders?: boolean, customColumns?: string[] | readonly string[], colWidth?: number, ) { @@ -85,12 +86,12 @@ export function useMatrix( return index ? generateDateTime(index) : []; }, [index]); - const columns: EnhancedGridColumn[] = useMemo(() => { + const columns = useMemo(() => { if (!currentState.data) { return []; } - const baseColumns = [ + const baseColumns: EnhancedGridColumn[] = [ { id: "date", title: "Date", @@ -99,6 +100,15 @@ export function useMatrix( }, ]; + if (enableRowHeaders) { + baseColumns.unshift({ + id: "rowHeaders", + title: "", + type: ColumnTypes.Text, + editable: false, + }); + } + const dataColumns = generateDataColumns( enableTimeSeriesColumns, columnCount, @@ -106,7 +116,7 @@ export function useMatrix( colWidth, ); - const aggregateColumns = enableAggregateColumns + const aggregateColumns: EnhancedGridColumn[] = enableAggregateColumns ? [ { id: "min", @@ -135,6 +145,7 @@ export function useMatrix( return [...baseColumns, ...dataColumns, ...aggregateColumns]; }, [ currentState.data, + enableRowHeaders, enableTimeSeriesColumns, columnCount, customColumns, From 0f302bef44e72acd54f03a5af59344973f44351e Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 12 Sep 2024 18:35:53 +0200 Subject: [PATCH 05/43] feat(ui): add `enablePercentDisplay` prop on `Matrix` --- .../components/common/MatrixGrid/Matrix.tsx | 3 +++ .../components/common/MatrixGrid/index.tsx | 5 ++++- .../common/MatrixGrid/useGridCellContent.ts | 21 ++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 0a08e668ac..06d00ec503 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -30,6 +30,7 @@ interface MatrixProps { enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; enableRowHeaders?: boolean; + enablePercentDisplay?: boolean; customColumns?: string[] | readonly string[]; colWidth?: number; rowHeaders?: string[]; @@ -41,6 +42,7 @@ function Matrix({ enableTimeSeriesColumns = true, enableAggregateColumns = false, enableRowHeaders = false, + enablePercentDisplay = false, customColumns, colWidth, rowHeaders, @@ -119,6 +121,7 @@ function Matrix({ onCellEdit={handleCellEdit} onMultipleCellsEdit={handleMultipleCellsEdit} readOnly={isSubmitting} + isPercentDisplayEnabled={enablePercentDisplay} /> {openImportDialog && ( void; onMultipleCellsEdit?: (updates: GridUpdate[]) => void; readOnly?: boolean; + isPercentDisplayEnabled?: boolean; } function MatrixGrid({ @@ -52,7 +53,8 @@ function MatrixGrid({ height = "100%", onCellEdit, onMultipleCellsEdit, - readOnly = false, + readOnly, + isPercentDisplayEnabled, }: MatrixGridProps) { const [selection, setSelection] = useState({ columns: CompactSelection.empty(), @@ -80,6 +82,7 @@ function MatrixGrid({ aggregates, rowHeaders, readOnly, + isPercentDisplayEnabled, ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index ca610950a8..96ad5f0d0c 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -101,6 +101,7 @@ const cellContentGenerators: Record = { * @param aggregates - Optional object mapping column IDs to arrays of aggregated values. * @param rowHeaders - Optional array of row header labels. * @param readOnly - Whether the grid is read-only (default is false). + * @param isPercentDisplayEnabled - Whether to display number values as percentages (default is false). * @returns A function that accepts a grid item and returns the configured grid cell content. */ export function useGridCellContent( @@ -111,6 +112,7 @@ export function useGridCellContent( aggregates?: Record, rowHeaders?: string[], readOnly = false, + isPercentDisplayEnabled = false, ): (cell: Item) => GridCell { const columnMap = useMemo(() => { return new Map(columns.map((column, index) => [index, column])); @@ -176,9 +178,26 @@ export function useGridCellContent( }; } + // Display number values as percentages if enabled + if (isPercentDisplayEnabled && gridCell.kind === GridCellKind.Number) { + return { + ...gridCell, + displayData: `${gridCell.data}%`, + }; + } + return gridCell; }, - [columnMap, gridToData, data, dateTime, aggregates, rowHeaders, readOnly], + [ + columnMap, + gridToData, + data, + dateTime, + aggregates, + rowHeaders, + readOnly, + isPercentDisplayEnabled, + ], ); return getCellContent; From 4677c9b29290b93277a8b4ae572ddfc43b388156 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 12 Sep 2024 18:42:32 +0200 Subject: [PATCH 06/43] feat(ui): add `enableReadOnly` prop on `Matrix` --- webapp/src/components/common/MatrixGrid/Matrix.tsx | 14 ++++++++------ webapp/src/components/common/MatrixGrid/index.tsx | 10 +++++----- .../common/MatrixGrid/useGridCellContent.ts | 8 ++++---- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 06d00ec503..9b69edc4e1 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -27,25 +27,27 @@ import EmptyView from "../page/SimpleContent"; interface MatrixProps { url: string; title?: string; + rowHeaders?: string[]; enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; enableRowHeaders?: boolean; enablePercentDisplay?: boolean; + enableReadOnly?: boolean; customColumns?: string[] | readonly string[]; colWidth?: number; - rowHeaders?: string[]; } function Matrix({ url, title = "global.timeSeries", + rowHeaders = [], enableTimeSeriesColumns = true, enableAggregateColumns = false, - enableRowHeaders = false, + enableRowHeaders = rowHeaders.length > 0, enablePercentDisplay = false, - customColumns, + enableReadOnly = false, + customColumns = [], colWidth, - rowHeaders, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -86,7 +88,7 @@ function Matrix({ } if (error) { - return ; + return ; } if (!data || data.length === 0) { @@ -120,7 +122,7 @@ function Matrix({ dateTime={dateTime} onCellEdit={handleCellEdit} onMultipleCellsEdit={handleMultipleCellsEdit} - readOnly={isSubmitting} + isReaOnlyEnabled={isSubmitting || enableReadOnly} isPercentDisplayEnabled={enablePercentDisplay} /> {openImportDialog && ( diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 6df71cdb1f..d9776cbf9a 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -38,7 +38,7 @@ export interface MatrixGridProps { height?: string; onCellEdit?: (update: GridUpdate) => void; onMultipleCellsEdit?: (updates: GridUpdate[]) => void; - readOnly?: boolean; + isReaOnlyEnabled?: boolean; isPercentDisplayEnabled?: boolean; } @@ -53,7 +53,7 @@ function MatrixGrid({ height = "100%", onCellEdit, onMultipleCellsEdit, - readOnly, + isReaOnlyEnabled, isPercentDisplayEnabled, }: MatrixGridProps) { const [selection, setSelection] = useState({ @@ -64,7 +64,7 @@ function MatrixGrid({ const { gridToData } = useColumnMapping(columns); const theme = useMemo(() => { - if (readOnly) { + if (isReaOnlyEnabled) { return { ...darkTheme, ...readOnlyDarkTheme, @@ -72,7 +72,7 @@ function MatrixGrid({ } return darkTheme; - }, [readOnly]); + }, [isReaOnlyEnabled]); const getCellContent = useGridCellContent( data, @@ -81,7 +81,7 @@ function MatrixGrid({ dateTime, aggregates, rowHeaders, - readOnly, + isReaOnlyEnabled, isPercentDisplayEnabled, ); diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index 96ad5f0d0c..67d0718212 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -100,7 +100,7 @@ const cellContentGenerators: Record = { * @param dateTime - Optional array of date-time strings for date columns. * @param aggregates - Optional object mapping column IDs to arrays of aggregated values. * @param rowHeaders - Optional array of row header labels. - * @param readOnly - Whether the grid is read-only (default is false). + * @param isReadOnlyEnabled - Whether the grid is read-only (default is false). * @param isPercentDisplayEnabled - Whether to display number values as percentages (default is false). * @returns A function that accepts a grid item and returns the configured grid cell content. */ @@ -111,7 +111,7 @@ export function useGridCellContent( dateTime?: string[], aggregates?: Record, rowHeaders?: string[], - readOnly = false, + isReadOnlyEnabled = false, isPercentDisplayEnabled = false, ): (cell: Item) => GridCell { const columnMap = useMemo(() => { @@ -171,7 +171,7 @@ export function useGridCellContent( ); // Prevent updates for read-only grids - if (readOnly) { + if (isReadOnlyEnabled) { return { ...gridCell, allowOverlay: false, @@ -195,7 +195,7 @@ export function useGridCellContent( dateTime, aggregates, rowHeaders, - readOnly, + isReadOnlyEnabled, isPercentDisplayEnabled, ], ); From 5254febbff9fe96d7cd62503cc0281eb6c528ee8 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 13 Sep 2024 11:27:10 +0200 Subject: [PATCH 07/43] refactor(ui-hydro): update `HydroMatrix` to use glide data grid --- .../Areas/Hydro/Allocation/index.tsx | 4 +- .../Areas/Hydro/Correlation/index.tsx | 4 +- .../Modelization/Areas/Hydro/HydroMatrix.tsx | 25 ++-- .../explore/Modelization/Areas/Hydro/style.ts | 10 -- .../explore/Modelization/Areas/Hydro/utils.ts | 107 ++++++++---------- .../components/common/MatrixGrid/Matrix.tsx | 2 +- 6 files changed, 62 insertions(+), 90 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/index.tsx index 8231ad0613..fc50dff4a8 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/index.tsx @@ -27,7 +27,7 @@ import { } from "./utils"; import { SubmitHandlerPlus } from "../../../../../../../common/Form/types"; import HydroMatrixDialog from "../HydroMatrixDialog"; -import { HydroMatrixType } from "../utils"; +import { HydroMatrix } from "../utils"; import { FormBox, FormPaper } from "../style"; import ViewMatrixButton from "../ViewMatrixButton"; @@ -84,7 +84,7 @@ function Allocation() { {matrixDialogOpen && ( setMatrixDialogOpen(false)} /> diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/index.tsx index d6dce25747..df566ba68d 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/index.tsx @@ -27,7 +27,7 @@ import { } from "./utils"; import Fields from "./Fields"; import HydroMatrixDialog from "../HydroMatrixDialog"; -import { HydroMatrixType } from "../utils"; +import { HydroMatrix } from "../utils"; import { FormBox, FormPaper } from "../style"; import ViewMatrixButton from "../ViewMatrixButton"; @@ -79,7 +79,7 @@ function Correlation() { {matrixDialogOpen && ( setMatrixDialogOpen(false)} /> diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index ff6218de35..f557f39bf3 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -16,16 +16,14 @@ import { useOutletContext } from "react-router"; import { StudyMetadata } from "../../../../../../../common/types"; import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../../redux/selectors"; -import MatrixInput from "../../../../../../common/MatrixInput"; -import { Root } from "./style"; import { MATRICES, HydroMatrixType } from "./utils"; +import Matrix from "../../../../../../common/MatrixGrid/Matrix"; interface Props { type: HydroMatrixType; } function HydroMatrix({ type }: Props) { - const { study } = useOutletContext<{ study: StudyMetadata }>(); const areaId = useAppSelector(getCurrentAreaId); const hydroMatrix = MATRICES[type]; @@ -35,19 +33,14 @@ function HydroMatrix({ type }: Props) { //////////////////////////////////////////////////////////////// return ( - - - + ); } diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/style.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/style.ts index b707cafec5..b329679cc6 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/style.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/style.ts @@ -14,16 +14,6 @@ import { styled, Box, Paper } from "@mui/material"; -export const Root = styled(Box)(({ theme }) => ({ - width: "100%", - height: "100%", - padding: theme.spacing(2), - paddingTop: 0, - marginTop: theme.spacing(1), - display: "flex", - overflowY: "auto", -})); - export const FormBox = styled(Box)(({ theme }) => ({ width: "100%", height: "100%", diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 316745826e..5d95859a2d 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -22,34 +22,34 @@ import InflowStructure from "./InflowStructure"; // Enums //////////////////////////////////////////////////////////////// -export enum HydroMatrixType { - Dailypower, - EnergyCredits, - ReservoirLevels, - WaterValues, - HydroStorage, - RunOfRiver, - MinGen, - InflowPattern, - OverallMonthlyHydro, - Allocation, - Correlation, -} +export const HydroMatrix = { + Dailypower: "Dailypower", + EnergyCredits: "EnergyCredits", + ReservoirLevels: "ReservoirLevels", + WaterValues: "WaterValues", + HydroStorage: "HydroStorage", + RunOfRiver: "RunOfRiver", + MinGen: "MinGen", + InflowPattern: "InflowPattern", + OverallMonthlyHydro: "OverallMonthlyHydro", + Allocation: "Allocation", + Correlation: "Correlation", +} as const; //////////////////////////////////////////////////////////////// // Types //////////////////////////////////////////////////////////////// export type fetchMatrixFn = (studyId: string) => Promise; +export type HydroMatrixType = (typeof HydroMatrix)[keyof typeof HydroMatrix]; export interface HydroMatrixProps { title: string; url: string; - cols?: string[]; - rows?: string[]; - stats: MatrixStats; + columns?: string[]; + rowHeaders?: string[]; fetchFn?: fetchMatrixFn; - disableEdit?: boolean; + enableReadOnly?: boolean; enablePercentDisplay?: boolean; } @@ -57,7 +57,7 @@ type Matrices = Record; export interface HydroRoute { path: string; - type: number; + type: HydroMatrixType; isSplitView?: boolean; splitConfig?: { direction: SplitViewProps["direction"]; @@ -79,112 +79,104 @@ export interface AreaCoefficientItem { export const HYDRO_ROUTES: HydroRoute[] = [ { path: "inflow-structure", - type: HydroMatrixType.InflowPattern, + type: HydroMatrix.InflowPattern, isSplitView: true, splitConfig: { direction: "horizontal", - partnerType: HydroMatrixType.OverallMonthlyHydro, + partnerType: HydroMatrix.OverallMonthlyHydro, sizes: [50, 50], }, form: InflowStructure, }, { path: "dailypower&energy", - type: HydroMatrixType.Dailypower, + type: HydroMatrix.Dailypower, isSplitView: true, splitConfig: { direction: "vertical", - partnerType: HydroMatrixType.EnergyCredits, + partnerType: HydroMatrix.EnergyCredits, sizes: [30, 70], }, }, { path: "reservoirlevels", - type: HydroMatrixType.ReservoirLevels, + type: HydroMatrix.ReservoirLevels, }, { path: "watervalues", - type: HydroMatrixType.WaterValues, + type: HydroMatrix.WaterValues, }, { path: "hydrostorage", - type: HydroMatrixType.HydroStorage, + type: HydroMatrix.HydroStorage, }, { path: "ror", - type: HydroMatrixType.RunOfRiver, + type: HydroMatrix.RunOfRiver, }, { path: "mingen", - type: HydroMatrixType.MinGen, + type: HydroMatrix.MinGen, }, ]; export const MATRICES: Matrices = { - [HydroMatrixType.Dailypower]: { + [HydroMatrix.Dailypower]: { title: "Credit Modulations", url: "input/hydro/common/capacity/creditmodulations_{areaId}", - cols: generateColumns("%"), - rows: ["Generating Power", "Pumping Power"], - stats: MatrixStats.NOCOL, + columns: generateColumns("%"), + rowHeaders: ["Generating Power", "Pumping Power"], enablePercentDisplay: true, }, - [HydroMatrixType.EnergyCredits]: { + [HydroMatrix.EnergyCredits]: { title: "Standard Credits", url: "input/hydro/common/capacity/maxpower_{areaId}", - cols: [ + columns: [ "Generating Max Power (MW)", "Generating Max Energy (Hours at Pmax)", "Pumping Max Power (MW)", "Pumping Max Energy (Hours at Pmax)", ], - stats: MatrixStats.NOCOL, }, - [HydroMatrixType.ReservoirLevels]: { + [HydroMatrix.ReservoirLevels]: { title: "Reservoir Levels", url: "input/hydro/common/capacity/reservoir_{areaId}", - cols: ["Lev Low (%)", "Lev Avg (%)", "Lev High (%)"], - stats: MatrixStats.NOCOL, + columns: ["Lev Low (%)", "Lev Avg (%)", "Lev High (%)"], enablePercentDisplay: true, }, - [HydroMatrixType.WaterValues]: { + [HydroMatrix.WaterValues]: { title: "Water Values", url: "input/hydro/common/capacity/waterValues_{areaId}", - cols: generateColumns("%"), - stats: MatrixStats.NOCOL, + // columns: generateColumns("%"), // TODO this causes the data is undefined error }, - [HydroMatrixType.HydroStorage]: { + [HydroMatrix.HydroStorage]: { title: "Hydro Storage", url: "input/hydro/series/{areaId}/mod", - stats: MatrixStats.STATS, }, - [HydroMatrixType.RunOfRiver]: { + [HydroMatrix.RunOfRiver]: { title: "Run Of River", url: "input/hydro/series/{areaId}/ror", - stats: MatrixStats.STATS, }, - [HydroMatrixType.MinGen]: { + [HydroMatrix.MinGen]: { title: "Min Gen", url: "input/hydro/series/{areaId}/mingen", - stats: MatrixStats.STATS, }, - [HydroMatrixType.InflowPattern]: { + [HydroMatrix.InflowPattern]: { title: "Inflow Pattern", url: "input/hydro/common/capacity/inflowPattern_{areaId}", - cols: ["Inflow Pattern (X)"], - stats: MatrixStats.NOCOL, + columns: ["Inflow Pattern (X)"], }, - [HydroMatrixType.OverallMonthlyHydro]: { + [HydroMatrix.OverallMonthlyHydro]: { title: "Overall Monthly Hydro", url: "input/hydro/prepro/{areaId}/energy", - cols: [ + columns: [ "Expectation (MWh)", "Std Deviation (MWh)", "Min. (MWh)", "Max. (MWh)", "ROR Share", ], - rows: [ + rowHeaders: [ "January", "February", "March", @@ -198,22 +190,19 @@ export const MATRICES: Matrices = { "November", "December", ], - stats: MatrixStats.NOCOL, }, - [HydroMatrixType.Allocation]: { + [HydroMatrix.Allocation]: { title: "Allocation", url: "", - stats: MatrixStats.NOCOL, fetchFn: getAllocationMatrix, - disableEdit: true, + enableReadOnly: true, enablePercentDisplay: true, }, - [HydroMatrixType.Correlation]: { + [HydroMatrix.Correlation]: { title: "Correlation", url: "", - stats: MatrixStats.NOCOL, fetchFn: getCorrelationMatrix, - disableEdit: true, + enableReadOnly: true, enablePercentDisplay: true, }, }; diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 9b69edc4e1..4de4ae62aa 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -46,7 +46,7 @@ function Matrix({ enableRowHeaders = rowHeaders.length > 0, enablePercentDisplay = false, enableReadOnly = false, - customColumns = [], + customColumns, colWidth, }: MatrixProps) { const { t } = useTranslation(); From 97d901a0ed054e193dfc9adf77f51d38101f0df8 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 17 Sep 2024 18:39:00 +0200 Subject: [PATCH 08/43] feat(ui): add `calculateMatrixAggregates` and update Hydro matrices --- .../Areas/Hydro/Allocation/utils.ts | 3 +- .../Areas/Hydro/Correlation/utils.ts | 3 +- .../Modelization/Areas/Hydro/HydroMatrix.tsx | 21 ++-- .../Modelization/Areas/Hydro/index.tsx | 4 +- .../explore/Modelization/Areas/Hydro/utils.ts | 8 +- .../explore/Modelization/Areas/Load.tsx | 2 +- .../components/common/MatrixGrid/Matrix.tsx | 19 ++- .../components/common/MatrixGrid/index.tsx | 4 +- .../src/components/common/MatrixGrid/types.ts | 8 ++ .../MatrixGrid/useGridCellContent.test.ts | 37 ++++-- .../common/MatrixGrid/useGridCellContent.ts | 29 +++-- .../common/MatrixGrid/useMatrix.test.tsx | 2 +- .../components/common/MatrixGrid/useMatrix.ts | 119 +++++++++++++----- .../common/MatrixGrid/utils.test.ts | 87 +++++++++++-- .../src/components/common/MatrixGrid/utils.ts | 67 +++++++++- 15 files changed, 330 insertions(+), 83 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts index 46041fdcc2..75cbf73862 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts @@ -18,6 +18,7 @@ import { MatrixType, } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; +import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// @@ -58,7 +59,7 @@ export async function setAllocationFormFields( export const getAllocationMatrix = async ( studyId: StudyMetadata["id"], -): Promise => { +): Promise => { const res = await client.get( `v1/studies/${studyId}/areas/hydro/allocation/matrix`, ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts index dc2b547e63..bcf4137a36 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts @@ -18,6 +18,7 @@ import { MatrixType, } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; +import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// @@ -58,7 +59,7 @@ export async function setCorrelationFormFields( export async function getCorrelationMatrix( studyId: StudyMetadata["id"], -): Promise { +): Promise { const res = await client.get( `v1/studies/${studyId}/areas/hydro/correlation/matrix`, ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index f557f39bf3..33a3f3461b 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -18,6 +18,7 @@ import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../../redux/selectors"; import { MATRICES, HydroMatrixType } from "./utils"; import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import { Box } from "@mui/material"; interface Props { type: HydroMatrixType; @@ -33,14 +34,18 @@ function HydroMatrix({ type }: Props) { //////////////////////////////////////////////////////////////// return ( - + + + ); } diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx index 4209e439bf..215b3b5954 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/index.tsx @@ -50,7 +50,9 @@ function Hydro() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ( + + ); } export default Hydro; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 5d95859a2d..e067c36b31 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -40,7 +40,7 @@ export const HydroMatrix = { // Types //////////////////////////////////////////////////////////////// -export type fetchMatrixFn = (studyId: string) => Promise; +export type fetchMatrixFn = (studyId: string) => Promise; export type HydroMatrixType = (typeof HydroMatrix)[keyof typeof HydroMatrix]; export interface HydroMatrixProps { @@ -49,6 +49,7 @@ export interface HydroMatrixProps { columns?: string[]; rowHeaders?: string[]; fetchFn?: fetchMatrixFn; + enableDateTimeColumn?: boolean; enableReadOnly?: boolean; enablePercentDisplay?: boolean; } @@ -126,6 +127,7 @@ export const MATRICES: Matrices = { url: "input/hydro/common/capacity/creditmodulations_{areaId}", columns: generateColumns("%"), rowHeaders: ["Generating Power", "Pumping Power"], + enableDateTimeColumn: false, enablePercentDisplay: true, }, [HydroMatrix.EnergyCredits]: { @@ -147,7 +149,7 @@ export const MATRICES: Matrices = { [HydroMatrix.WaterValues]: { title: "Water Values", url: "input/hydro/common/capacity/waterValues_{areaId}", - // columns: generateColumns("%"), // TODO this causes the data is undefined error + // columns: generateColumns("%"), // TODO this causes Runtime error to be fixed }, [HydroMatrix.HydroStorage]: { title: "Hydro Storage", @@ -195,6 +197,7 @@ export const MATRICES: Matrices = { title: "Allocation", url: "", fetchFn: getAllocationMatrix, + enableDateTimeColumn: false, enableReadOnly: true, enablePercentDisplay: true, }, @@ -202,6 +205,7 @@ export const MATRICES: Matrices = { title: "Correlation", url: "", fetchFn: getCorrelationMatrix, + enableDateTimeColumn: false, enableReadOnly: true, enablePercentDisplay: true, }, diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx index a9d67519de..b6c051dc41 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx @@ -24,7 +24,7 @@ function Load() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ; } export default Load; diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 4de4ae62aa..6affd46d11 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -23,11 +23,13 @@ import { StudyMetadata } from "../../../common/types"; import { MatrixContainer, MatrixHeader, MatrixTitle } from "./style"; import MatrixActions from "./MatrixActions"; import EmptyView from "../page/SimpleContent"; +import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; interface MatrixProps { url: string; title?: string; - rowHeaders?: string[]; + customRowHeaders?: string[]; + enableDateTimeColumn?: boolean; enableTimeSeriesColumns?: boolean; enableAggregateColumns?: boolean; enableRowHeaders?: boolean; @@ -35,19 +37,22 @@ interface MatrixProps { enableReadOnly?: boolean; customColumns?: string[] | readonly string[]; colWidth?: number; + fetchMatrixData?: fetchMatrixFn; } function Matrix({ url, title = "global.timeSeries", - rowHeaders = [], + customRowHeaders = [], + enableDateTimeColumn = true, enableTimeSeriesColumns = true, enableAggregateColumns = false, - enableRowHeaders = rowHeaders.length > 0, + enableRowHeaders = customRowHeaders.length > 0, enablePercentDisplay = false, enableReadOnly = false, customColumns, colWidth, + fetchMatrixData, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -55,6 +60,7 @@ function Matrix({ const { data, + aggregates, error, isLoading, isSubmitting, @@ -72,11 +78,13 @@ function Matrix({ } = useMatrix( study.id, url, + enableDateTimeColumn, enableTimeSeriesColumns, enableAggregateColumns, enableRowHeaders, customColumns, colWidth, + fetchMatrixData, ); //////////////////////////////////////////////////////////////// @@ -88,7 +96,7 @@ function Matrix({ } if (error) { - return ; + return ; } if (!data || data.length === 0) { @@ -116,9 +124,10 @@ function Matrix({ ; + aggregates?: MatrixAggregates; rowHeaders?: string[]; width?: string; height?: string; diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index 5168f1e819..87f7b80f3f 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -66,6 +66,14 @@ export interface EnhancedGridColumn extends BaseGridColumn { type: ColumnType; editable: boolean; } + +export interface MatrixAggregates { + min: number[]; + max: number[]; + avg: number[]; + total: number[]; +} + // Represents data coming from the API export interface MatrixDataDTO { data: number[][]; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts index 084a52d429..11485c9f0c 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts @@ -14,7 +14,11 @@ import { renderHook } from "@testing-library/react"; import { useGridCellContent } from "./useGridCellContent"; -import { ColumnTypes, type EnhancedGridColumn } from "./types"; +import { + ColumnTypes, + MatrixAggregates, + type EnhancedGridColumn, +} from "./types"; import { useColumnMapping } from "./useColumnMapping"; // Mocking i18next @@ -55,7 +59,7 @@ function renderGridCellContent( data: number[][], columns: EnhancedGridColumn[], dateTime?: string[], - aggregates?: Record, + aggregates?: MatrixAggregates, rowHeaders?: string[], ) { const { result: mappingResult } = renderHook(() => useColumnMapping(columns)); @@ -130,14 +134,17 @@ describe("useGridCellContent", () => { ]; const aggregates = { - total: [60, 75, 45], + min: [5, 15, 25], + max: [15, 25, 35], + avg: [10, 20, 30], + total: [30, 60, 90], }; - // Tests for each row in the aggregates array + // Test each row for correct numeric cell content test.each([ - [0, 60], // Row index 0, expected sum 60 - [1, 75], // Row index 1, expected sum 75 - [2, 45], // Row index 2, expected sum 45 + [0, 30], // Total of first row + [1, 60], // Total of second row + [2, 90], // Total of third row ])( "ensures the correct numeric cell content is returned for aggregates at row %i", (row, expectedData) => { @@ -148,7 +155,7 @@ describe("useGridCellContent", () => { aggregates, ); - const cell = getCellContent([0, row]); // Column index is 0 because we only have one column of aggregates + const cell = getCellContent([0, row]); // Accessing the only column if ("data" in cell) { expect(cell.kind).toBe("number"); @@ -200,7 +207,10 @@ describe("useGridCellContent", () => { ]; const aggregates = { - total: [300, 400], + min: [100, 200], + max: [150, 250], + avg: [125, 225], + total: [250, 450], }; const getCellContent = renderGridCellContent( @@ -232,7 +242,7 @@ describe("useGridCellContent", () => { const aggregateCell = getCellContent([3, 0]); if (aggregateCell.kind === "number" && "data" in aggregateCell) { - expect(aggregateCell.data).toBe(300); + expect(aggregateCell.data).toBe(250); } else { throw new Error("Expected an Aggregate cell with data"); } @@ -286,7 +296,10 @@ describe("useGridCellContent with mixed column types", () => { [150, 250], ]; const aggregates = { - total: [300, 400], + min: [100, 200], + max: [150, 250], + avg: [125, 225], + total: [250, 450], }; const getCellContent = renderGridCellContent( @@ -339,7 +352,7 @@ describe("useGridCellContent with mixed column types", () => { const aggregateCell = getCellContent([4, 0]); if (aggregateCell.kind === "number" && "data" in aggregateCell) { - expect(aggregateCell.data).toBe(300); + expect(aggregateCell.data).toBe(250); } else { throw new Error("Expected a number cell with data for aggregate column"); } diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index 67d0718212..7e385a448c 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -14,7 +14,12 @@ import { useCallback, useMemo } from "react"; import { GridCell, GridCellKind, Item } from "@glideapps/glide-data-grid"; -import { type EnhancedGridColumn, type ColumnType, ColumnTypes } from "./types"; +import { + type EnhancedGridColumn, + type ColumnType, + ColumnTypes, + MatrixAggregates, +} from "./types"; import { formatDateTime } from "./utils"; type CellContentGenerator = ( @@ -23,7 +28,7 @@ type CellContentGenerator = ( column: EnhancedGridColumn, data: number[][], dateTime?: string[], - aggregates?: Record, + aggregates?: MatrixAggregates, rowHeaders?: string[], ) => GridCell; @@ -66,14 +71,14 @@ const cellContentGenerators: Record = { }; }, [ColumnTypes.Aggregate]: (row, col, column, data, dateTime, aggregates) => { - const value = aggregates?.[column.id]?.[row]; + const value = aggregates?.[column.id as keyof MatrixAggregates]?.[row]; return { kind: GridCellKind.Number, data: value, displayData: value?.toString() ?? "", readonly: !column.editable, - allowOverlay: false, + allowOverlay: true, }; }, }; @@ -109,7 +114,7 @@ export function useGridCellContent( columns: EnhancedGridColumn[], gridToData: (cell: Item) => Item | null, dateTime?: string[], - aggregates?: Record, + aggregates?: MatrixAggregates, rowHeaders?: string[], isReadOnlyEnabled = false, isPercentDisplayEnabled = false, @@ -170,19 +175,21 @@ export function useGridCellContent( rowHeaders, ); - // Prevent updates for read-only grids - if (isReadOnlyEnabled) { + // Display number values as percentages if enabled + if (isPercentDisplayEnabled && gridCell.kind === GridCellKind.Number) { return { ...gridCell, - allowOverlay: false, + displayData: `${gridCell.data}%`, + // If ReadOnly is enabled, we don't want to allow overlay + allowOverlay: !isReadOnlyEnabled, }; } - // Display number values as percentages if enabled - if (isPercentDisplayEnabled && gridCell.kind === GridCellKind.Number) { + // Prevent updates for read-only grids + if (isReadOnlyEnabled) { return { ...gridCell, - displayData: `${gridCell.data}%`, + allowOverlay: false, }; } diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx index aad25a253f..f6de8f7596 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx +++ b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx @@ -57,7 +57,7 @@ describe("useMatrix", () => { vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); const { result } = renderHook(() => - useMatrix(mockStudyId, mockUrl, true, true), + useMatrix(mockStudyId, mockUrl, true, true, true), ); await waitFor(() => { diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index c626521fbe..e219845c9f 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -29,25 +29,35 @@ import { ColumnTypes, GridUpdate, MatrixUpdateDTO, + MatrixAggregates, } from "./types"; -import { generateDataColumns, generateDateTime } from "./utils"; +import { + aggregatesTheme, + calculateMatrixAggregates, + generateDataColumns, + generateDateTime, +} from "./utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; import { importFile } from "../../../services/api/studies/raw"; +import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; interface DataState { - data: number[][]; + data: MatrixDataDTO["data"]; + aggregates: MatrixAggregates; pendingUpdates: MatrixUpdateDTO[]; } export function useMatrix( studyId: string, url: string, + enableDateTimeColumn: boolean, enableTimeSeriesColumns: boolean, enableAggregateColumns: boolean, enableRowHeaders?: boolean, customColumns?: string[] | readonly string[], colWidth?: number, + fetchMatrixData?: fetchMatrixFn, ) { const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [columnCount, setColumnCount] = useState(0); @@ -56,27 +66,60 @@ export function useMatrix( const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(undefined); const [{ present: currentState }, { set: setState, undo, redo, canRedo }] = - useUndo({ data: [], pendingUpdates: [] }); + useUndo({ + data: [], + aggregates: { min: [], max: [], avg: [], total: [] }, + pendingUpdates: [], + }); - const fetchMatrix = useCallback(async () => { - setIsLoading(true); - try { - const [matrix, index] = await Promise.all([ - getStudyData(studyId, url), - getStudyMatrixIndex(studyId, url), - ]); - - setState({ data: matrix.data, pendingUpdates: [] }); - setColumnCount(matrix.columns.length); - setIndex(index); - setIsLoading(false); - } catch (error) { - setError(new Error(t("data.error.matrix"))); - enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); - } finally { - setIsLoading(false); - } - }, [enqueueErrorSnackbar, setState, studyId, url]); + const fetchMatrix = useCallback( + async (loadingState = true) => { + // !NOTE This is a temporary solution to ensure the matrix is up to date + // TODO: Remove this once the matrix API is updated to return the correct data + if (loadingState) { + setIsLoading(true); + } + + try { + const [matrix, index] = await Promise.all([ + fetchMatrixData + ? // If a custom fetch function is provided, use it + fetchMatrixData(studyId) + : getStudyData(studyId, url, 1), + getStudyMatrixIndex(studyId, url), + ]); + + setState({ + data: matrix.data, + aggregates: enableAggregateColumns + ? calculateMatrixAggregates(matrix.data) + : { min: [], max: [], avg: [], total: [] }, + pendingUpdates: [], + }); + setColumnCount(matrix.columns.length); + setIndex(index); + setIsLoading(false); + + return { + matrix, + index, + }; + } catch (error) { + setError(new Error(t("data.error.matrix"))); + enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); + } finally { + setIsLoading(false); + } + }, + [ + enableAggregateColumns, + enqueueErrorSnackbar, + fetchMatrixData, + setState, + studyId, + url, + ], + ); useEffect(() => { fetchMatrix(); @@ -91,14 +134,16 @@ export function useMatrix( return []; } - const baseColumns: EnhancedGridColumn[] = [ - { + const baseColumns: EnhancedGridColumn[] = []; + + if (enableDateTimeColumn) { + baseColumns.push({ id: "date", title: "Date", type: ColumnTypes.DateTime, editable: false, - }, - ]; + }); + } if (enableRowHeaders) { baseColumns.unshift({ @@ -122,22 +167,22 @@ export function useMatrix( id: "min", title: "Min", type: ColumnTypes.Aggregate, - width: 50, editable: false, + themeOverride: aggregatesTheme, }, { id: "max", title: "Max", type: ColumnTypes.Aggregate, - width: 50, editable: false, + themeOverride: aggregatesTheme, }, { id: "avg", title: "Avg", type: ColumnTypes.Aggregate, - width: 50, editable: false, + themeOverride: aggregatesTheme, }, ] : []; @@ -145,6 +190,7 @@ export function useMatrix( return [...baseColumns, ...dataColumns, ...aggregateColumns]; }, [ currentState.data, + enableDateTimeColumn, enableRowHeaders, enableTimeSeriesColumns, columnCount, @@ -180,6 +226,7 @@ export function useMatrix( setState({ data: updatedData, + aggregates: currentState.aggregates, pendingUpdates: [...currentState.pendingUpdates, ...newUpdates], }); }, @@ -209,12 +256,23 @@ export function useMatrix( } setIsSubmitting(true); + try { await updateMatrix(studyId, url, currentState.pendingUpdates); - setState({ data: currentState.data, pendingUpdates: [] }); + + setState({ + data: currentState.data, + aggregates: currentState.aggregates, + pendingUpdates: [], + }); + enqueueSnackbar(t("matrix.success.matrixUpdate"), { variant: "success", }); + + // !NOTE This is a temporary solution to ensure the matrix is up to date + // TODO: Remove this once the matrix API is updated to return the correct data + await fetchMatrix(false); } catch (error) { setError(new Error(t("matrix.error.matrixUpdate"))); enqueueErrorSnackbar(t("matrix.error.matrixUpdate"), error as AxiosError); @@ -238,6 +296,7 @@ export function useMatrix( return { data: currentState.data, + aggregates: currentState.aggregates, error, isLoading, isSubmitting, diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index afe4e21c5c..b6444c43cb 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -17,7 +17,11 @@ import { StudyOutputDownloadLevelDTO, } from "../../../common/types"; import { ColumnTypes } from "./types"; -import { generateDateTime, generateTimeSeriesColumns } from "./utils"; +import { + calculateMatrixAggregates, + generateDateTime, + generateTimeSeriesColumns, +} from "./utils"; describe("generateDateTime", () => { test("generates correct number of dates", () => { @@ -118,7 +122,6 @@ describe("generateTimeSeriesColumns", () => { title: "TS 1", type: ColumnTypes.Number, style: "normal", - width: 50, editable: true, }, { @@ -126,7 +129,6 @@ describe("generateTimeSeriesColumns", () => { title: "TS 2", type: ColumnTypes.Number, style: "normal", - width: 50, editable: true, }, { @@ -134,7 +136,6 @@ describe("generateTimeSeriesColumns", () => { title: "TS 3", type: ColumnTypes.Number, style: "normal", - width: 50, editable: true, }, ]); @@ -145,7 +146,6 @@ describe("generateTimeSeriesColumns", () => { count: 2, startIndex: 10, prefix: "Data", - width: 80, editable: false, }); expect(result).toEqual([ @@ -154,7 +154,6 @@ describe("generateTimeSeriesColumns", () => { title: "Data 10", type: ColumnTypes.Number, style: "normal", - width: 80, editable: false, }, { @@ -162,7 +161,6 @@ describe("generateTimeSeriesColumns", () => { title: "Data 11", type: ColumnTypes.Number, style: "normal", - width: 80, editable: false, }, ]); @@ -188,3 +186,78 @@ describe("generateTimeSeriesColumns", () => { }); }); }); + +describe("calculateMatrixAggregates", () => { + it("should calculate correct aggregates for a simple matrix", () => { + const matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]; + const result = calculateMatrixAggregates(matrix); + + expect(result.min).toEqual([1, 4, 7]); + expect(result.max).toEqual([3, 6, 9]); + expect(result.avg).toEqual([2, 5, 8]); + expect(result.total).toEqual([6, 15, 24]); + }); + + it("should handle decimal numbers correctly by rounding", () => { + const matrix = [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 6.6], + ]; + const result = calculateMatrixAggregates(matrix); + + expect(result.min).toEqual([1.1, 4.4]); + expect(result.max).toEqual([3.3, 6.6]); + expect(result.avg).toEqual([2, 6]); + expect(result.total).toEqual([7, 17]); + }); + + it("should handle negative numbers", () => { + const matrix = [ + [-1, -2, -3], + [-4, 0, 4], + ]; + const result = calculateMatrixAggregates(matrix); + + expect(result.min).toEqual([-3, -4]); + expect(result.max).toEqual([-1, 4]); + expect(result.avg).toEqual([-2, 0]); + expect(result.total).toEqual([-6, 0]); + }); + + it("should handle single-element rows", () => { + const matrix = [[1], [2], [3]]; + const result = calculateMatrixAggregates(matrix); + + expect(result.min).toEqual([1, 2, 3]); + expect(result.max).toEqual([1, 2, 3]); + expect(result.avg).toEqual([1, 2, 3]); + expect(result.total).toEqual([1, 2, 3]); + }); + + it("should handle large numbers", () => { + const matrix = [ + [1000000, 2000000, 3000000], + [4000000, 5000000, 6000000], + ]; + const result = calculateMatrixAggregates(matrix); + + expect(result.min).toEqual([1000000, 4000000]); + expect(result.max).toEqual([3000000, 6000000]); + expect(result.avg).toEqual([2000000, 5000000]); + expect(result.total).toEqual([6000000, 15000000]); + }); + + it("should round average correctly", () => { + const matrix = [ + [1, 2, 4], + [10, 20, 39], + ]; + const result = calculateMatrixAggregates(matrix); + + expect(result.avg).toEqual([2, 23]); + }); +}); diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 731e5d2b21..77a0482eb5 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -19,6 +19,7 @@ import { ColumnTypes, TimeSeriesColumnOptions, CustomColumnOptions, + MatrixAggregates, } from "./types"; import { getCurrentLanguage } from "../../../utils/i18nUtils"; import { Theme } from "@glideapps/glide-data-grid"; @@ -71,6 +72,23 @@ export const readOnlyDarkTheme: Partial = { drilldownBorder: "rgba(255, 255, 255, 0.2)", }; +export const aggregatesTheme: Partial = { + bgCell: "#31324A", + bgCellMedium: "#383A5C", + textDark: "#1976D2", + textMedium: "#2196F3", + textLight: "#64B5F6", + accentColor: "#2196F3", + accentLight: "#64B5F633", + fontFamily: "Inter, sans-serif", + baseFontStyle: "bold 13px", + editorFontSize: "13px", + headerFontStyle: "bold 11px", + accentFg: "#2196F3", // This affects the selection border + borderColor: "#2196F3", // This affects the general cell borders + drilldownBorder: "#2196F3", // This affects the border when drilling down into a cell +}; + const dateIncrementStrategies: Record< MatrixIndex["level"], DateIncrementStrategy @@ -194,7 +212,7 @@ export function generateTimeSeriesColumns({ count, startIndex = 1, prefix = "TS", - width = 50, + width, editable = true, style = "normal", }: TimeSeriesColumnOptions): EnhancedGridColumn[] { @@ -257,3 +275,50 @@ export function generateDataColumns( return []; } + +/** + * Calculates aggregate values (min, max, avg, total) for each column in a 2D numeric matrix. + * + * This function processes a 2D array (matrix) of numbers, computing four types of aggregates + * for each column. + * + * @param matrix - A 2D array of numbers representing the matrix. Each inner array is treated as a column. + * @returns An object containing four arrays, each corresponding to an aggregate type: + * min: An array of minimum values for each column. + * max: An array of maximum values for each column. + * avg: An array of average values for each column. + * total: An array of sum totals for each column. + * + * @example Calculating aggregates for a 3x3 matrix + * const matrix = [ + * [1, 2, 3], + * [4, 5, 6], + * [7, 8, 9] + * ]; + * const result = calculateAggregates(matrix); + * console.log(result); + * Output: { + * min: [1, 2, 3], + * max: [7, 8, 9], + * avg: [4, 5, 6], + * total: [12, 15, 18] + * } + */ +export function calculateMatrixAggregates(matrix: number[][]) { + const aggregates: MatrixAggregates = { + min: [], + max: [], + avg: [], + total: [], + }; + + matrix.forEach((row) => { + aggregates.min.push(Math.min(...row)); + aggregates.max.push(Math.max(...row)); + const sum = row.reduce((sum, num) => sum + num, 0); + aggregates.avg.push(Number((sum / row.length).toFixed())); + aggregates.total.push(Number(sum.toFixed())); + }); + + return aggregates; +} From c03e5e60eb99fbfc6be0d46ab0b17a4aa537c12b Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 17 Sep 2024 13:28:36 +0200 Subject: [PATCH 09/43] refactor(ui-links): update `Links` matrices --- .../Links/LinkView/LinkMatrixView.tsx | 191 +++++++++++------- 1 file changed, 116 insertions(+), 75 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx index 7cb84b4ae6..fe498f56fb 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx @@ -18,14 +18,9 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; -import { MatrixStats, StudyMetadata } from "../../../../../../../common/types"; -import MatrixInput from "../../../../../../common/MatrixInput"; - -export const StyledTab = styled(Tabs)({ - width: "100%", - borderBottom: 1, - borderColor: "divider", -}); +import { StudyMetadata } from "../../../../../../../common/types"; +import SplitView from "../../../../../../common/SplitView"; +import Matrix from "../../../../../../common/MatrixGrid/Matrix"; interface Props { study: StudyMetadata; @@ -33,78 +28,124 @@ interface Props { area2: string; } -function LinkMatrixView(props: Props) { - const [t] = useTranslation(); - const { study, area1, area2 } = props; - const [value, setValue] = React.useState(0); +interface MatrixConfig { + url: string; + titleKey: string; + columnsNames?: string[]; +} + +interface SplitMatrixContent { + type: "split"; + matrices: [MatrixConfig, MatrixConfig]; +} - const columnsNames = [ - `${t( - "study.modelization.links.matrix.columns.hurdleCostsDirect", - )} (${area1}->${area2})`, - `${t( - "study.modelization.links.matrix.columns.hurdleCostsIndirect", - )} (${area2}->${area1})`, - t("study.modelization.links.matrix.columns.impedances"), - t("study.modelization.links.matrix.columns.loopFlow"), - t("study.modelization.links.matrix.columns.pShiftMin"), - t("study.modelization.links.matrix.columns.pShiftMax"), - ]; +interface SingleMatrixContent { + type: "single"; + matrix: MatrixConfig; +} - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setValue(newValue); +interface MatrixItem { + titleKey: string; + content: SplitMatrixContent | SingleMatrixContent; +} + +function LinkMatrixView({ area1, area2 }: Props) { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState("parameters"); + + const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { + setActiveTab(newValue); }; + + const MATRICES: MatrixItem[] = useMemo( + () => [ + { + titleKey: "parameters", + content: { + type: "single", + matrix: { + url: `input/links/${area1.toLowerCase()}/${area2.toLowerCase()}_parameters`, + titleKey: "parameters", + columnsNames: [ + `${t( + "study.modelization.links.matrix.columns.hurdleCostsDirect", + )} (${area1}->${area2})`, + `${t( + "study.modelization.links.matrix.columns.hurdleCostsIndirect", + )} (${area2}->${area1})`, + t("study.modelization.links.matrix.columns.impedances"), + t("study.modelization.links.matrix.columns.loopFlow"), + t("study.modelization.links.matrix.columns.pShiftMin"), + t("study.modelization.links.matrix.columns.pShiftMax"), + ], + }, + }, + }, + { + titleKey: "capacities", + content: { + type: "split", + matrices: [ + { + url: `input/links/${area1.toLowerCase()}/capacities/${area2.toLowerCase()}_direct`, + titleKey: "transCapaDirect", + }, + { + url: `input/links/${area1.toLowerCase()}/capacities/${area2.toLowerCase()}_indirect`, + titleKey: "transCapaIndirect", + }, + ], + }, + }, + ], + [area1, area2, t], + ); + return ( - - - - - - - {value === 0 ? ( - + + {MATRICES.map(({ titleKey }) => ( + - ) : ( - <> - ${area2})`} - url={`input/links/${area1.toLowerCase()}/capacities/${area2.toLowerCase()}_direct`} - computStats={MatrixStats.NOCOL} - /> - - ${area1})`} - url={`input/links/${area1.toLowerCase()}/capacities/${area2.toLowerCase()}_indirect`} - computStats={MatrixStats.NOCOL} - /> - + ))} + + + {MATRICES.map( + ({ titleKey, content }) => + activeTab === titleKey && ( + + {content.type === "split" ? ( + + {content.matrices.map(({ url, titleKey }) => ( + + + + ))} + + ) : ( + + )} + + ), )} From eebd01030a26bc82fc0adc18b9886e79f60c382a Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 17 Sep 2024 13:29:51 +0200 Subject: [PATCH 10/43] fix(ui): prevent 0 to be processed as falsy value --- webapp/src/components/common/MatrixGrid/useMatrix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index e219845c9f..5575ade4ec 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -206,7 +206,7 @@ export function useMatrix( const newUpdates: MatrixUpdateDTO[] = updates .map(({ coordinates: [row, col], value }) => { - if (value.kind === GridCellKind.Number && value.data) { + if (value.kind === GridCellKind.Number && value.data !== undefined) { updatedData[col][row] = value.data; return { From 9ad6ac46a065a32daec4505b2a2669491092fd96 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 18 Sep 2024 15:18:03 +0200 Subject: [PATCH 11/43] refactor(ui-results): update `Results` view matrices --- .../explore/Results/ResultDetails/index.tsx | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index 7beaf0ec2a..2a080f5d68 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -39,7 +39,6 @@ import { } from "../../../../../../redux/selectors"; import { getStudyData } from "../../../../../../services/api/study"; import { isSearchMatching } from "../../../../../../utils/stringUtils"; -import EditableMatrix from "../../../../../common/EditableMatrix"; import PropertiesView from "../../../../../common/PropertiesView"; import SplitLayoutView from "../../../../../common/SplitLayoutView"; import ListElement from "../../common/ListElement"; @@ -60,7 +59,10 @@ import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; import SelectFE from "../../../../../common/fieldEditors/SelectFE"; import NumberFE from "../../../../../common/fieldEditors/NumberFE"; import moment from "moment"; -import DownloadMatrixButton from "../../../../../common/buttons/DownloadMatrixButton.tsx"; +import DownloadMatrixButton from "../../../../../common/DownloadMatrixButton.tsx"; +import MatrixGrid from "../../../../../common/MatrixGrid/index.tsx"; +import { generateCustomColumns } from "../../../../../common/MatrixGrid/utils.ts"; +import { ColumnTypes } from "../../../../../common/MatrixGrid/types.ts"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -284,16 +286,21 @@ function ResultDetails() { ifPending={() => ( )} - ifResolved={(matrix) => - matrix && ( - - ) - } + ifResolved={(matrix) => { + console.log("synthesisRes", matrix); + return ( + matrix && ( + + ) + ); + }} /> ) : ( @@ -412,16 +419,31 @@ function ResultDetails() { ifPending={() => ( )} - ifResolved={([, matrix]) => - matrix && ( - - ) - } + ifResolved={([, matrix]) => { + return ( + matrix && ( + <> + + + ) + ); + }} ifRejected={(err) => ( Date: Wed, 18 Sep 2024 17:34:01 +0200 Subject: [PATCH 12/43] refactor(ui-results): refresh and improve `ResultsDetails` --- .../Results/ResultDetails/ResultFilters.tsx | 143 +++++++++ .../explore/Results/ResultDetails/index.tsx | 296 +++++------------- 2 files changed, 225 insertions(+), 214 deletions(-) create mode 100644 webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx new file mode 100644 index 0000000000..b6bcc9f4b9 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx @@ -0,0 +1,143 @@ +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { DataType, Timestep } from "./utils"; +import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; +import SelectFE from "../../../../../common/fieldEditors/SelectFE"; +import NumberFE from "../../../../../common/fieldEditors/NumberFE"; +import DownloadMatrixButton from "../../../../../common/DownloadMatrixButton"; + +interface Props { + year: number; + setYear: (year: number) => void; + dataType: DataType; + setDataType: (dataType: DataType) => void; + timestep: Timestep; + setTimestep: (timestep: Timestep) => void; + maxYear: number; + studyId: string; + path: string; +} + +function ResultFilters({ + year, + setYear, + dataType, + setDataType, + timestep, + setTimestep, + maxYear, + studyId, + path, +}: Props) { + const { t } = useTranslation(); + + const controls = [ + { + label: `${t("study.results.mc")}:`, + control: ( + <> + { + setYear(event?.target.value ? -1 : 1); + }} + /> + {year > 0 && ( + { + setYear(Number(event.target.value)); + }} + /> + )} + + ), + }, + { + label: `${t("study.results.display")}:`, + control: ( + { + setDataType(event?.target.value as DataType); + }} + /> + ), + }, + { + label: `${t("study.results.temporality")}:`, + control: ( + { + setTimestep(event?.target.value as Timestep); + }} + /> + ), + }, + ]; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + {controls.map(({ label, control }) => ( + + + {label} + + {control} + + ))} + + + ); +} + +export default ResultFilters; diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index 2a080f5d68..317dc599ff 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -22,7 +22,6 @@ import { import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useOutletContext, useParams } from "react-router"; -import axios from "axios"; import GridOffIcon from "@mui/icons-material/GridOff"; import { Area, @@ -40,7 +39,6 @@ import { import { getStudyData } from "../../../../../../services/api/study"; import { isSearchMatching } from "../../../../../../utils/stringUtils"; import PropertiesView from "../../../../../common/PropertiesView"; -import SplitLayoutView from "../../../../../common/SplitLayoutView"; import ListElement from "../../common/ListElement"; import { createPath, @@ -55,14 +53,14 @@ import UsePromiseCond, { } from "../../../../../common/utils/UsePromiseCond"; import useStudySynthesis from "../../../../../../redux/hooks/useStudySynthesis"; import ButtonBack from "../../../../../common/ButtonBack"; -import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; -import SelectFE from "../../../../../common/fieldEditors/SelectFE"; -import NumberFE from "../../../../../common/fieldEditors/NumberFE"; import moment from "moment"; -import DownloadMatrixButton from "../../../../../common/DownloadMatrixButton.tsx"; import MatrixGrid from "../../../../../common/MatrixGrid/index.tsx"; import { generateCustomColumns } from "../../../../../common/MatrixGrid/utils.ts"; import { ColumnTypes } from "../../../../../common/MatrixGrid/types.ts"; +import SplitView from "../../../../../common/SplitView/index.tsx"; +import ResultFilters from "./ResultFilters.tsx"; +import { toError } from "../../../../../../utils/fnUtils.ts"; +import EmptyView from "../../../../../common/page/SimpleContent.tsx"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -225,16 +223,12 @@ function ResultDetails() { //////////////////////////////////////////////////////////////// return ( - + {/* Left */} + + navigate("..")} /> } @@ -269,208 +263,82 @@ function ResultDetails() { } onSearchFilterChange={setSearchValue} /> - } - right={ - isSynthesis ? ( - - ( - - )} - ifResolved={(matrix) => { - console.log("synthesisRes", matrix); - return ( - matrix && ( - - ) - ); - }} - /> - + + {/* Right */} + + + {isSynthesis ? ( + } + ifResolved={(matrix) => + matrix && ( + + ) + } + /> ) : ( - - - {( - [ - [ - `${t("study.results.mc")}:`, - () => ( - <> - { - setYear(event?.target.value ? -1 : 1); - }} - /> - {year > 0 && ( - { - setYear(Number(event.target.value)); - }} - /> - )} - - ), - ], - [ - `${t("study.results.display")}:`, - () => ( - { - setDataType(event?.target.value as DataType); - }} - /> - ), - ], - [ - `${t("study.results.temporality")}:`, - () => ( - { - setTimestep(event?.target.value as Timestep); - }} - /> - ), - ], - ] as const - ).map(([label, Field]) => ( - - - {label} - - - - ))} - - - - ( - - )} - ifResolved={([, matrix]) => { - return ( - matrix && ( - <> - - - ) - ); - }} - ifRejected={(err) => ( - - {axios.isAxiosError(err) && err.response?.status === 404 ? ( - <> - - {t("study.results.noData")} - - ) : ( - t("data.error.matrix") - )} - - )} + } + ifResolved={([, matrix]) => + matrix && ( + + ) + } + ifRejected={(err) => ( + - - - ) - } - /> + )} + /> + )} + + ); } From e309997c16dcb8df7d1e27bf70448f3e84bf1105 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 20 Sep 2024 09:18:43 +0200 Subject: [PATCH 13/43] refactor(ui-bc): update `BindingConstraints` matrices --- .../BindingConstView/ConstraintFields.tsx | 2 +- .../BindingConstView/Matrix.tsx | 41 ++++++++----------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx index 49063ced45..ec0eea9385 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx @@ -174,7 +174,7 @@ function Fields({ study, constraintId }: Props) { {matrixDialogOpen && ( - void; } -// TODO rename MatrixDialog or ConstraintMatrixDialog -function Matrix({ study, operator, constraintId, open, onClose }: Props) { +function ConstraintMatrix({ + study, + operator, + constraintId, + open, + onClose, +}: Props) { const { t } = useTranslation(); const dialogProps: BasicDialogProps = { open, @@ -59,63 +64,51 @@ function Matrix({ study, operator, constraintId, open, onClose }: Props) { {Number(study.version) >= 870 ? ( <> {operator === "less" && ( - )} {operator === "equal" && ( - )} {operator === "greater" && ( - )} {operator === "both" && ( - - )} ) : ( - ", "="]} - computStats={MatrixStats.NOCOL} + customColumns={["<", ">", "="]} /> )} ); } -export default Matrix; +export default ConstraintMatrix; From e5e5e8f6f6f6bda839c86be726a892b83d2de76d Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 20 Sep 2024 10:18:41 +0200 Subject: [PATCH 14/43] refactor(ui): use the number of updates as the indicator for the save button instead of the number of cells edited --- webapp/src/components/common/MatrixGrid/useMatrix.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 5575ade4ec..3b37f6989f 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -46,6 +46,7 @@ interface DataState { data: MatrixDataDTO["data"]; aggregates: MatrixAggregates; pendingUpdates: MatrixUpdateDTO[]; + updateCount: number; } export function useMatrix( @@ -70,6 +71,7 @@ export function useMatrix( data: [], aggregates: { min: [], max: [], avg: [], total: [] }, pendingUpdates: [], + updateCount: 0, }); const fetchMatrix = useCallback( @@ -95,6 +97,7 @@ export function useMatrix( ? calculateMatrixAggregates(matrix.data) : { min: [], max: [], avg: [], total: [] }, pendingUpdates: [], + updateCount: 0, }); setColumnCount(matrix.columns.length); setIndex(index); @@ -228,6 +231,9 @@ export function useMatrix( data: updatedData, aggregates: currentState.aggregates, pendingUpdates: [...currentState.pendingUpdates, ...newUpdates], + updateCount: currentState.updateCount + ? currentState.updateCount + 1 + : 1, }); }, [currentState, setState], @@ -261,9 +267,9 @@ export function useMatrix( await updateMatrix(studyId, url, currentState.pendingUpdates); setState({ - data: currentState.data, - aggregates: currentState.aggregates, + ...currentState, pendingUpdates: [], + updateCount: 0, }); enqueueSnackbar(t("matrix.success.matrixUpdate"), { @@ -306,7 +312,7 @@ export function useMatrix( handleMultipleCellsEdit, handleImport, handleSaveUpdates, - pendingUpdatesCount: currentState.pendingUpdates.length, + pendingUpdatesCount: currentState.updateCount, undo: handleUndo, redo: handleRedo, canUndo: canUndoChanges, From 359bec9035ebcaa3c2a77936deba3f5b8c9e1ff1 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 20 Sep 2024 10:23:29 +0200 Subject: [PATCH 15/43] feat(ui): add dynamic aggregates changes at each update --- .../components/common/MatrixGrid/useMatrix.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 3b37f6989f..b35bbcf765 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -227,16 +227,25 @@ export function useMatrix( (update): update is NonNullable => update !== null, ); + // Recalculate aggregates with the updated data + const newAggregates = enableAggregateColumns + ? calculateMatrixAggregates(updatedData) + : { min: [], max: [], avg: [], total: [] }; + setState({ data: updatedData, - aggregates: currentState.aggregates, + aggregates: newAggregates, pendingUpdates: [...currentState.pendingUpdates, ...newUpdates], - updateCount: currentState.updateCount - ? currentState.updateCount + 1 - : 1, + updateCount: currentState.updateCount + 1 || 1, }); }, - [currentState, setState], + [ + currentState.data, + currentState.pendingUpdates, + currentState.updateCount, + enableAggregateColumns, + setState, + ], ); const handleCellEdit = function (update: GridUpdate) { From e326c28e794978de9b017014d166e7c459cbd804 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 20 Sep 2024 18:15:02 +0200 Subject: [PATCH 16/43] refactor(ui): change position of aggregates cols and background colors --- .../components/common/MatrixGrid/useMatrix.ts | 16 ++++++++-------- webapp/src/components/common/MatrixGrid/utils.ts | 11 ++--------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index b35bbcf765..dac4021252 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -167,25 +167,25 @@ export function useMatrix( const aggregateColumns: EnhancedGridColumn[] = enableAggregateColumns ? [ { - id: "min", - title: "Min", + id: "avg", + title: "Avg", type: ColumnTypes.Aggregate, editable: false, themeOverride: aggregatesTheme, }, { - id: "max", - title: "Max", + id: "min", + title: "Min", type: ColumnTypes.Aggregate, editable: false, - themeOverride: aggregatesTheme, + themeOverride: { ...aggregatesTheme, bgCell: "#464770" }, }, { - id: "avg", - title: "Avg", + id: "max", + title: "Max", type: ColumnTypes.Aggregate, editable: false, - themeOverride: aggregatesTheme, + themeOverride: { ...aggregatesTheme, bgCell: "#464770" }, }, ] : []; diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 77a0482eb5..c27c8f9b85 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -73,20 +73,13 @@ export const readOnlyDarkTheme: Partial = { }; export const aggregatesTheme: Partial = { - bgCell: "#31324A", + bgCell: "#3D3E5F", bgCellMedium: "#383A5C", - textDark: "#1976D2", - textMedium: "#2196F3", - textLight: "#64B5F6", - accentColor: "#2196F3", - accentLight: "#64B5F633", + textDark: "#FFFFFF", fontFamily: "Inter, sans-serif", baseFontStyle: "bold 13px", editorFontSize: "13px", headerFontStyle: "bold 11px", - accentFg: "#2196F3", // This affects the selection border - borderColor: "#2196F3", // This affects the general cell borders - drilldownBorder: "#2196F3", // This affects the border when drilling down into a cell }; const dateIncrementStrategies: Record< From d43782fd308454560af88bd0ed07f50d4cb19a63 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 20 Sep 2024 18:29:09 +0200 Subject: [PATCH 17/43] feat(ui): make the `dateTime` column sticky --- webapp/src/components/common/MatrixGrid/index.tsx | 1 + webapp/src/components/common/MatrixGrid/useMatrix.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index e4b6aa2f1d..29363ac26b 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -159,6 +159,7 @@ function MatrixGrid({ onPaste fillHandle rowMarkers="both" + freezeColumns={1} // Make the first column sticky />
diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index dac4021252..54739e8bb5 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -145,6 +145,7 @@ export function useMatrix( title: "Date", type: ColumnTypes.DateTime, editable: false, + themeOverride: { bgCell: "#2D2E40" }, }); } From 6cdd151e3e5ee6f81c13788f474a9482f9af90fb Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 23 Sep 2024 14:47:52 +0200 Subject: [PATCH 18/43] feat(ui): add columns resize to `MatrixGrid` --- .../src/components/common/MatrixGrid/index.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 29363ac26b..4638aa2966 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -18,6 +18,7 @@ import DataEditor, { EditableGridCell, EditListItem, GridCellKind, + GridColumn, GridSelection, Item, } from "@glideapps/glide-data-grid"; @@ -45,7 +46,7 @@ export interface MatrixGridProps { function MatrixGrid({ data, rows, - columns, + columns: initialColumns, dateTime, aggregates, rowHeaders, @@ -56,6 +57,7 @@ function MatrixGrid({ isReaOnlyEnabled, isPercentDisplayEnabled, }: MatrixGridProps) { + const [columns, setColumns] = useState(initialColumns); const [selection, setSelection] = useState({ columns: CompactSelection.empty(), rows: CompactSelection.empty(), @@ -88,6 +90,18 @@ function MatrixGrid({ //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// + const handleColumnResize = ( + column: GridColumn, + newSize: number, + colIndex: number, + newSizeWithGrow: number, + ) => { + const newColumns = columns.map((col, index) => + index === colIndex ? { ...col, width: newSize } : col, + ); + + setColumns(newColumns); + }; const handleCellEdited = (coordinates: Item, value: EditableGridCell) => { if (value.kind !== GridCellKind.Number) { @@ -160,6 +174,7 @@ function MatrixGrid({ fillHandle rowMarkers="both" freezeColumns={1} // Make the first column sticky + onColumnResize={handleColumnResize} />
From 39cb34c0a8f56bdd74eb4e49749b625f3dc024ef Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 23 Sep 2024 15:05:26 +0200 Subject: [PATCH 19/43] feat(ui): change the decimal and thousand separator for number cells --- webapp/src/components/common/MatrixGrid/index.tsx | 1 + .../common/MatrixGrid/useGridCellContent.ts | 12 ++++++++---- webapp/src/components/common/MatrixGrid/utils.ts | 13 +++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 4638aa2966..456967a2b0 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -90,6 +90,7 @@ function MatrixGrid({ //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// + const handleColumnResize = ( column: GridColumn, newSize: number, diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index 7e385a448c..f9364a129b 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -20,7 +20,7 @@ import { ColumnTypes, MatrixAggregates, } from "./types"; -import { formatDateTime } from "./utils"; +import { formatDateTime, formatNumber } from "./utils"; type CellContentGenerator = ( row: number, @@ -65,9 +65,11 @@ const cellContentGenerators: Record = { return { kind: GridCellKind.Number, data: value, - displayData: value?.toString(), + displayData: formatNumber(value), // Format thousands and decimal separator readonly: !column.editable, allowOverlay: true, + decimalSeparator: ".", + thousandSeparator: " ", }; }, [ColumnTypes.Aggregate]: (row, col, column, data, dateTime, aggregates) => { @@ -76,9 +78,11 @@ const cellContentGenerators: Record = { return { kind: GridCellKind.Number, data: value, - displayData: value?.toString() ?? "", + displayData: formatNumber(value ?? 0), // Format thousands and decimal separator readonly: !column.editable, - allowOverlay: true, + allowOverlay: false, + decimalSeparator: ".", + thousandSeparator: " ", }; }, }; diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index c27c8f9b85..f61938aa11 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -106,6 +106,19 @@ const dateTimeFormatOptions: Intl.DateTimeFormatOptions = { // Functions //////////////////////////////////////////////////////////////// +/** + * Formats a number by adding spaces as thousand separators. + * + * @param num - The number to format. + * @returns The formatted number as a string. + */ +export function formatNumber(num: number): string { + // TODO: Add tests + const parts = num.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " "); + return parts.join("."); +} + /** * Formats a date and time string using predefined locale and format options. * From eb3b1d2ccaa3c9860d60fe5372c8bc9b14577174 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 26 Sep 2024 15:59:49 +0200 Subject: [PATCH 20/43] refactor(ui): improve aggregates columns usage --- .../Modelization/Areas/Hydro/HydroMatrix.tsx | 1 + .../explore/Modelization/Areas/Hydro/utils.ts | 3 + .../explore/Modelization/Areas/Load.tsx | 2 +- .../explore/Modelization/Areas/MiscGen.tsx | 4 +- .../explore/Modelization/Areas/Reserve.tsx | 4 +- .../explore/Modelization/Areas/Solar.tsx | 2 +- .../explore/Modelization/Areas/Wind.tsx | 2 +- .../components/common/MatrixGrid/Matrix.tsx | 7 +- .../components/common/MatrixGrid/index.tsx | 2 +- .../src/components/common/MatrixGrid/types.ts | 23 +++- .../MatrixGrid/useGridCellContent.test.ts | 99 ++++++++++++++- .../common/MatrixGrid/useGridCellContent.ts | 4 +- .../common/MatrixGrid/useMatrix.test.tsx | 2 +- .../components/common/MatrixGrid/useMatrix.ts | 76 ++++++----- .../common/MatrixGrid/utils.test.ts | 54 +++++++- .../src/components/common/MatrixGrid/utils.ts | 119 +++++++++++------- 16 files changed, 289 insertions(+), 115 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index 33a3f3461b..701891c9db 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -40,6 +40,7 @@ function HydroMatrix({ type }: Props) { url={hydroMatrix.url.replace("{areaId}", areaId)} customColumns={hydroMatrix.columns} customRowHeaders={hydroMatrix.rowHeaders} + aggregateColumns={hydroMatrix.aggregates} enableDateTimeColumn={hydroMatrix.enableDateTimeColumn} enableReadOnly={hydroMatrix.enableReadOnly} enablePercentDisplay={hydroMatrix.enablePercentDisplay} diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index e067c36b31..de05c04983 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -49,6 +49,7 @@ export interface HydroMatrixProps { columns?: string[]; rowHeaders?: string[]; fetchFn?: fetchMatrixFn; + aggregates?: AggregateConfig; enableDateTimeColumn?: boolean; enableReadOnly?: boolean; enablePercentDisplay?: boolean; @@ -154,10 +155,12 @@ export const MATRICES: Matrices = { [HydroMatrix.HydroStorage]: { title: "Hydro Storage", url: "input/hydro/series/{areaId}/mod", + aggregates: "stats", }, [HydroMatrix.RunOfRiver]: { title: "Run Of River", url: "input/hydro/series/{areaId}/ror", + aggregates: "stats", }, [HydroMatrix.MinGen]: { title: "Min Gen", diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx index b6c051dc41..2f83633bb7 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx @@ -24,7 +24,7 @@ function Load() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ; } export default Load; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx index dde3f72734..e2926384b5 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx @@ -35,7 +35,9 @@ function MiscGen() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ( + + ); } export default MiscGen; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx index 279ddebfde..844dba6d7a 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx @@ -31,7 +31,9 @@ function Reserve() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ( + + ); } export default Reserve; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx index 029146ddc0..0217250d97 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx @@ -25,7 +25,7 @@ function Solar() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ; } export default Solar; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx index af966811f2..7a545803c0 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx @@ -25,7 +25,7 @@ function Wind() { // JSX //////////////////////////////////////////////////////////////// - return ; + return ; } export default Wind; diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 6affd46d11..896f0a377f 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -24,6 +24,7 @@ import { MatrixContainer, MatrixHeader, MatrixTitle } from "./style"; import MatrixActions from "./MatrixActions"; import EmptyView from "../page/SimpleContent"; import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; +import { AggregateConfig } from "./types"; interface MatrixProps { url: string; @@ -31,7 +32,7 @@ interface MatrixProps { customRowHeaders?: string[]; enableDateTimeColumn?: boolean; enableTimeSeriesColumns?: boolean; - enableAggregateColumns?: boolean; + aggregateColumns?: AggregateConfig; enableRowHeaders?: boolean; enablePercentDisplay?: boolean; enableReadOnly?: boolean; @@ -46,7 +47,7 @@ function Matrix({ customRowHeaders = [], enableDateTimeColumn = true, enableTimeSeriesColumns = true, - enableAggregateColumns = false, + aggregateColumns = false, enableRowHeaders = customRowHeaders.length > 0, enablePercentDisplay = false, enableReadOnly = false, @@ -80,8 +81,8 @@ function Matrix({ url, enableDateTimeColumn, enableTimeSeriesColumns, - enableAggregateColumns, enableRowHeaders, + aggregateColumns, customColumns, colWidth, fetchMatrixData, diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 456967a2b0..a96ec48176 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -33,7 +33,7 @@ export interface MatrixGridProps { rows: number; columns: EnhancedGridColumn[]; dateTime?: string[]; - aggregates?: MatrixAggregates; + aggregates?: Partial; rowHeaders?: string[]; width?: string; height?: string; diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index 87f7b80f3f..f090a869e6 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -30,12 +30,20 @@ export const ColumnTypes = { } as const; export const Operations = { - ADD: "+", - SUB: "-", - MUL: "*", - DIV: "/", - ABS: "ABS", - EQ: "=", + Add: "+", + Sub: "-", + Mul: "*", + Div: "/", + Abs: "ABS", + Eq: "=", +} as const; + +// !NOTE: Keep lowercase to match Glide Data Grid column ids +export const Aggregates = { + Min: "min", + Max: "max", + Avg: "avg", + Total: "total", } as const; //////////////////////////////////////////////////////////////// @@ -67,6 +75,9 @@ export interface EnhancedGridColumn extends BaseGridColumn { editable: boolean; } +export type AggregateType = (typeof Aggregates)[keyof typeof Aggregates]; +export type AggregateConfig = AggregateType[] | boolean | "stats" | "all"; + export interface MatrixAggregates { min: number[]; max: number[]; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts index 11485c9f0c..880cf9fe5b 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts @@ -527,7 +527,7 @@ describe("useGridCellContent additional tests", () => { } }); - test("handles very large numbers correctly", () => { + test("formats number cells correctly", () => { const columns: EnhancedGridColumn[] = [ { id: "data1", @@ -537,17 +537,104 @@ describe("useGridCellContent additional tests", () => { editable: true, }, ]; - const largeNumber = 1e20; - const data = [[largeNumber]]; + + const data = [[1234567.89]]; + + const getCellContent = renderGridCellContent(data, columns); + const cell = getCellContent([0, 0]); + + if (cell.kind === "number" && "data" in cell) { + expect(cell.data).toBe(1234567.89); + expect(cell.displayData).toBe("1 234 567.89"); + } else { + throw new Error("Expected a number cell with formatted display data"); + } + }); + + test("handles very large and very small numbers correctly", () => { + const columns: EnhancedGridColumn[] = [ + { + id: "large", + title: "Large", + type: ColumnTypes.Number, + width: 50, + editable: true, + }, + { + id: "small", + title: "Small", + type: ColumnTypes.Number, + width: 50, + editable: true, + }, + ]; + + const data = [[1e20, 0.00001]]; + + const getCellContent = renderGridCellContent(data, columns); + + const largeCell = getCellContent([0, 0]); + if (largeCell.kind === "number" && "data" in largeCell) { + expect(largeCell.data).toBe(1e20); + expect(largeCell.displayData).toBe("100 000 000 000 000 000 000"); + } else { + throw new Error("Expected a number cell for large number"); + } + + const smallCell = getCellContent([1, 0]); + if (smallCell.kind === "number" && "data" in smallCell) { + expect(smallCell.data).toBe(0.00001); + expect(smallCell.displayData).toBe("0.00001"); + } else { + throw new Error("Expected a number cell for small number"); + } + }); + + test("handles negative numbers correctly", () => { + const columns: EnhancedGridColumn[] = [ + { + id: "negative", + title: "Negative", + type: ColumnTypes.Number, + width: 50, + editable: true, + }, + ]; + + const data = [[-1234567.89]]; const getCellContent = renderGridCellContent(data, columns); + const cell = getCellContent([0, 0]); + if (cell.kind === "number" && "data" in cell) { + expect(cell.data).toBe(-1234567.89); + expect(cell.displayData).toBe("-1 234 567.89"); + } else { + throw new Error("Expected a number cell with formatted negative number"); + } + }); + + test("handles zero correctly", () => { + const columns: EnhancedGridColumn[] = [ + { + id: "zero", + title: "Zero", + type: ColumnTypes.Number, + width: 50, + editable: true, + }, + ]; + + const data = [[0]]; + + const getCellContent = renderGridCellContent(data, columns); const cell = getCellContent([0, 0]); + if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(largeNumber); - expect(cell.displayData).toBe(largeNumber.toString()); + expect(cell.data).toBe(0); + expect(cell.displayData).toBe("0"); } else { - throw new Error("Expected a number cell with correct large number data"); + throw new Error("Expected a number cell for zero"); } }); }); diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index f9364a129b..efd696f3d2 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -28,7 +28,7 @@ type CellContentGenerator = ( column: EnhancedGridColumn, data: number[][], dateTime?: string[], - aggregates?: MatrixAggregates, + aggregates?: Partial, rowHeaders?: string[], ) => GridCell; @@ -118,7 +118,7 @@ export function useGridCellContent( columns: EnhancedGridColumn[], gridToData: (cell: Item) => Item | null, dateTime?: string[], - aggregates?: MatrixAggregates, + aggregates?: Partial, rowHeaders?: string[], isReadOnlyEnabled = false, isPercentDisplayEnabled = false, diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx index f6de8f7596..492a433097 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx +++ b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx @@ -138,7 +138,7 @@ describe("useMatrix", () => { expect(result.current.data[1][0]).toBe(5); expect(result.current.data[0][1]).toBe(6); - expect(result.current.pendingUpdatesCount).toBe(2); + expect(result.current.pendingUpdatesCount).toBe(1); }); test("should handle save updates", async () => { diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 54739e8bb5..9ae7536c78 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -16,7 +16,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { AxiosError } from "axios"; import { enqueueSnackbar } from "notistack"; import { t } from "i18next"; -import { MatrixIndex, Operator } from "../../../common/types"; +import { MatrixIndex } from "../../../common/types"; import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; import { getStudyMatrixIndex, @@ -30,12 +30,16 @@ import { GridUpdate, MatrixUpdateDTO, MatrixAggregates, + AggregateConfig, + Aggregates, + Operations, } from "./types"; import { aggregatesTheme, calculateMatrixAggregates, generateDataColumns, generateDateTime, + getAggregateTypes, } from "./utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; @@ -44,7 +48,7 @@ import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/ interface DataState { data: MatrixDataDTO["data"]; - aggregates: MatrixAggregates; + aggregates: Partial; pendingUpdates: MatrixUpdateDTO[]; updateCount: number; } @@ -54,8 +58,8 @@ export function useMatrix( url: string, enableDateTimeColumn: boolean, enableTimeSeriesColumns: boolean, - enableAggregateColumns: boolean, enableRowHeaders?: boolean, + aggregatesConfig?: AggregateConfig, customColumns?: string[] | readonly string[], colWidth?: number, fetchMatrixData?: fetchMatrixFn, @@ -74,6 +78,12 @@ export function useMatrix( updateCount: 0, }); + // Determine the aggregate types to display in the matrix + const aggregateTypes = useMemo( + () => getAggregateTypes(aggregatesConfig || []), + [aggregatesConfig], + ); + const fetchMatrix = useCallback( async (loadingState = true) => { // !NOTE This is a temporary solution to ensure the matrix is up to date @@ -93,9 +103,7 @@ export function useMatrix( setState({ data: matrix.data, - aggregates: enableAggregateColumns - ? calculateMatrixAggregates(matrix.data) - : { min: [], max: [], avg: [], total: [] }, + aggregates: calculateMatrixAggregates(matrix.data, aggregateTypes), pendingUpdates: [], updateCount: 0, }); @@ -115,7 +123,7 @@ export function useMatrix( } }, [ - enableAggregateColumns, + aggregateTypes, enqueueErrorSnackbar, fetchMatrixData, setState, @@ -165,33 +173,20 @@ export function useMatrix( colWidth, ); - const aggregateColumns: EnhancedGridColumn[] = enableAggregateColumns - ? [ - { - id: "avg", - title: "Avg", - type: ColumnTypes.Aggregate, - editable: false, - themeOverride: aggregatesTheme, - }, - { - id: "min", - title: "Min", - type: ColumnTypes.Aggregate, - editable: false, - themeOverride: { ...aggregatesTheme, bgCell: "#464770" }, - }, - { - id: "max", - title: "Max", - type: ColumnTypes.Aggregate, - editable: false, - themeOverride: { ...aggregatesTheme, bgCell: "#464770" }, - }, - ] - : []; - - return [...baseColumns, ...dataColumns, ...aggregateColumns]; + const aggregatesColumns: EnhancedGridColumn[] = aggregateTypes.map( + (aggregateType) => ({ + id: aggregateType, + title: aggregateType.charAt(0).toUpperCase() + aggregateType.slice(1), // Capitalize first letter + type: ColumnTypes.Aggregate, + editable: false, + themeOverride: + aggregateType === Aggregates.Avg + ? aggregatesTheme + : { ...aggregatesTheme, bgCell: "#464770" }, + }), + ); + + return [...baseColumns, ...dataColumns, ...aggregatesColumns]; }, [ currentState.data, enableDateTimeColumn, @@ -200,7 +195,7 @@ export function useMatrix( columnCount, customColumns, colWidth, - enableAggregateColumns, + aggregateTypes, ]); // Apply updates to the matrix data and store them in the pending updates list @@ -216,7 +211,7 @@ export function useMatrix( return { coordinates: [[col, row]], operation: { - operation: Operator.EQ, + operation: Operations.Eq, value: value.data, }, }; @@ -229,9 +224,10 @@ export function useMatrix( ); // Recalculate aggregates with the updated data - const newAggregates = enableAggregateColumns - ? calculateMatrixAggregates(updatedData) - : { min: [], max: [], avg: [], total: [] }; + const newAggregates = calculateMatrixAggregates( + updatedData, + aggregateTypes, + ); setState({ data: updatedData, @@ -244,7 +240,7 @@ export function useMatrix( currentState.data, currentState.pendingUpdates, currentState.updateCount, - enableAggregateColumns, + aggregateTypes, setState, ], ); diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index b6444c43cb..6d96350d86 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -19,6 +19,7 @@ import { import { ColumnTypes } from "./types"; import { calculateMatrixAggregates, + formatNumber, generateDateTime, generateTimeSeriesColumns, } from "./utils"; @@ -194,7 +195,12 @@ describe("calculateMatrixAggregates", () => { [4, 5, 6], [7, 8, 9], ]; - const result = calculateMatrixAggregates(matrix); + const result = calculateMatrixAggregates(matrix, [ + "min", + "max", + "avg", + "total", + ]); expect(result.min).toEqual([1, 4, 7]); expect(result.max).toEqual([3, 6, 9]); @@ -207,7 +213,12 @@ describe("calculateMatrixAggregates", () => { [1.1, 2.2, 3.3], [4.4, 5.5, 6.6], ]; - const result = calculateMatrixAggregates(matrix); + const result = calculateMatrixAggregates(matrix, [ + "min", + "max", + "avg", + "total", + ]); expect(result.min).toEqual([1.1, 4.4]); expect(result.max).toEqual([3.3, 6.6]); @@ -220,7 +231,12 @@ describe("calculateMatrixAggregates", () => { [-1, -2, -3], [-4, 0, 4], ]; - const result = calculateMatrixAggregates(matrix); + const result = calculateMatrixAggregates(matrix, [ + "min", + "max", + "avg", + "total", + ]); expect(result.min).toEqual([-3, -4]); expect(result.max).toEqual([-1, 4]); @@ -230,7 +246,12 @@ describe("calculateMatrixAggregates", () => { it("should handle single-element rows", () => { const matrix = [[1], [2], [3]]; - const result = calculateMatrixAggregates(matrix); + const result = calculateMatrixAggregates(matrix, [ + "min", + "max", + "avg", + "total", + ]); expect(result.min).toEqual([1, 2, 3]); expect(result.max).toEqual([1, 2, 3]); @@ -243,7 +264,12 @@ describe("calculateMatrixAggregates", () => { [1000000, 2000000, 3000000], [4000000, 5000000, 6000000], ]; - const result = calculateMatrixAggregates(matrix); + const result = calculateMatrixAggregates(matrix, [ + "min", + "max", + "avg", + "total", + ]); expect(result.min).toEqual([1000000, 4000000]); expect(result.max).toEqual([3000000, 6000000]); @@ -256,8 +282,24 @@ describe("calculateMatrixAggregates", () => { [1, 2, 4], [10, 20, 39], ]; - const result = calculateMatrixAggregates(matrix); + const result = calculateMatrixAggregates(matrix, ["avg"]); expect(result.avg).toEqual([2, 23]); }); }); + +describe("formatNumber", () => { + test("formats numbers correctly", () => { + expect(formatNumber(1234567.89)).toBe("1 234 567.89"); + expect(formatNumber(1000000)).toBe("1 000 000"); + expect(formatNumber(1234.5678)).toBe("1 234.5678"); + expect(formatNumber(undefined)).toBe(""); + }); + + test("handles edge cases", () => { + expect(formatNumber(0)).toBe("0"); + expect(formatNumber(-1234567.89)).toBe("-1 234 567.89"); + expect(formatNumber(0.00001)).toBe("0.00001"); + expect(formatNumber(1e20)).toBe("100 000 000 000 000 000 000"); + }); +}); diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index f61938aa11..a1709130f6 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -20,6 +20,9 @@ import { TimeSeriesColumnOptions, CustomColumnOptions, MatrixAggregates, + AggregateType, + AggregateConfig, + Aggregates, } from "./types"; import { getCurrentLanguage } from "../../../utils/i18nUtils"; import { Theme } from "@glideapps/glide-data-grid"; @@ -112,11 +115,18 @@ const dateTimeFormatOptions: Intl.DateTimeFormatOptions = { * @param num - The number to format. * @returns The formatted number as a string. */ -export function formatNumber(num: number): string { - // TODO: Add tests - const parts = num.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " "); - return parts.join("."); +export function formatNumber(num: number | undefined): string { + if (num === undefined) { + return ""; + } + + const [integerPart, decimalPart] = num.toString().split("."); + + // Format integer part with thousand separators + const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, " "); + + // Return formatted number, preserving decimal part if it exists + return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger; } /** @@ -282,48 +292,67 @@ export function generateDataColumns( return []; } -/** - * Calculates aggregate values (min, max, avg, total) for each column in a 2D numeric matrix. - * - * This function processes a 2D array (matrix) of numbers, computing four types of aggregates - * for each column. - * - * @param matrix - A 2D array of numbers representing the matrix. Each inner array is treated as a column. - * @returns An object containing four arrays, each corresponding to an aggregate type: - * min: An array of minimum values for each column. - * max: An array of maximum values for each column. - * avg: An array of average values for each column. - * total: An array of sum totals for each column. - * - * @example Calculating aggregates for a 3x3 matrix - * const matrix = [ - * [1, 2, 3], - * [4, 5, 6], - * [7, 8, 9] - * ]; - * const result = calculateAggregates(matrix); - * console.log(result); - * Output: { - * min: [1, 2, 3], - * max: [7, 8, 9], - * avg: [4, 5, 6], - * total: [12, 15, 18] - * } - */ -export function calculateMatrixAggregates(matrix: number[][]) { - const aggregates: MatrixAggregates = { - min: [], - max: [], - avg: [], - total: [], - }; +export function getAggregateTypes( + aggregateConfig: AggregateConfig, +): AggregateType[] { + if (aggregateConfig === "stats") { + return [Aggregates.Avg, Aggregates.Min, Aggregates.Max]; + } + + if (aggregateConfig === "all") { + return [Aggregates.Min, Aggregates.Max, Aggregates.Avg, Aggregates.Total]; + } + + if (Array.isArray(aggregateConfig)) { + return aggregateConfig; + } + + return []; +} + +export function calculateMatrixAggregates( + matrix: number[][], + aggregateTypes: AggregateType[], +): Partial { + const aggregates: Partial = {}; matrix.forEach((row) => { - aggregates.min.push(Math.min(...row)); - aggregates.max.push(Math.max(...row)); - const sum = row.reduce((sum, num) => sum + num, 0); - aggregates.avg.push(Number((sum / row.length).toFixed())); - aggregates.total.push(Number(sum.toFixed())); + if (aggregateTypes.includes(Aggregates.Min)) { + if (!aggregates.min) { + aggregates.min = []; + } + aggregates.min.push(Math.min(...row)); + } + + if (aggregateTypes.includes(Aggregates.Max)) { + if (!aggregates.max) { + aggregates.max = []; + } + aggregates.max.push(Math.max(...row)); + } + + if ( + aggregateTypes.includes(Aggregates.Avg) || + aggregateTypes.includes(Aggregates.Total) + ) { + const sum = row.reduce((sum, num) => sum + num, 0); + + if (aggregateTypes.includes(Aggregates.Avg)) { + if (!aggregates.avg) { + aggregates.avg = []; + } + + aggregates.avg.push(Number((sum / row.length).toFixed())); + } + + if (aggregateTypes.includes(Aggregates.Total)) { + if (!aggregates.total) { + aggregates.total = []; + } + + aggregates.total.push(Number(sum.toFixed())); + } + } }); return aggregates; From 5188e5e33d07200ccc0c9bc989035fcfe0cda2aa Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 30 Sep 2024 16:22:26 +0200 Subject: [PATCH 21/43] feat(ui): update date generation using `date-fns` instead of `moment` --- webapp/package-lock.json | 21 +++ webapp/package.json | 2 + webapp/public/locales/en/main.json | 1 + webapp/public/locales/fr/main.json | 1 + .../explore/Results/ResultDetails/index.tsx | 59 ++---- .../src/components/common/MatrixGrid/types.ts | 32 +++- .../common/MatrixGrid/useGridCellContent.ts | 4 +- .../src/components/common/MatrixGrid/utils.ts | 170 +++++++++--------- 8 files changed, 152 insertions(+), 138 deletions(-) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index c516722650..809c088d3d 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -20,6 +20,7 @@ "axios": "1.7.7", "clsx": "2.1.1", "d3": "5.16.0", + "date-fns": "4.1.0", "debug": "4.3.7", "draft-convert": "2.1.13", "draft-js": "0.11.7", @@ -72,6 +73,7 @@ "@testing-library/user-event": "14.5.2", "@total-typescript/ts-reset": "0.6.1", "@types/d3": "5.16.0", + "@types/date-fns": "2.6.0", "@types/debug": "4.1.12", "@types/draft-convert": "2.1.8", "@types/draft-js": "0.11.18", @@ -3732,6 +3734,16 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/date-fns": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.6.0.tgz", + "integrity": "sha512-9DSw2ZRzV0Tmpa6PHHJbMcZn79HHus+BBBohcOaDzkK/G3zMjDUDYjJIWBFLbkh+1+/IOS0A59BpQfdr37hASg==", + "deprecated": "This is a stub types definition for date-fns (https://github.com/date-fns/date-fns). date-fns provides its own type definitions, so you don't need @types/date-fns installed!", + "dev": true, + "dependencies": { + "date-fns": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -6148,6 +6160,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/webapp/package.json b/webapp/package.json index da78848498..117add1312 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -27,6 +27,7 @@ "clsx": "2.1.1", "d3": "5.16.0", "debug": "4.3.7", + "date-fns": "4.1.0", "draft-convert": "2.1.13", "draft-js": "0.11.7", "draftjs-to-html": "0.9.1", @@ -83,6 +84,7 @@ "@types/draft-js": "0.11.18", "@types/draftjs-to-html": "0.8.4", "@types/js-cookie": "3.0.6", + "@types/date-fns": "2.6.0", "@types/jsoneditor": "9.9.5", "@types/lodash": "4.17.9", "@types/node": "22.7.3", diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index eb81c6c734..cd400940b1 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -80,6 +80,7 @@ "global.time.weekly": "Weekly", "global.time.monthly": "Monthly", "global.time.annual": "Annual", + "global.time.weekShort": "W.", "global.update.success": "Update successful", "global.errorLogs": "Error logs", "global.error.emptyName": "Name cannot be empty", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index a22f7ce299..99c53ce430 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -80,6 +80,7 @@ "global.time.weekly": "Hebdomadaire", "global.time.monthly": "Mensuel", "global.time.annual": "Annuel", + "global.time.weekShort": "S.", "global.update.success": "Mise à jour réussie", "global.errorLogs": "Logs d'erreurs", "global.error.emptyName": "Le nom ne peut pas être vide", diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index 317dc599ff..ad2a838019 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -53,14 +53,17 @@ import UsePromiseCond, { } from "../../../../../common/utils/UsePromiseCond"; import useStudySynthesis from "../../../../../../redux/hooks/useStudySynthesis"; import ButtonBack from "../../../../../common/ButtonBack"; -import moment from "moment"; import MatrixGrid from "../../../../../common/MatrixGrid/index.tsx"; -import { generateCustomColumns } from "../../../../../common/MatrixGrid/utils.ts"; +import { + generateCustomColumns, + generateDateTime, +} from "../../../../../common/MatrixGrid/utils.ts"; import { ColumnTypes } from "../../../../../common/MatrixGrid/types.ts"; import SplitView from "../../../../../common/SplitView/index.tsx"; import ResultFilters from "./ResultFilters.tsx"; import { toError } from "../../../../../../utils/fnUtils.ts"; import EmptyView from "../../../../../common/page/SimpleContent.tsx"; +import { getStudyMatrixIndex } from "../../../../../../services/api/matrix.ts"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -151,6 +154,15 @@ function ResultDetails() { }, ); + const { data: dateTimeMetadata } = usePromise( + () => getStudyMatrixIndex(study.id, path), + { + deps: [study.id, path], + }, + ); + + const dateTime = dateTimeMetadata && generateDateTime(dateTimeMetadata); + const synthesisRes = usePromise( () => { if (outputId && selectedItem && isSynthesis) { @@ -164,47 +176,6 @@ function ResultDetails() { }, ); - // !NOTE: Workaround to display the date in the correct format, to be replaced by a proper solution. - const dateTimeFromIndex = useMemo(() => { - if (!matrixRes.data) { - return []; - } - - // Annual format has a static string - if (timestep === Timestep.Annual) { - return ["Annual"]; - } - - // Directly use API's week index (handles 53 weeks) as no formatting is required. - // !NOTE: Suboptimal: Assumes API consistency, lacks flexibility. - if (timestep === Timestep.Weekly) { - return matrixRes.data.index.map((weekNumber) => weekNumber.toString()); - } - - // Original date/time format mapping for moment parsing - const parseFormat = { - [Timestep.Hourly]: "MM/DD HH:mm", - [Timestep.Daily]: "MM/DD", - [Timestep.Monthly]: "MM", - }[timestep]; - - // Output formats for each timestep to match legacy UI requirements - const outputFormat = { - [Timestep.Hourly]: "DD MMM HH:mm I", - [Timestep.Daily]: "DD MMM I", - [Timestep.Monthly]: "MMM", - }[timestep]; - - const needsIndex = - timestep === Timestep.Hourly || timestep === Timestep.Daily; - - return matrixRes.data.index.map((dateTime, i) => - moment(dateTime, parseFormat).format( - outputFormat.replace("I", needsIndex ? ` - ${i + 1}` : ""), - ), - ); - }, [matrixRes.data, timestep]); - //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// @@ -320,7 +291,7 @@ function ResultDetails() { titles: matrix.columns, }), ]} - dateTime={dateTimeFromIndex} + dateTime={dateTime} isReaOnlyEnabled /> ) diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index f090a869e6..a996de9a63 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -22,6 +22,8 @@ import { // Enums //////////////////////////////////////////////////////////////// +// TODO update enums to be singular + export const ColumnTypes = { DateTime: "datetime", Number: "number", @@ -46,13 +48,37 @@ export const Aggregates = { Total: "total", } as const; +export const TimeFrequency = { + // TODO update old enum occurrences + ANNUAL: "annual", + MONTHLY: "monthly", + WEEKLY: "weekly", + DAILY: "daily", + HOURLY: "hourly", +} as const; + //////////////////////////////////////////////////////////////// // Types //////////////////////////////////////////////////////////////// // Derived types export type ColumnType = (typeof ColumnTypes)[keyof typeof ColumnTypes]; +// TODO add sufix Type export type Operation = (typeof Operations)[keyof typeof Operations]; +export type AggregateType = (typeof Aggregates)[keyof typeof Aggregates]; +export type TimeFrequencyType = + (typeof TimeFrequency)[keyof typeof TimeFrequency]; + +export type DateIncrementFunction = (date: Date, amount: number) => Date; +export type FormatFunction = (date: Date, firstWeekSize: number) => string; + +// !NOTE: This is temporary, date/time array should be generated by the API +export interface DateTimeMetadataDTO { + start_date: string; + steps: number; + first_week_size: number; + level: TimeFrequencyType; +} export interface TimeSeriesColumnOptions { count: number; @@ -75,7 +101,6 @@ export interface EnhancedGridColumn extends BaseGridColumn { editable: boolean; } -export type AggregateType = (typeof Aggregates)[keyof typeof Aggregates]; export type AggregateConfig = AggregateType[] | boolean | "stats" | "all"; export interface MatrixAggregates { @@ -111,8 +136,3 @@ export interface MatrixUpdateDTO { coordinates: number[][]; // Array of [col, row] pairs operation: MatrixUpdate; } - -export type DateIncrementStrategy = ( - date: moment.Moment, - step: number, -) => moment.Moment; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index efd696f3d2..57c94c5fe4 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -20,7 +20,7 @@ import { ColumnTypes, MatrixAggregates, } from "./types"; -import { formatDateTime, formatNumber } from "./utils"; +import { formatNumber } from "./utils"; type CellContentGenerator = ( row: number, @@ -55,7 +55,7 @@ const cellContentGenerators: Record = { [ColumnTypes.DateTime]: (row, col, column, data, dateTime) => ({ kind: GridCellKind.Text, data: "", // Date/time columns are not editable - displayData: formatDateTime(dateTime?.[row] ?? ""), + displayData: dateTime?.[row] ?? "", readonly: !column.editable, allowOverlay: false, }), diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index a1709130f6..7589e5d711 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -12,9 +12,7 @@ * This file is part of the Antares project. */ -import moment from "moment"; import { - DateIncrementStrategy, EnhancedGridColumn, ColumnTypes, TimeSeriesColumnOptions, @@ -23,10 +21,28 @@ import { AggregateType, AggregateConfig, Aggregates, + DateIncrementFunction, + FormatFunction, + TimeFrequency, + TimeFrequencyType, + DateTimeMetadataDTO, } from "./types"; +import { + type FirstWeekContainsDate, + parseISO, + addHours, + addDays, + addWeeks, + addMonths, + addYears, + format, + startOfWeek, + Locale, +} from "date-fns"; +import { fr, enUS } from "date-fns/locale"; import { getCurrentLanguage } from "../../../utils/i18nUtils"; import { Theme } from "@glideapps/glide-data-grid"; -import { MatrixIndex } from "../../../common/types"; +import { t } from "i18next"; export const darkTheme: Theme = { accentColor: "rgba(255, 184, 0, 0.9)", @@ -85,26 +101,6 @@ export const aggregatesTheme: Partial = { headerFontStyle: "bold 11px", }; -const dateIncrementStrategies: Record< - MatrixIndex["level"], - DateIncrementStrategy -> = { - hourly: (date, step) => date.clone().add(step, "hours"), - daily: (date, step) => date.clone().add(step, "days"), - weekly: (date, step) => date.clone().add(step, "weeks"), - monthly: (date, step) => date.clone().add(step, "months"), - annual: (date, step) => date.clone().add(step, "years"), -}; - -const dateTimeFormatOptions: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - timeZone: "UTC", // Ensures consistent UTC-based time representation -}; - //////////////////////////////////////////////////////////////// // Functions //////////////////////////////////////////////////////////////// @@ -121,7 +117,6 @@ export function formatNumber(num: number | undefined): string { } const [integerPart, decimalPart] = num.toString().split("."); - // Format integer part with thousand separators const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, " "); @@ -129,78 +124,80 @@ export function formatNumber(num: number | undefined): string { return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger; } +function getLocale(): Locale { + const lang = getCurrentLanguage(); + return lang && lang.startsWith("fr") ? fr : enUS; +} + /** - * Formats a date and time string using predefined locale and format options. - * - * This function takes a date/time string, creates a Date object from it, - * and then formats it according to the specified options. The formatting - * is done using the French locale as the primary choice, falling back to - * English if French is not available. - * - * Important: This function will always return the time in UTC, regardless - * of the system's local time zone. This behavior is controlled by the - * 'timeZone' option in dateTimeFormatOptions. - * - * @param dateTime - The date/time string to format. This should be an ISO 8601 string (e.g., "2024-01-01T00:00:00Z"). - * @returns The formatted date/time string in the format specified by dateTimeFormatOptions, always in UTC. - * - * @example returns "1 janv. 2024, 00:00" (French locale) - * formatDateTime("2024-01-01T00:00:00Z") + * Configuration object for different time frequencies * - * @example returns "Jan 1, 2024, 12:00 AM" (English locale) - * formatDateTime("2024-01-01T00:00:00Z") + * This object defines how to increment and format dates for various time frequencies. + * The WEEKLY frequency is of particular interest as it implements custom week starts + * and handles ISO week numbering. */ -export function formatDateTime(dateTime: string): string { - const date = moment.utc(dateTime); - const currentLocale = getCurrentLanguage(); - const locales = [currentLocale, "en-US"]; - - return date.toDate().toLocaleString(locales, dateTimeFormatOptions); -} +const TIME_FREQUENCY_CONFIG: Record< + TimeFrequencyType, + { + increment: DateIncrementFunction; + format: FormatFunction; + } +> = { + [TimeFrequency.ANNUAL]: { + increment: addYears, + format: () => t("global.time.annual"), + }, + [TimeFrequency.MONTHLY]: { + increment: addMonths, + format: (date: Date) => format(date, "MMM", { locale: getLocale() }), + }, + [TimeFrequency.WEEKLY]: { + increment: addWeeks, + format: (date: Date, firstWeekSize: number) => { + const weekStart = startOfWeek(date, { locale: getLocale() }); + + return format(weekStart, `'${t("global.time.weekShort")}' ww`, { + locale: getLocale(), + weekStartsOn: firstWeekSize === 1 ? 0 : 1, + firstWeekContainsDate: firstWeekSize as FirstWeekContainsDate, + }); + }, + }, + [TimeFrequency.DAILY]: { + increment: addDays, + format: (date: Date) => format(date, "EEE d", { locale: getLocale() }), + }, + [TimeFrequency.HOURLY]: { + increment: addHours, + format: (date: Date) => + format(date, "EEE d HH:mm", { locale: getLocale() }), + }, +}; /** - * Generates an array of date-time strings based on the provided time metadata. - * - * This function creates a series of date-time strings, starting from the given start date - * and incrementing based on the specified level (hourly, daily, weekly, monthly, or yearly). - * It uses the Moment.js library for date manipulation and the ISO 8601 format for date-time strings. + * Generates an array of formatted date/time strings based on the provided configuration * - * @param timeMetadata - The time metadata object. - * @param timeMetadata.start_date - The starting date-time in ISO 8601 format (e.g., "2023-01-01T00:00:00Z"). - * @param timeMetadata.steps - The number of date-time strings to generate. - * @param timeMetadata.level - The increment level for date-time generation. + * This function handles various time frequencies, with special attention to weekly formatting. + * For weekly frequency, it respects custom week starts while maintaining ISO week numbering. * - * @returns An array of ISO 8601 formatted date-time strings. - * - * @example - * const result = generateDateTime({ - * start_date: "2023-01-01T00:00:00Z", - * steps: 3, - * level: "daily" - * }); - * - * Returns: [ - * "2023-01-01T00:00:00.000Z", - * "2023-01-02T00:00:00.000Z", - * "2023-01-03T00:00:00.000Z" - * ] - * - * @see {@link MatrixIndex} for the structure of the timeMetadata object. - * @see {@link DateIncrementStrategy} for the date increment strategy type. + * @param config - Configuration object for date/time generation + * @param config.start_date - The starting date for generation + * @param config.steps - Number of increments to generate + * @param config.first_week_size - Defines the number of days for the first the week (from 1 to 7) + * @param config.level - The time frequency level (ANNUAL, MONTHLY, WEEKLY, DAILY, HOURLY) + * @returns An array of formatted date/time strings */ -export function generateDateTime({ +export const generateDateTime = (config: DateTimeMetadataDTO): string[] => { // eslint-disable-next-line camelcase - start_date, - steps, - level, -}: MatrixIndex): string[] { - const startDate = moment.utc(start_date, "YYYY-MM-DD HH:mm:ss"); - const incrementStrategy = dateIncrementStrategies[level]; + const { start_date, steps, first_week_size, level } = config; + const { increment, format } = TIME_FREQUENCY_CONFIG[level]; + const initialDate = parseISO(start_date); - return Array.from({ length: steps }, (_, i) => - incrementStrategy(startDate, i).toISOString(), - ); -} + return Array.from({ length: steps }, (_, index) => { + const currentDate = increment(initialDate, index); + return format(currentDate, first_week_size); + }); +}; /** * Generates an array of EnhancedGridColumn objects representing time series data columns. @@ -292,6 +289,7 @@ export function generateDataColumns( return []; } +// TODO add docs + refactor export function getAggregateTypes( aggregateConfig: AggregateConfig, ): AggregateType[] { From 242e42a9cacceabf9d76b396230849bfaf95e7f4 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 1 Oct 2024 09:40:50 +0200 Subject: [PATCH 22/43] feat(ui): add prompt for unsaved changes when switching views --- webapp/src/components/common/MatrixGrid/useMatrix.test.tsx | 1 + webapp/src/components/common/MatrixGrid/useMatrix.ts | 7 +++++++ webapp/src/components/common/MatrixGrid/utils.ts | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx index 492a433097..4a6c8b4e68 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx +++ b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx @@ -30,6 +30,7 @@ import { GridCellKind } from "@glideapps/glide-data-grid"; vi.mock("../../../services/api/matrix"); vi.mock("../../../services/api/study"); vi.mock("../../../services/api/studies/raw"); +vi.mock("../../../hooks/usePrompt"); describe("useMatrix", () => { const mockStudyId = "study123"; diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 9ae7536c78..b747aadce2 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -45,6 +45,7 @@ import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; import { importFile } from "../../../services/api/studies/raw"; import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; +import usePrompt from "../../../hooks/usePrompt"; interface DataState { data: MatrixDataDTO["data"]; @@ -84,6 +85,12 @@ export function useMatrix( [aggregatesConfig], ); + // Display warning prompts to prevent unintended navigation + // 1. When the matrix is currently being submitted + usePrompt(t("form.submit.inProgress"), isSubmitting); + // 2. When there are unsaved changes in the matrix + usePrompt(t("form.changeNotSaved"), currentState.pendingUpdates.length > 0); + const fetchMatrix = useCallback( async (loadingState = true) => { // !NOTE This is a temporary solution to ensure the matrix is up to date diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 7589e5d711..e416b9b300 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -194,8 +194,8 @@ export const generateDateTime = (config: DateTimeMetadataDTO): string[] => { const initialDate = parseISO(start_date); return Array.from({ length: steps }, (_, index) => { - const currentDate = increment(initialDate, index); - return format(currentDate, first_week_size); + const date = increment(initialDate, index); + return format(date, first_week_size); }); }; From c99bcc978a190de357246a13ac5f3483d96f97e5 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 1 Oct 2024 10:06:29 +0200 Subject: [PATCH 23/43] feat(ui): allow fill handle on any direction --- webapp/src/components/common/MatrixGrid/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index a96ec48176..940bf23e10 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -176,6 +176,7 @@ function MatrixGrid({ rowMarkers="both" freezeColumns={1} // Make the first column sticky onColumnResize={handleColumnResize} + allowedFillDirections="any" />
From 4e138535860f39730d824a76de0b41148a64d64c Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 9 Oct 2024 15:47:15 +0200 Subject: [PATCH 24/43] feat(ui): add month display to daily and hourly frequencies --- webapp/src/components/common/MatrixGrid/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index e416b9b300..ad750728a0 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -165,12 +165,12 @@ const TIME_FREQUENCY_CONFIG: Record< }, [TimeFrequency.DAILY]: { increment: addDays, - format: (date: Date) => format(date, "EEE d", { locale: getLocale() }), + format: (date: Date) => format(date, "EEE d MMM", { locale: getLocale() }), }, [TimeFrequency.HOURLY]: { increment: addHours, format: (date: Date) => - format(date, "EEE d HH:mm", { locale: getLocale() }), + format(date, "EEE d MMM HH:mm", { locale: getLocale() }), }, }; From c6f01319c3398a9aa7936886fc4f03a1e015affe Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 9 Oct 2024 16:29:19 +0200 Subject: [PATCH 25/43] feat(ui): reduce cells height and add smooth scroll --- .../components/common/MatrixGrid/Matrix.tsx | 2 +- .../src/components/common/MatrixGrid/index.tsx | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 896f0a377f..8b24f35cb0 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -132,7 +132,7 @@ function Matrix({ dateTime={dateTime} onCellEdit={handleCellEdit} onMultipleCellsEdit={handleMultipleCellsEdit} - isReaOnlyEnabled={isSubmitting || enableReadOnly} + isReadOnlyEnabled={isSubmitting || enableReadOnly} isPercentDisplayEnabled={enablePercentDisplay} /> {openImportDialog && ( diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 940bf23e10..b82625bb7a 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -39,7 +39,7 @@ export interface MatrixGridProps { height?: string; onCellEdit?: (update: GridUpdate) => void; onMultipleCellsEdit?: (updates: GridUpdate[]) => void; - isReaOnlyEnabled?: boolean; + isReadOnlyEnabled?: boolean; isPercentDisplayEnabled?: boolean; } @@ -54,7 +54,7 @@ function MatrixGrid({ height = "100%", onCellEdit, onMultipleCellsEdit, - isReaOnlyEnabled, + isReadOnlyEnabled, isPercentDisplayEnabled, }: MatrixGridProps) { const [columns, setColumns] = useState(initialColumns); @@ -66,7 +66,7 @@ function MatrixGrid({ const { gridToData } = useColumnMapping(columns); const theme = useMemo(() => { - if (isReaOnlyEnabled) { + if (isReadOnlyEnabled) { return { ...darkTheme, ...readOnlyDarkTheme, @@ -74,7 +74,7 @@ function MatrixGrid({ } return darkTheme; - }, [isReaOnlyEnabled]); + }, [isReadOnlyEnabled]); const getCellContent = useGridCellContent( data, @@ -83,7 +83,7 @@ function MatrixGrid({ dateTime, aggregates, rowHeaders, - isReaOnlyEnabled, + isReadOnlyEnabled, isPercentDisplayEnabled, ); @@ -173,10 +173,16 @@ function MatrixGrid({ getCellsForSelection // Enable copy support onPaste fillHandle + allowedFillDirections="any" rowMarkers="both" freezeColumns={1} // Make the first column sticky onColumnResize={handleColumnResize} - allowedFillDirections="any" + smoothScrollX + smoothScrollY + rowHeight={30} + verticalBorder={false} + overscrollX={100} + overscrollY={100} />
From eb3eb37f1b7de3f1cfaab4da9d48b55e26cca2af Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 14 Oct 2024 16:43:23 +0200 Subject: [PATCH 26/43] feat(ui): disable copy/paste keybindings --- webapp/src/components/common/MatrixGrid/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index b82625bb7a..8b19f32b9a 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -170,8 +170,8 @@ function MatrixGrid({ onCellsEdited={handleCellsEdited} gridSelection={selection} onGridSelectionChange={setSelection} - getCellsForSelection // Enable copy support - onPaste + keybindings={{ paste: false, copy: false }} + onPaste={false} fillHandle allowedFillDirections="any" rowMarkers="both" From dbb485faaa855e1ce092bd360af0895cd0b4a9aa Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 21 Oct 2024 17:30:55 +0200 Subject: [PATCH 27/43] fix(ui): resolve imports errors due to merge conflicts fix: correct lint and types issues fix(ui): resolve merge conflict --- .../Areas/Hydro/Allocation/utils.ts | 6 +- .../Areas/Hydro/Correlation/utils.ts | 6 +- .../Modelization/Areas/Hydro/HydroMatrix.tsx | 2 - .../explore/Modelization/Areas/Hydro/utils.ts | 5 +- .../explore/Modelization/Areas/MiscGen.tsx | 1 - .../Modelization/Areas/Renewables/Matrix.tsx | 13 +--- .../explore/Modelization/Areas/Reserve.tsx | 1 - .../explore/Modelization/Areas/Solar.tsx | 1 - .../Modelization/Areas/Storages/Matrix.tsx | 3 +- .../explore/Modelization/Areas/Wind.tsx | 1 - .../BindingConstView/ConstraintFields.tsx | 2 +- .../Links/LinkView/LinkMatrixView.tsx | 5 +- .../Results/ResultDetails/ResultFilters.tsx | 16 ++++- .../explore/Results/ResultDetails/index.tsx | 4 +- .../MatrixGrid/useGridCellContent.test.ts | 56 --------------- .../common/MatrixGrid/utils.test.ts | 72 ------------------- 16 files changed, 29 insertions(+), 165 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts index 75cbf73862..7bb47baffb 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts @@ -12,11 +12,7 @@ * This file is part of the Antares project. */ -import { - StudyMetadata, - Area, - MatrixType, -} from "../../../../../../../../common/types"; +import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; import { AreaCoefficientItem } from "../utils"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts index bcf4137a36..5806dd3e9b 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts @@ -12,11 +12,7 @@ * This file is part of the Antares project. */ -import { - StudyMetadata, - Area, - MatrixType, -} from "../../../../../../../../common/types"; +import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; import { AreaCoefficientItem } from "../utils"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index 701891c9db..0a6a6f22e1 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -12,8 +12,6 @@ * This file is part of the Antares project. */ -import { useOutletContext } from "react-router"; -import { StudyMetadata } from "../../../../../../../common/types"; import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../../redux/selectors"; import { MATRICES, HydroMatrixType } from "./utils"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index de05c04983..ac83b0a214 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -12,7 +12,10 @@ * This file is part of the Antares project. */ -import { MatrixStats, MatrixType } from "../../../../../../../common/types"; +import { + MatrixDataDTO, + AggregateConfig, +} from "../../../../../../common/MatrixGrid/types"; import { SplitViewProps } from "../../../../../../common/SplitView"; import { getAllocationMatrix } from "./Allocation/utils"; import { getCorrelationMatrix } from "./Correlation/utils"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx index e2926384b5..ccdba1652e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx @@ -12,7 +12,6 @@ * This file is part of the Antares project. */ -import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; import Matrix from "../../../../../common/MatrixGrid/Matrix"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx index 2948c33584..c6bbac8c3e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx @@ -12,17 +12,8 @@ * This file is part of the Antares project. */ -import * as React from "react"; -import Tabs from "@mui/material/Tabs"; -import Tab from "@mui/material/Tab"; -import Box from "@mui/material/Box"; -import { useTranslation } from "react-i18next"; -import { - Cluster, - MatrixStats, - StudyMetadata, -} from "../../../../../../../common/types"; -import MatrixInput from "../../../../../../common/MatrixInput"; +import { Cluster } from "../../../../../../../common/types"; +import Matrix from "../../../../../../common/MatrixGrid/Matrix"; interface Props { areaId: string; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx index 844dba6d7a..da9d01870e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx @@ -12,7 +12,6 @@ * This file is part of the Antares project. */ -import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; import Matrix from "../../../../../common/MatrixGrid/Matrix"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx index 0217250d97..487f4b4e70 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx @@ -12,7 +12,6 @@ * This file is part of the Antares project. */ -import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; import Matrix from "../../../../../common/MatrixGrid/Matrix"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx index bb7449dad0..9dbd2484ec 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx @@ -12,8 +12,7 @@ * This file is part of the Antares project. */ -import * as React from "react"; -import * as R from "ramda"; +import { useMemo, useState } from "react"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx index 7a545803c0..40e7277c0f 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx @@ -12,7 +12,6 @@ * This file is part of the Antares project. */ -import { useOutletContext } from "react-router"; import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; import Matrix from "../../../../../common/MatrixGrid/Matrix"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx index ec0eea9385..285c3fd11e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/ConstraintFields.tsx @@ -27,10 +27,10 @@ import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; import { useFormContextPlus } from "../../../../../../common/Form"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import Matrix from "./Matrix"; import { Box, Button } from "@mui/material"; import { Dataset } from "@mui/icons-material"; import { validateString } from "@/utils/validation/string"; +import ConstraintMatrix from "./Matrix"; interface Props { study: StudyMetadata; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx index fe498f56fb..c0a92068a3 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx @@ -12,8 +12,7 @@ * This file is part of the Antares project. */ -import * as React from "react"; -import { Divider, styled } from "@mui/material"; +import { SyntheticEvent, useMemo, useState } from "react"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; @@ -53,7 +52,7 @@ function LinkMatrixView({ area1, area2 }: Props) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("parameters"); - const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { + const handleTabChange = (event: SyntheticEvent, newValue: string) => { setActiveTab(newValue); }; diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx index b6bcc9f4b9..bdefbea3c1 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx @@ -1,10 +1,24 @@ +/** + * Copyright (c) 2024, 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 } from "@mui/material"; import { useTranslation } from "react-i18next"; import { DataType, Timestep } from "./utils"; import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; import SelectFE from "../../../../../common/fieldEditors/SelectFE"; import NumberFE from "../../../../../common/fieldEditors/NumberFE"; -import DownloadMatrixButton from "../../../../../common/DownloadMatrixButton"; +import DownloadMatrixButton from "../../../../../common/buttons/DownloadMatrixButton"; interface Props { year: number; diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index ad2a838019..3011f257d6 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -266,7 +266,7 @@ function ResultDetails() { columns={generateCustomColumns({ titles: matrix.columns, })} - isReaOnlyEnabled + isReadOnlyEnabled /> ) } @@ -292,7 +292,7 @@ function ResultDetails() { }), ]} dateTime={dateTime} - isReaOnlyEnabled + isReadOnlyEnabled /> ) } diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts index 880cf9fe5b..8adf3041d5 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts @@ -80,42 +80,6 @@ function renderGridCellContent( } describe("useGridCellContent", () => { - test("returns correct text cell content for DateTime columns", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "date", - title: "Date", - type: ColumnTypes.DateTime, - width: 150, - editable: false, - }, - { - id: "data1", - title: "TS 1", - type: ColumnTypes.Number, - width: 50, - editable: true, - }, - ]; - - const dateTime = ["2024-01-07T00:00:00Z", "2024-01-02T00:00:00Z"]; - - const data = [ - [11, 10], - [12, 15], - ]; - - const getCellContent = renderGridCellContent(data, columns, dateTime); - const cell = getCellContent([0, 0]); - - if ("displayData" in cell) { - expect(cell.kind).toBe("text"); - expect(cell.displayData).toBe("7 janv. 2024, 00:00"); - } else { - throw new Error("Expected a text cell with displayData"); - } - }); - describe("returns correct cell content for Aggregate columns", () => { const columns: EnhancedGridColumn[] = [ { @@ -220,17 +184,6 @@ describe("useGridCellContent", () => { aggregates, ); - const dateTimeCell = getCellContent([0, 0]); - - if (dateTimeCell.kind === "text" && "displayData" in dateTimeCell) { - expect(dateTimeCell.data).toBe(""); - expect(dateTimeCell.displayData).toBe("1 janv. 2021, 00:00"); - } else { - throw new Error( - "Expected a DateTime cell with displayData containing the year 2021", - ); - } - const numberCell = getCellContent([1, 0]); if (numberCell.kind === "number" && "data" in numberCell) { @@ -319,15 +272,6 @@ describe("useGridCellContent with mixed column types", () => { throw new Error("Expected a text cell with data for row header"); } - // Test date column (DateTime column) - const dateCell = getCellContent([1, 0]); - if (dateCell.kind === "text" && "displayData" in dateCell) { - expect(dateCell.data).toBe(""); - expect(dateCell.displayData).toBe("1 janv. 2024, 00:00"); - } else { - throw new Error("Expected a text cell with data for date"); - } - // Test first data column (Number column) const firstDataCell = getCellContent([2, 0]); diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index 6d96350d86..403e46c0ed 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -35,78 +35,6 @@ describe("generateDateTime", () => { const result = generateDateTime(metadata); expect(result).toHaveLength(5); }); - - test.each([ - { - level: "hourly", - start: "2023-01-01T00:00:00Z", - expected: [ - "2023-01-01T00:00:00.000Z", - "2023-01-01T01:00:00.000Z", - "2023-01-01T02:00:00.000Z", - ], - }, - { - level: "daily", - start: "2023-01-01T00:00:00Z", - expected: [ - "2023-01-01T00:00:00.000Z", - "2023-01-02T00:00:00.000Z", - "2023-01-03T00:00:00.000Z", - ], - }, - { - level: "weekly", - start: "2023-01-01T00:00:00Z", - expected: [ - "2023-01-01T00:00:00.000Z", - "2023-01-08T00:00:00.000Z", - "2023-01-15T00:00:00.000Z", - ], - }, - { - level: "monthly", - start: "2023-01-15T00:00:00Z", - expected: [ - "2023-01-15T00:00:00.000Z", - "2023-02-15T00:00:00.000Z", - "2023-03-15T00:00:00.000Z", - ], - }, - { - level: "annual", - start: "2020-02-29T00:00:00Z", - expected: ["2020-02-29T00:00:00.000Z", "2021-02-28T00:00:00.000Z"], - }, - ] as const)( - "generates correct dates for $level level", - ({ level, start, expected }) => { - const metadata: MatrixIndex = { - start_date: start, - steps: expected.length, - first_week_size: 7, - level: level as MatrixIndex["level"], - }; - - const result = generateDateTime(metadata); - - expect(result).toEqual(expected); - }, - ); - - test("handles edge cases", () => { - const metadata: MatrixIndex = { - start_date: "2023-12-31T23:59:59Z", - steps: 2, - first_week_size: 7, - level: StudyOutputDownloadLevelDTO.HOURLY, - }; - const result = generateDateTime(metadata); - expect(result).toEqual([ - "2023-12-31T23:59:59.000Z", - "2024-01-01T00:59:59.000Z", - ]); - }); }); describe("generateTimeSeriesColumns", () => { From f936584b69b5e96de6d8525289bd6df8850e5161 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 14 Oct 2024 20:25:20 +0200 Subject: [PATCH 28/43] fix(ui): prevent regex DoS vulnerability in `formatNumber` function --- webapp/src/components/common/MatrixGrid/index.tsx | 2 +- webapp/src/components/common/MatrixGrid/utils.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 8b19f32b9a..7873ab1d12 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -171,7 +171,7 @@ function MatrixGrid({ gridSelection={selection} onGridSelectionChange={setSelection} keybindings={{ paste: false, copy: false }} - onPaste={false} + getCellsForSelection // TODO handle large copy/paste using this fillHandle allowedFillDirections="any" rowMarkers="both" diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index ad750728a0..89b126afa1 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -117,8 +117,17 @@ export function formatNumber(num: number | undefined): string { } const [integerPart, decimalPart] = num.toString().split("."); - // Format integer part with thousand separators - const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, " "); + + // Format integer part with thousand separators using a non-regex approach + const formattedInteger = integerPart + .split("") + .reverse() + .reduce((acc, digit, index) => { + if (index > 0 && index % 3 === 0) { + return digit + " " + acc; + } + return digit + acc; + }, ""); // Return formatted number, preserving decimal part if it exists return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger; From a818c4976af8764092117ad742d62d65912c235b Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 21 Oct 2024 17:08:05 +0200 Subject: [PATCH 29/43] refactor(ui): improves `Matrix` types and variables naming refactor(ui): rename variables and add minor improvements --- webapp/src/common/types.ts | 21 +++++ .../Modelization/Areas/Hydro/HydroMatrix.tsx | 4 +- .../explore/Modelization/Areas/Hydro/utils.ts | 16 ++-- .../Modelization/Areas/Storages/Matrix.tsx | 25 +----- .../Modelization/Links/LinkView/LinkForm.tsx | 14 +--- .../Links/LinkView/LinkMatrixView.tsx | 23 +----- .../Results/ResultDetails/ResultFilters.tsx | 12 +-- .../explore/Results/ResultDetails/index.tsx | 8 +- .../components/common/MatrixGrid/Matrix.tsx | 18 ++--- .../common/MatrixGrid/index.test.tsx | 20 ++--- .../components/common/MatrixGrid/index.tsx | 10 +-- .../src/components/common/MatrixGrid/types.ts | 28 +++---- .../MatrixGrid/useColumnMapping.test.ts | 20 ++--- .../common/MatrixGrid/useColumnMapping.ts | 4 +- .../MatrixGrid/useGridCellContent.test.ts | 54 ++++++------- .../common/MatrixGrid/useGridCellContent.ts | 22 ++--- .../components/common/MatrixGrid/useMatrix.ts | 16 ++-- .../common/MatrixGrid/utils.test.ts | 14 ++-- .../src/components/common/MatrixGrid/utils.ts | 81 ++++++++++--------- .../components/common/MatrixInput/index.tsx | 6 +- 20 files changed, 193 insertions(+), 223 deletions(-) diff --git a/webapp/src/common/types.ts b/webapp/src/common/types.ts index 9966fdf62c..a58766e444 100644 --- a/webapp/src/common/types.ts +++ b/webapp/src/common/types.ts @@ -661,3 +661,24 @@ export interface TaskView { } export type ValidationReturn = string | true; + +export interface MatrixConfig { + url: string; + titleKey: string; + columnsNames?: string[]; +} + +export interface SplitMatrixContent { + type: "split"; + matrices: [MatrixConfig, MatrixConfig]; +} + +export interface SingleMatrixContent { + type: "single"; + matrix: MatrixConfig; +} + +export interface MatrixItem { + titleKey: string; + content: SplitMatrixContent | SingleMatrixContent; +} diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index 0a6a6f22e1..ff54f64df7 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -40,8 +40,8 @@ function HydroMatrix({ type }: Props) { customRowHeaders={hydroMatrix.rowHeaders} aggregateColumns={hydroMatrix.aggregates} enableDateTimeColumn={hydroMatrix.enableDateTimeColumn} - enableReadOnly={hydroMatrix.enableReadOnly} - enablePercentDisplay={hydroMatrix.enablePercentDisplay} + readOnly={hydroMatrix.readOnly} + showPercent={hydroMatrix.showPercent} fetchMatrixData={hydroMatrix.fetchFn} /> diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index ac83b0a214..c6438d1125 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -54,8 +54,8 @@ export interface HydroMatrixProps { fetchFn?: fetchMatrixFn; aggregates?: AggregateConfig; enableDateTimeColumn?: boolean; - enableReadOnly?: boolean; - enablePercentDisplay?: boolean; + readOnly?: boolean; + showPercent?: boolean; } type Matrices = Record; @@ -132,7 +132,7 @@ export const MATRICES: Matrices = { columns: generateColumns("%"), rowHeaders: ["Generating Power", "Pumping Power"], enableDateTimeColumn: false, - enablePercentDisplay: true, + showPercent: true, }, [HydroMatrix.EnergyCredits]: { title: "Standard Credits", @@ -148,7 +148,7 @@ export const MATRICES: Matrices = { title: "Reservoir Levels", url: "input/hydro/common/capacity/reservoir_{areaId}", columns: ["Lev Low (%)", "Lev Avg (%)", "Lev High (%)"], - enablePercentDisplay: true, + showPercent: true, }, [HydroMatrix.WaterValues]: { title: "Water Values", @@ -204,16 +204,16 @@ export const MATRICES: Matrices = { url: "", fetchFn: getAllocationMatrix, enableDateTimeColumn: false, - enableReadOnly: true, - enablePercentDisplay: true, + readOnly: true, + showPercent: true, }, [HydroMatrix.Correlation]: { title: "Correlation", url: "", fetchFn: getCorrelationMatrix, enableDateTimeColumn: false, - enableReadOnly: true, - enablePercentDisplay: true, + readOnly: true, + showPercent: true, }, }; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx index 9dbd2484ec..1062c86138 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx @@ -17,7 +17,10 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../../../../common/types"; +import { + type MatrixItem, + type StudyMetadata, +} from "../../../../../../../common/types"; import { Storage } from "./utils"; import SplitView from "../../../../../../common/SplitView"; import Matrix from "../../../../../../common/MatrixGrid/Matrix"; @@ -28,26 +31,6 @@ interface Props { storageId: Storage["id"]; } -interface MatrixConfig { - url: string; - titleKey: string; -} - -interface SplitMatrixContent { - type: "split"; - matrices: [MatrixConfig, MatrixConfig]; -} - -interface SingleMatrixContent { - type: "single"; - matrix: MatrixConfig; -} - -interface MatrixItem { - titleKey: string; - content: SplitMatrixContent | SingleMatrixContent; -} - function StorageMatrices({ areaId, storageId }: Props) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("modulation"); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx index 6bae8f2f63..88d5e389e2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx @@ -22,16 +22,12 @@ import Fieldset from "../../../../../../common/Fieldset"; import { AutoSubmitHandler } from "../../../../../../common/Form/types"; import { getLinkPath, LinkFields } from "./utils"; import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; -import { - LinkElement, - MatrixStats, - StudyMetadata, -} from "../../../../../../../common/types"; +import { LinkElement, StudyMetadata } from "../../../../../../../common/types"; import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; -import MatrixInput from "../../../../../../common/MatrixInput"; import LinkMatrixView from "./LinkMatrixView"; import OutputFilters from "../../../common/OutputFilters"; import { useFormContextPlus } from "../../../../../../common/Form"; +import Matrix from "../../../../../../common/MatrixGrid/Matrix"; interface Props { link: LinkElement; @@ -251,11 +247,9 @@ function LinkForm(props: Props) { {isTabMatrix ? ( ) : ( - )} diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx index c0a92068a3..503b6c3bd1 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx @@ -17,7 +17,7 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; -import { StudyMetadata } from "../../../../../../../common/types"; +import { MatrixItem, StudyMetadata } from "../../../../../../../common/types"; import SplitView from "../../../../../../common/SplitView"; import Matrix from "../../../../../../common/MatrixGrid/Matrix"; @@ -27,27 +27,6 @@ interface Props { area2: string; } -interface MatrixConfig { - url: string; - titleKey: string; - columnsNames?: string[]; -} - -interface SplitMatrixContent { - type: "split"; - matrices: [MatrixConfig, MatrixConfig]; -} - -interface SingleMatrixContent { - type: "single"; - matrix: MatrixConfig; -} - -interface MatrixItem { - titleKey: string; - content: SplitMatrixContent | SingleMatrixContent; -} - function LinkMatrixView({ area1, area2 }: Props) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("parameters"); diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx index bdefbea3c1..d6295c3127 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/ResultFilters.tsx @@ -45,10 +45,10 @@ function ResultFilters({ }: Props) { const { t } = useTranslation(); - const controls = [ + const filters = [ { label: `${t("study.results.mc")}:`, - control: ( + field: ( <> - {controls.map(({ label, control }) => ( + {filters.map(({ label, field }) => ( {label} - {control} + {field} ))} diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index 3011f257d6..fbcfedfe9c 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -58,7 +58,7 @@ import { generateCustomColumns, generateDateTime, } from "../../../../../common/MatrixGrid/utils.ts"; -import { ColumnTypes } from "../../../../../common/MatrixGrid/types.ts"; +import { Column } from "../../../../../common/MatrixGrid/types.ts"; import SplitView from "../../../../../common/SplitView/index.tsx"; import ResultFilters from "./ResultFilters.tsx"; import { toError } from "../../../../../../utils/fnUtils.ts"; @@ -266,7 +266,7 @@ function ResultDetails() { columns={generateCustomColumns({ titles: matrix.columns, })} - isReadOnlyEnabled + isReadOnly /> ) } @@ -284,7 +284,7 @@ function ResultDetails() { { id: "date", title: "Date", - type: ColumnTypes.DateTime, + type: Column.DateTime, editable: false, }, ...generateCustomColumns({ @@ -292,7 +292,7 @@ function ResultDetails() { }), ]} dateTime={dateTime} - isReadOnlyEnabled + isReadOnly /> ) } diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index 8b24f35cb0..ae7cefe6ba 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -33,9 +33,9 @@ interface MatrixProps { enableDateTimeColumn?: boolean; enableTimeSeriesColumns?: boolean; aggregateColumns?: AggregateConfig; - enableRowHeaders?: boolean; - enablePercentDisplay?: boolean; - enableReadOnly?: boolean; + rowHeaders?: boolean; + showPercent?: boolean; + readOnly?: boolean; customColumns?: string[] | readonly string[]; colWidth?: number; fetchMatrixData?: fetchMatrixFn; @@ -48,9 +48,9 @@ function Matrix({ enableDateTimeColumn = true, enableTimeSeriesColumns = true, aggregateColumns = false, - enableRowHeaders = customRowHeaders.length > 0, - enablePercentDisplay = false, - enableReadOnly = false, + rowHeaders = customRowHeaders.length > 0, + showPercent = false, + readOnly = false, customColumns, colWidth, fetchMatrixData, @@ -81,7 +81,7 @@ function Matrix({ url, enableDateTimeColumn, enableTimeSeriesColumns, - enableRowHeaders, + rowHeaders, aggregateColumns, customColumns, colWidth, @@ -132,8 +132,8 @@ function Matrix({ dateTime={dateTime} onCellEdit={handleCellEdit} onMultipleCellsEdit={handleMultipleCellsEdit} - isReadOnlyEnabled={isSubmitting || enableReadOnly} - isPercentDisplayEnabled={enablePercentDisplay} + isReadOnly={isSubmitting || readOnly} + isPercentDisplayEnabled={showPercent} /> {openImportDialog && ( { @@ -67,7 +67,7 @@ describe("MatrixGrid rendering", () => { id: "col1", title: "Column 1", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 0, }, @@ -75,7 +75,7 @@ describe("MatrixGrid rendering", () => { id: "col2", title: "Column 2", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 1, }, @@ -83,7 +83,7 @@ describe("MatrixGrid rendering", () => { id: "col3", title: "Column 3", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 2, }, @@ -118,7 +118,7 @@ describe("MatrixGrid rendering", () => { id: "col1", title: "Column 1", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 0, }, @@ -126,7 +126,7 @@ describe("MatrixGrid rendering", () => { id: "col2", title: "Column 2", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 1, }, @@ -134,7 +134,7 @@ describe("MatrixGrid rendering", () => { id: "col3", title: "Column 3", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 2, }, @@ -171,7 +171,7 @@ describe("MatrixGrid rendering", () => { id: "col1", title: "Column 1", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 0, }, @@ -179,7 +179,7 @@ describe("MatrixGrid rendering", () => { id: "col2", title: "Column 2", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 1, }, @@ -187,7 +187,7 @@ describe("MatrixGrid rendering", () => { id: "col3", title: "Column 3", width: 100, - type: ColumnTypes.Number, + type: Column.Number, editable: true, order: 2, }, diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index 7873ab1d12..b68f608605 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -39,7 +39,7 @@ export interface MatrixGridProps { height?: string; onCellEdit?: (update: GridUpdate) => void; onMultipleCellsEdit?: (updates: GridUpdate[]) => void; - isReadOnlyEnabled?: boolean; + isReadOnly?: boolean; isPercentDisplayEnabled?: boolean; } @@ -54,7 +54,7 @@ function MatrixGrid({ height = "100%", onCellEdit, onMultipleCellsEdit, - isReadOnlyEnabled, + isReadOnly, isPercentDisplayEnabled, }: MatrixGridProps) { const [columns, setColumns] = useState(initialColumns); @@ -66,7 +66,7 @@ function MatrixGrid({ const { gridToData } = useColumnMapping(columns); const theme = useMemo(() => { - if (isReadOnlyEnabled) { + if (isReadOnly) { return { ...darkTheme, ...readOnlyDarkTheme, @@ -74,7 +74,7 @@ function MatrixGrid({ } return darkTheme; - }, [isReadOnlyEnabled]); + }, [isReadOnly]); const getCellContent = useGridCellContent( data, @@ -83,7 +83,7 @@ function MatrixGrid({ dateTime, aggregates, rowHeaders, - isReadOnlyEnabled, + isReadOnly, isPercentDisplayEnabled, ); diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index a996de9a63..aa6044fca1 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -22,16 +22,14 @@ import { // Enums //////////////////////////////////////////////////////////////// -// TODO update enums to be singular - -export const ColumnTypes = { +export const Column = { DateTime: "datetime", Number: "number", Text: "text", Aggregate: "aggregate", } as const; -export const Operations = { +export const Operation = { Add: "+", Sub: "-", Mul: "*", @@ -41,7 +39,7 @@ export const Operations = { } as const; // !NOTE: Keep lowercase to match Glide Data Grid column ids -export const Aggregates = { +export const Aggregate = { Min: "min", Max: "max", Avg: "avg", @@ -49,12 +47,11 @@ export const Aggregates = { } as const; export const TimeFrequency = { - // TODO update old enum occurrences - ANNUAL: "annual", - MONTHLY: "monthly", - WEEKLY: "weekly", - DAILY: "daily", - HOURLY: "hourly", + Annual: "annual", + Monthly: "monthly", + Weekly: "weekly", + Daily: "daily", + Hourly: "hourly", } as const; //////////////////////////////////////////////////////////////// @@ -62,10 +59,9 @@ export const TimeFrequency = { //////////////////////////////////////////////////////////////// // Derived types -export type ColumnType = (typeof ColumnTypes)[keyof typeof ColumnTypes]; -// TODO add sufix Type -export type Operation = (typeof Operations)[keyof typeof Operations]; -export type AggregateType = (typeof Aggregates)[keyof typeof Aggregates]; +export type ColumnType = (typeof Column)[keyof typeof Column]; +export type OperationType = (typeof Operation)[keyof typeof Operation]; +export type AggregateType = (typeof Aggregate)[keyof typeof Aggregate]; export type TimeFrequencyType = (typeof TimeFrequency)[keyof typeof TimeFrequency]; @@ -127,7 +123,7 @@ export interface GridUpdate { // Shape of updates to be sent to the API export interface MatrixUpdate { - operation: Operation; + operation: OperationType; value: number; } diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts b/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts index e88473b9a6..2dc3ebd7f2 100644 --- a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts +++ b/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts @@ -15,42 +15,42 @@ import { renderHook } from "@testing-library/react"; import { describe, test, expect } from "vitest"; import { useColumnMapping } from "./useColumnMapping"; -import { EnhancedGridColumn, ColumnTypes } from "./types"; +import { EnhancedGridColumn, Column } from "./types"; describe("useColumnMapping", () => { const testColumns: EnhancedGridColumn[] = [ { id: "text", title: "Text", - type: ColumnTypes.Text, + type: Column.Text, width: 100, editable: false, }, { id: "date", title: "Date", - type: ColumnTypes.DateTime, + type: Column.DateTime, width: 100, editable: false, }, { id: "num1", title: "Number 1", - type: ColumnTypes.Number, + type: Column.Number, width: 100, editable: true, }, { id: "num2", title: "Number 2", - type: ColumnTypes.Number, + type: Column.Number, width: 100, editable: true, }, { id: "agg", title: "Aggregate", - type: ColumnTypes.Aggregate, + type: Column.Aggregate, width: 100, editable: false, }, @@ -90,14 +90,14 @@ describe("useColumnMapping", () => { { id: "text", title: "Text", - type: ColumnTypes.Text, + type: Column.Text, width: 100, editable: false, }, { id: "date", title: "Date", - type: ColumnTypes.DateTime, + type: Column.DateTime, width: 100, editable: false, }, @@ -113,14 +113,14 @@ describe("useColumnMapping", () => { { id: "num1", title: "Number 1", - type: ColumnTypes.Number, + type: Column.Number, width: 100, editable: true, }, { id: "num2", title: "Number 2", - type: ColumnTypes.Number, + type: Column.Number, width: 100, editable: true, }, diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.ts b/webapp/src/components/common/MatrixGrid/useColumnMapping.ts index 522f93d025..ffbf683965 100644 --- a/webapp/src/components/common/MatrixGrid/useColumnMapping.ts +++ b/webapp/src/components/common/MatrixGrid/useColumnMapping.ts @@ -14,7 +14,7 @@ import { useMemo } from "react"; import { Item } from "@glideapps/glide-data-grid"; -import { EnhancedGridColumn, ColumnTypes } from "./types"; +import { EnhancedGridColumn, Column } from "./types"; /** * A custom hook that provides coordinate mapping functions for a grid with mixed column types. @@ -48,7 +48,7 @@ import { EnhancedGridColumn, ColumnTypes } from "./types"; export function useColumnMapping(columns: EnhancedGridColumn[]) { return useMemo(() => { const dataColumnIndices = columns.reduce((acc, col, index) => { - if (col.type === ColumnTypes.Number) { + if (col.type === Column.Number) { acc.push(index); } return acc; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts index 8adf3041d5..cdb2c2003c 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts @@ -14,11 +14,7 @@ import { renderHook } from "@testing-library/react"; import { useGridCellContent } from "./useGridCellContent"; -import { - ColumnTypes, - MatrixAggregates, - type EnhancedGridColumn, -} from "./types"; +import { Column, MatrixAggregates, type EnhancedGridColumn } from "./types"; import { useColumnMapping } from "./useColumnMapping"; // Mocking i18next @@ -85,7 +81,7 @@ describe("useGridCellContent", () => { { id: "total", title: "Total", - type: ColumnTypes.Aggregate, + type: Column.Aggregate, width: 100, editable: false, }, @@ -136,28 +132,28 @@ describe("useGridCellContent", () => { { id: "date", title: "Date", - type: ColumnTypes.DateTime, + type: Column.DateTime, width: 150, editable: false, }, { id: "ts1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "ts2", title: "TS 2", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "total", title: "Total", - type: ColumnTypes.Aggregate, + type: Column.Aggregate, width: 100, editable: false, }, @@ -208,35 +204,35 @@ describe("useGridCellContent with mixed column types", () => { { id: "rowHeader", title: "Row", - type: ColumnTypes.Text, + type: Column.Text, width: 100, editable: false, }, { id: "date", title: "Date", - type: ColumnTypes.DateTime, + type: Column.DateTime, width: 150, editable: false, }, { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "data2", title: "TS 2", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "total", title: "Total", - type: ColumnTypes.Aggregate, + type: Column.Aggregate, width: 100, editable: false, }, @@ -307,21 +303,21 @@ describe("useGridCellContent with mixed column types", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "data2", title: "TS 2", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "data3", title: "TS 3", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -352,7 +348,7 @@ describe("useGridCellContent additional tests", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -374,7 +370,7 @@ describe("useGridCellContent additional tests", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -397,7 +393,7 @@ describe("useGridCellContent additional tests", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -419,7 +415,7 @@ describe("useGridCellContent additional tests", () => { { id: "total", title: "Total", - type: ColumnTypes.Aggregate, + type: Column.Aggregate, width: 100, editable: false, }, @@ -444,14 +440,14 @@ describe("useGridCellContent additional tests", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "data2", title: "TS 2", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: false, }, @@ -476,7 +472,7 @@ describe("useGridCellContent additional tests", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -500,14 +496,14 @@ describe("useGridCellContent additional tests", () => { { id: "large", title: "Large", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, { id: "small", title: "Small", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -539,7 +535,7 @@ describe("useGridCellContent additional tests", () => { { id: "negative", title: "Negative", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, @@ -563,7 +559,7 @@ describe("useGridCellContent additional tests", () => { { id: "zero", title: "Zero", - type: ColumnTypes.Number, + type: Column.Number, width: 50, editable: true, }, diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index 57c94c5fe4..2257fa518d 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -17,8 +17,8 @@ import { GridCell, GridCellKind, Item } from "@glideapps/glide-data-grid"; import { type EnhancedGridColumn, type ColumnType, - ColumnTypes, MatrixAggregates, + Column, } from "./types"; import { formatNumber } from "./utils"; @@ -37,7 +37,7 @@ type CellContentGenerator = ( * Each generator function creates the appropriate GridCell based on the column type and data. */ const cellContentGenerators: Record = { - [ColumnTypes.Text]: ( + [Column.Text]: ( row, col, column, @@ -52,14 +52,14 @@ const cellContentGenerators: Record = { readonly: !column.editable, allowOverlay: false, }), - [ColumnTypes.DateTime]: (row, col, column, data, dateTime) => ({ + [Column.DateTime]: (row, col, column, data, dateTime) => ({ kind: GridCellKind.Text, data: "", // Date/time columns are not editable displayData: dateTime?.[row] ?? "", readonly: !column.editable, allowOverlay: false, }), - [ColumnTypes.Number]: (row, col, column, data) => { + [Column.Number]: (row, col, column, data) => { const value = data?.[row]?.[col]; return { @@ -72,7 +72,7 @@ const cellContentGenerators: Record = { thousandSeparator: " ", }; }, - [ColumnTypes.Aggregate]: (row, col, column, data, dateTime, aggregates) => { + [Column.Aggregate]: (row, col, column, data, dateTime, aggregates) => { const value = aggregates?.[column.id as keyof MatrixAggregates]?.[row]; return { @@ -109,7 +109,7 @@ const cellContentGenerators: Record = { * @param dateTime - Optional array of date-time strings for date columns. * @param aggregates - Optional object mapping column IDs to arrays of aggregated values. * @param rowHeaders - Optional array of row header labels. - * @param isReadOnlyEnabled - Whether the grid is read-only (default is false). + * @param isReadOnly - Whether the grid is read-only (default is false). * @param isPercentDisplayEnabled - Whether to display number values as percentages (default is false). * @returns A function that accepts a grid item and returns the configured grid cell content. */ @@ -120,7 +120,7 @@ export function useGridCellContent( dateTime?: string[], aggregates?: Partial, rowHeaders?: string[], - isReadOnlyEnabled = false, + isReadOnly = false, isPercentDisplayEnabled = false, ): (cell: Item) => GridCell { const columnMap = useMemo(() => { @@ -160,7 +160,7 @@ export function useGridCellContent( // accounting for any non-data columns in the grid let adjustedCol = col; - if (column.type === ColumnTypes.Number && gridToData) { + if (column.type === Column.Number && gridToData) { // Map grid cell to data array index const dataCell = gridToData(cell); @@ -185,12 +185,12 @@ export function useGridCellContent( ...gridCell, displayData: `${gridCell.data}%`, // If ReadOnly is enabled, we don't want to allow overlay - allowOverlay: !isReadOnlyEnabled, + allowOverlay: !isReadOnly, }; } // Prevent updates for read-only grids - if (isReadOnlyEnabled) { + if (isReadOnly) { return { ...gridCell, allowOverlay: false, @@ -206,7 +206,7 @@ export function useGridCellContent( dateTime, aggregates, rowHeaders, - isReadOnlyEnabled, + isReadOnly, isPercentDisplayEnabled, ], ); diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index b747aadce2..225b1146e0 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -26,13 +26,13 @@ import { getStudyData } from "../../../services/api/study"; import { EnhancedGridColumn, MatrixDataDTO, - ColumnTypes, GridUpdate, MatrixUpdateDTO, MatrixAggregates, AggregateConfig, - Aggregates, - Operations, + Column, + Operation, + Aggregate, } from "./types"; import { aggregatesTheme, @@ -158,7 +158,7 @@ export function useMatrix( baseColumns.push({ id: "date", title: "Date", - type: ColumnTypes.DateTime, + type: Column.DateTime, editable: false, themeOverride: { bgCell: "#2D2E40" }, }); @@ -168,7 +168,7 @@ export function useMatrix( baseColumns.unshift({ id: "rowHeaders", title: "", - type: ColumnTypes.Text, + type: Column.Text, editable: false, }); } @@ -184,10 +184,10 @@ export function useMatrix( (aggregateType) => ({ id: aggregateType, title: aggregateType.charAt(0).toUpperCase() + aggregateType.slice(1), // Capitalize first letter - type: ColumnTypes.Aggregate, + type: Column.Aggregate, editable: false, themeOverride: - aggregateType === Aggregates.Avg + aggregateType === Aggregate.Avg ? aggregatesTheme : { ...aggregatesTheme, bgCell: "#464770" }, }), @@ -218,7 +218,7 @@ export function useMatrix( return { coordinates: [[col, row]], operation: { - operation: Operations.Eq, + operation: Operation.Eq, value: value.data, }, }; diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index 403e46c0ed..93a419f742 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -16,7 +16,7 @@ import { MatrixIndex, StudyOutputDownloadLevelDTO, } from "../../../common/types"; -import { ColumnTypes } from "./types"; +import { Column } from "./types"; import { calculateMatrixAggregates, formatNumber, @@ -49,21 +49,21 @@ describe("generateTimeSeriesColumns", () => { { id: "data1", title: "TS 1", - type: ColumnTypes.Number, + type: Column.Number, style: "normal", editable: true, }, { id: "data2", title: "TS 2", - type: ColumnTypes.Number, + type: Column.Number, style: "normal", editable: true, }, { id: "data3", title: "TS 3", - type: ColumnTypes.Number, + type: Column.Number, style: "normal", editable: true, }, @@ -81,14 +81,14 @@ describe("generateTimeSeriesColumns", () => { { id: "data10", title: "Data 10", - type: ColumnTypes.Number, + type: Column.Number, style: "normal", editable: false, }, { id: "data11", title: "Data 11", - type: ColumnTypes.Number, + type: Column.Number, style: "normal", editable: false, }, @@ -110,7 +110,7 @@ describe("generateTimeSeriesColumns", () => { test("maintains consistent type and style", () => { const result = generateTimeSeriesColumns({ count: 1000 }); result.forEach((column) => { - expect(column.type).toBe(ColumnTypes.Number); + expect(column.type).toBe(Column.Number); expect(column.style).toBe("normal"); }); }); diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 89b126afa1..37254f0211 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -14,18 +14,18 @@ import { EnhancedGridColumn, - ColumnTypes, TimeSeriesColumnOptions, CustomColumnOptions, MatrixAggregates, AggregateType, AggregateConfig, - Aggregates, DateIncrementFunction, FormatFunction, TimeFrequency, TimeFrequencyType, DateTimeMetadataDTO, + Aggregate, + Column, } from "./types"; import { type FirstWeekContainsDate, @@ -82,7 +82,7 @@ export const darkTheme: Theme = { export const readOnlyDarkTheme: Partial = { bgCell: "#1A1C2A", bgCellMedium: "#22243A", - textDark: "#A0A0A0", + textDark: "#FAF9F6", textMedium: "#808080", textLight: "#606060", accentColor: "#4A4C66", @@ -118,7 +118,6 @@ export function formatNumber(num: number | undefined): string { const [integerPart, decimalPart] = num.toString().split("."); - // Format integer part with thousand separators using a non-regex approach const formattedInteger = integerPart .split("") .reverse() @@ -152,15 +151,15 @@ const TIME_FREQUENCY_CONFIG: Record< format: FormatFunction; } > = { - [TimeFrequency.ANNUAL]: { + [TimeFrequency.Annual]: { increment: addYears, format: () => t("global.time.annual"), }, - [TimeFrequency.MONTHLY]: { + [TimeFrequency.Monthly]: { increment: addMonths, format: (date: Date) => format(date, "MMM", { locale: getLocale() }), }, - [TimeFrequency.WEEKLY]: { + [TimeFrequency.Weekly]: { increment: addWeeks, format: (date: Date, firstWeekSize: number) => { const weekStart = startOfWeek(date, { locale: getLocale() }); @@ -172,11 +171,11 @@ const TIME_FREQUENCY_CONFIG: Record< }); }, }, - [TimeFrequency.DAILY]: { + [TimeFrequency.Daily]: { increment: addDays, format: (date: Date) => format(date, "EEE d MMM", { locale: getLocale() }), }, - [TimeFrequency.HOURLY]: { + [TimeFrequency.Hourly]: { increment: addHours, format: (date: Date) => format(date, "EEE d MMM HH:mm", { locale: getLocale() }), @@ -222,12 +221,12 @@ export const generateDateTime = (config: DateTimeMetadataDTO): string[] => { * * @example Usage within a column definition array * const columns = [ - * { id: "rowHeaders", title: "", type: ColumnTypes.Text, ... }, - * { id: "date", title: "Date", type: ColumnTypes.DateTime, ... }, + * { id: "rowHeaders", title: "", type: Column.Text, ... }, + * { id: "date", title: "Date", type: Column.DateTime, ... }, * ...generateTimeSeriesColumns({ count: 60 }), - * { id: "min", title: "Min", type: ColumnTypes.Aggregate, ... }, - * { id: "max", title: "Max", type: ColumnTypes.Aggregate, ... }, - * { id: "avg", title: "Avg", type: ColumnTypes.Aggregate, ... } + * { id: "min", title: "Min", type: Column.Aggregate, ... }, + * { id: "max", title: "Max", type: Column.Aggregate, ... }, + * { id: "avg", title: "Avg", type: Column.Aggregate, ... } * ]; */ export function generateTimeSeriesColumns({ @@ -241,7 +240,7 @@ export function generateTimeSeriesColumns({ return Array.from({ length: count }, (_, index) => ({ id: `data${startIndex + index}`, title: `${prefix} ${startIndex + index}`, - type: ColumnTypes.Number, + type: Column.Number, style, width, editable, @@ -263,7 +262,7 @@ export function generateCustomColumns({ return titles.map((title, index) => ({ id: `custom${index + 1}`, title, - type: ColumnTypes.Number, + type: Column.Number, style: "normal", width, editable: true, @@ -298,16 +297,21 @@ export function generateDataColumns( return []; } -// TODO add docs + refactor +/** + * Determines the aggregate types based on the provided configuration. + * + * @param aggregateConfig - The configuration for aggregates. + * @returns An array of AggregateType. + */ export function getAggregateTypes( aggregateConfig: AggregateConfig, ): AggregateType[] { if (aggregateConfig === "stats") { - return [Aggregates.Avg, Aggregates.Min, Aggregates.Max]; + return [Aggregate.Avg, Aggregate.Min, Aggregate.Max]; } if (aggregateConfig === "all") { - return [Aggregates.Min, Aggregates.Max, Aggregates.Avg, Aggregates.Total]; + return [Aggregate.Min, Aggregate.Max, Aggregate.Avg, Aggregate.Total]; } if (Array.isArray(aggregateConfig)) { @@ -317,6 +321,13 @@ export function getAggregateTypes( return []; } +/** + * Calculates matrix aggregates based on the provided matrix and aggregate types. + * + * @param matrix - The input matrix of numbers. + * @param aggregateTypes - The types of aggregates to calculate. + * @returns An object containing the calculated aggregates. + */ export function calculateMatrixAggregates( matrix: number[][], aggregateTypes: AggregateType[], @@ -324,39 +335,29 @@ export function calculateMatrixAggregates( const aggregates: Partial = {}; matrix.forEach((row) => { - if (aggregateTypes.includes(Aggregates.Min)) { - if (!aggregates.min) { - aggregates.min = []; - } + if (aggregateTypes.includes(Aggregate.Min)) { + aggregates.min = aggregates.min || []; aggregates.min.push(Math.min(...row)); } - if (aggregateTypes.includes(Aggregates.Max)) { - if (!aggregates.max) { - aggregates.max = []; - } + if (aggregateTypes.includes(Aggregate.Max)) { + aggregates.max = aggregates.max || []; aggregates.max.push(Math.max(...row)); } if ( - aggregateTypes.includes(Aggregates.Avg) || - aggregateTypes.includes(Aggregates.Total) + aggregateTypes.includes(Aggregate.Avg) || + aggregateTypes.includes(Aggregate.Total) ) { - const sum = row.reduce((sum, num) => sum + num, 0); - - if (aggregateTypes.includes(Aggregates.Avg)) { - if (!aggregates.avg) { - aggregates.avg = []; - } + const sum = row.reduce((acc, num) => acc + num, 0); + if (aggregateTypes.includes(Aggregate.Avg)) { + aggregates.avg = aggregates.avg || []; aggregates.avg.push(Number((sum / row.length).toFixed())); } - if (aggregateTypes.includes(Aggregates.Total)) { - if (!aggregates.total) { - aggregates.total = []; - } - + if (aggregateTypes.includes(Aggregate.Total)) { + aggregates.total = aggregates.total || []; aggregates.total.push(Number(sum.toFixed())); } } diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index 62219a43d8..4034517235 100644 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ b/webapp/src/components/common/MatrixInput/index.tsx @@ -49,7 +49,7 @@ interface Props { fetchFn?: fetchMatrixFn; disableEdit?: boolean; disableImport?: boolean; - enablePercentDisplay?: boolean; + showPercent?: boolean; } function MatrixInput({ @@ -62,7 +62,7 @@ function MatrixInput({ fetchFn, disableEdit = false, disableImport = false, - enablePercentDisplay, + showPercent, }: Props) { const { enqueueSnackbar } = useSnackbar(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); @@ -201,7 +201,7 @@ function MatrixInput({ readOnly={disableEdit} onUpdate={handleUpdate} computStats={computStats} - isPercentDisplayEnabled={enablePercentDisplay} + isPercentDisplayEnabled={showPercent} /> ) : ( !isLoading && ( From e95c0636a74229a9327e681a660f8a31caee221f Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 16 Oct 2024 16:26:41 +0200 Subject: [PATCH 30/43] test(ui): enhance `utils` tests and add `MatrixActions` component tests --- .../common/MatrixGrid/MatrixActions.test.tsx | 164 +++++++++++ .../MatrixGrid/useGridCellContent.test.ts | 34 --- .../common/MatrixGrid/utils.test.ts | 258 +++++++++++++++++- 3 files changed, 420 insertions(+), 36 deletions(-) create mode 100644 webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx diff --git a/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx b/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx new file mode 100644 index 0000000000..f7b3802fef --- /dev/null +++ b/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2024, 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 React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import MatrixActions from "./MatrixActions"; + +vi.mock("../buttons/SplitButton", () => ({ + default: ({ + children, + onClick, + disabled, + }: { + children: React.ReactNode; + onClick: () => void; + disabled?: boolean; + }) => ( + + ), +})); + +vi.mock("../buttons/DownloadMatrixButton", () => ({ + default: ({ disabled, label }: { disabled: boolean; label?: string }) => ( + + ), +})); + +describe("MatrixActions", () => { + const defaultProps = { + onImport: vi.fn(), + onSave: vi.fn(), + studyId: "study1", + path: "/path/to/matrix", + disabled: false, + pendingUpdatesCount: 0, + isSubmitting: false, + undo: vi.fn(), + redo: vi.fn(), + canUndo: true, + canRedo: true, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders all buttons and controls", () => { + render(); + + expect(screen.getByLabelText("global.undo")).toBeDefined(); + expect(screen.getByLabelText("global.redo")).toBeDefined(); + expect(screen.getByText("(0)")).toBeDefined(); + expect(screen.getByText("global.import")).toBeDefined(); + expect(screen.getByText("global.export")).toBeDefined(); + }); + + it("disables undo button when canUndo is false", () => { + render(); + expect( + screen.getByLabelText("global.undo").querySelector("button"), + ).toBeDisabled(); + }); + + it("disables redo button when canRedo is false", () => { + render(); + expect( + screen.getByLabelText("global.redo").querySelector("button"), + ).toBeDisabled(); + }); + + it("calls undo function when undo button is clicked", () => { + render(); + const undoButton = screen + .getByLabelText("global.undo") + .querySelector("button"); + if (undoButton) { + fireEvent.click(undoButton); + expect(defaultProps.undo).toHaveBeenCalled(); + } else { + throw new Error("Undo button not found"); + } + }); + + it("calls redo function when redo button is clicked", () => { + render(); + const redoButton = screen + .getByLabelText("global.redo") + .querySelector("button"); + if (redoButton) { + fireEvent.click(redoButton); + expect(defaultProps.redo).toHaveBeenCalled(); + } else { + throw new Error("Redo button not found"); + } + }); + + it("disables save button when pendingUpdatesCount is 0", () => { + render(); + expect(screen.getByText("(0)").closest("button")).toBeDisabled(); + }); + + it("enables save button when pendingUpdatesCount is greater than 0", () => { + render(); + expect(screen.getByText("(1)").closest("button")).not.toBeDisabled(); + }); + + it("calls onSave function when save button is clicked", () => { + render(); + const saveButton = screen.getByText("(1)").closest("button"); + if (saveButton) { + fireEvent.click(saveButton); + expect(defaultProps.onSave).toHaveBeenCalled(); + } else { + throw new Error("Save button not found"); + } + }); + + it("shows loading state on save button when isSubmitting is true", () => { + render( + , + ); + expect(screen.getByText("(1)").closest("button")).toBeDisabled(); + }); + + it("calls onImport function when import button is clicked", () => { + render(); + fireEvent.click(screen.getByText("global.import")); + expect(defaultProps.onImport).toHaveBeenCalled(); + }); + + it("disables import button when isSubmitting is true", () => { + render(); + expect(screen.getByText("global.import")).toBeDisabled(); + }); + + it("passes correct props to DownloadMatrixButton", () => { + const { rerender } = render(); + expect(screen.getByText("global.export")).not.toBeDisabled(); + + rerender(); + expect(screen.getByText("global.export")).toBeDisabled(); + + rerender(); + expect(screen.getByText("global.export")).toBeDisabled(); + }); +}); diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts index cdb2c2003c..ad28cfed59 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts @@ -17,40 +17,6 @@ import { useGridCellContent } from "./useGridCellContent"; import { Column, MatrixAggregates, type EnhancedGridColumn } from "./types"; import { useColumnMapping } from "./useColumnMapping"; -// Mocking i18next -vi.mock("i18next", () => { - const i18n = { - language: "fr", - use: vi.fn().mockReturnThis(), - init: vi.fn(), - t: vi.fn((key) => key), - changeLanguage: vi.fn((lang) => { - i18n.language = lang; - return Promise.resolve(); - }), - on: vi.fn(), - }; - return { default: i18n }; -}); - -// Mocking react-i18next -vi.mock("react-i18next", async (importOriginal) => { - const actual = await importOriginal(); - return Object.assign({}, actual, { - useTranslation: () => ({ - t: vi.fn((key) => key), - i18n: { - changeLanguage: vi.fn(), - language: "fr", - }, - }), - initReactI18next: { - type: "3rdParty", - init: vi.fn(), - }, - }); -}); - function renderGridCellContent( data: number[][], columns: EnhancedGridColumn[], diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index 93a419f742..c2054c34fd 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -16,18 +16,112 @@ import { MatrixIndex, StudyOutputDownloadLevelDTO, } from "../../../common/types"; -import { Column } from "./types"; +import { + Aggregate, + AggregateType, + Column, + DateTimeMetadataDTO, + TimeFrequency, +} from "./types"; import { calculateMatrixAggregates, formatNumber, + generateCustomColumns, + generateDataColumns, generateDateTime, generateTimeSeriesColumns, + getAggregateTypes, } from "./utils"; +vi.mock("date-fns", async () => { + const actual = (await vi.importActual( + "date-fns", + )) as typeof import("date-fns"); + return { + ...actual, + format: vi.fn((date: Date, formatString: string) => { + if (formatString.includes("ww")) { + const weekNumber = actual.getWeek(date); + return `W ${weekNumber.toString().padStart(2, "0")}`; + } + return actual.format(date, formatString); + }), + }; +}); + describe("generateDateTime", () => { + beforeAll(() => { + // Set a fixed date for consistent testing + vi.useFakeTimers(); + vi.setSystemTime(new Date("2023-01-01 00:00:00")); + }); + + it("generates correct annual format", () => { + const config: DateTimeMetadataDTO = { + start_date: "2023-01-01 00:00:00", + steps: 3, + first_week_size: 7, + level: TimeFrequency.Annual, + }; + const result = generateDateTime(config); + expect(result).toEqual([ + "global.time.annual", + "global.time.annual", + "global.time.annual", + ]); + }); + + it("generates correct monthly format", () => { + const config: DateTimeMetadataDTO = { + start_date: "2023-01-01 00:00:00", + steps: 3, + first_week_size: 7, + level: TimeFrequency.Monthly, + }; + const result = generateDateTime(config); + expect(result).toEqual(["Jan", "Feb", "Mar"]); + }); + + it("generates correct weekly format with first_week_size 1", () => { + const config: DateTimeMetadataDTO = { + start_date: "2023-01-01 00:00:00", + steps: 3, + first_week_size: 1, + level: TimeFrequency.Weekly, + }; + const result = generateDateTime(config); + expect(result).toEqual(["W 01", "W 02", "W 03"]); + }); + + it("generates correct daily format", () => { + const config: DateTimeMetadataDTO = { + start_date: "2023-01-01 00:00:00", + steps: 3, + first_week_size: 7, + level: TimeFrequency.Daily, + }; + const result = generateDateTime(config); + expect(result).toEqual(["Sun 1 Jan", "Mon 2 Jan", "Tue 3 Jan"]); + }); + + it("generates correct hourly format", () => { + const config: DateTimeMetadataDTO = { + start_date: "2039-07-01 00:00:00", + steps: 3, + first_week_size: 7, + level: TimeFrequency.Hourly, + }; + const result = generateDateTime(config); + expect(result).toEqual([ + "Fri 1 Jul 00:00", + "Fri 1 Jul 01:00", + "Fri 1 Jul 02:00", + ]); + }); + test("generates correct number of dates", () => { const metadata: MatrixIndex = { - start_date: "2023-01-01T00:00:00Z", + start_date: "2023-01-01 00:00:00", steps: 5, first_week_size: 7, level: StudyOutputDownloadLevelDTO.DAILY, @@ -231,3 +325,163 @@ describe("formatNumber", () => { expect(formatNumber(1e20)).toBe("100 000 000 000 000 000 000"); }); }); + +describe("generateCustomColumns", () => { + it("should generate custom columns with correct properties", () => { + const titles = ["Custom 1", "Custom 2", "Custom 3"]; + const width = 100; + const result = generateCustomColumns({ titles, width }); + + expect(result).toHaveLength(3); + result.forEach((column, index) => { + expect(column).toEqual({ + id: `custom${index + 1}`, + title: titles[index], + type: Column.Number, + style: "normal", + width: 100, + editable: true, + }); + }); + }); + + it("should handle empty titles array", () => { + const result = generateCustomColumns({ titles: [], width: 100 }); + expect(result).toEqual([]); + }); +}); + +describe("generateDataColumns", () => { + it("should generate custom columns when provided", () => { + const customColumns = ["Custom 1", "Custom 2"]; + const result = generateDataColumns(false, 5, customColumns, 100); + + expect(result).toHaveLength(2); + expect(result[0].title).toBe("Custom 1"); + expect(result[1].title).toBe("Custom 2"); + }); + + it("should generate time series columns when enabled", () => { + const result = generateDataColumns(true, 3); + + expect(result).toHaveLength(3); + expect(result[0].title).toBe("TS 1"); + expect(result[1].title).toBe("TS 2"); + expect(result[2].title).toBe("TS 3"); + }); + + it("should return empty array when custom columns not provided and time series disabled", () => { + const result = generateDataColumns(false, 5); + expect(result).toEqual([]); + }); +}); + +describe("getAggregateTypes", () => { + it('should return correct aggregate types for "stats" config', () => { + const result = getAggregateTypes("stats"); + expect(result).toEqual([Aggregate.Avg, Aggregate.Min, Aggregate.Max]); + }); + + it('should return correct aggregate types for "all" config', () => { + const result = getAggregateTypes("all"); + expect(result).toEqual([ + Aggregate.Min, + Aggregate.Max, + Aggregate.Avg, + Aggregate.Total, + ]); + }); + + it("should return provided aggregate types for array config", () => { + const config: AggregateType[] = [Aggregate.Min, Aggregate.Max]; + const result = getAggregateTypes(config); + expect(result).toEqual(config); + }); + + it("should return empty array for invalid config", () => { + // @ts-expect-error : we are testing an invalid value + const result = getAggregateTypes("invalid"); + expect(result).toEqual([]); + }); +}); + +describe("calculateMatrixAggregates", () => { + const matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]; + + it("should calculate min aggregate correctly", () => { + const result = calculateMatrixAggregates(matrix, [Aggregate.Min]); + expect(result).toEqual({ min: [1, 4, 7] }); + }); + + it("should calculate max aggregate correctly", () => { + const result = calculateMatrixAggregates(matrix, [Aggregate.Max]); + expect(result).toEqual({ max: [3, 6, 9] }); + }); + + it("should calculate avg aggregate correctly", () => { + const result = calculateMatrixAggregates(matrix, [Aggregate.Avg]); + expect(result).toEqual({ avg: [2, 5, 8] }); + }); + + it("should calculate total aggregate correctly", () => { + const result = calculateMatrixAggregates(matrix, [Aggregate.Total]); + expect(result).toEqual({ total: [6, 15, 24] }); + }); + + it("should calculate multiple aggregates correctly", () => { + const result = calculateMatrixAggregates(matrix, [ + Aggregate.Min, + Aggregate.Max, + Aggregate.Avg, + Aggregate.Total, + ]); + expect(result).toEqual({ + min: [1, 4, 7], + max: [3, 6, 9], + avg: [2, 5, 8], + total: [6, 15, 24], + }); + }); + + it("should handle empty matrix", () => { + const result = calculateMatrixAggregates( + [], + [Aggregate.Min, Aggregate.Max, Aggregate.Avg, Aggregate.Total], + ); + expect(result).toEqual({}); + }); +}); + +describe("formatNumber", () => { + it("should format integer numbers correctly", () => { + expect(formatNumber(1234567)).toBe("1 234 567"); + expect(formatNumber(1000000)).toBe("1 000 000"); + expect(formatNumber(1)).toBe("1"); + }); + + it("should format decimal numbers correctly", () => { + expect(formatNumber(1234.56)).toBe("1 234.56"); + expect(formatNumber(1000000.123)).toBe("1 000 000.123"); + }); + + it("should handle negative numbers", () => { + expect(formatNumber(-1234567)).toBe("-1 234 567"); + expect(formatNumber(-1234.56)).toBe("-1 234.56"); + }); + + it("should handle zero", () => { + expect(formatNumber(0)).toBe("0"); + }); + + it("should handle undefined", () => { + expect(formatNumber(undefined)).toBe(""); + }); + + it("should handle large numbers", () => { + expect(formatNumber(1e15)).toBe("1 000 000 000 000 000"); + }); +}); From 6f9cd44da25fa0bc135bd97f5033796feb248ae0 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 21 Oct 2024 17:25:23 +0200 Subject: [PATCH 31/43] feat(ui-debug): replace `MatrixInput` and add disable import prop --- .../Singlestudy/explore/Debug/Data/Matrix.tsx | 18 +++++------------- .../components/common/MatrixGrid/Matrix.tsx | 3 +++ .../common/MatrixGrid/MatrixActions.tsx | 4 +++- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx index 4957e3a88e..f360f12a13 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx @@ -12,20 +12,12 @@ * This file is part of the Antares project. */ -import { MatrixStats } from "../../../../../../common/types"; -import MatrixInput from "../../../../../common/MatrixInput"; +import Matrix from "../../../../../common/MatrixGrid/Matrix"; import type { DataCompProps } from "../utils"; -function Matrix({ studyId, filename, filePath, canEdit }: DataCompProps) { - return ( - - ); +function DebugMatrix({ studyId, filename, filePath, canEdit }: DataCompProps) { + console.log("canEdit", canEdit); + return ; } -export default Matrix; +export default DebugMatrix; diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index ae7cefe6ba..c10dd05f7b 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -39,6 +39,7 @@ interface MatrixProps { customColumns?: string[] | readonly string[]; colWidth?: number; fetchMatrixData?: fetchMatrixFn; + isImportDisabled?: boolean; } function Matrix({ @@ -54,6 +55,7 @@ function Matrix({ customColumns, colWidth, fetchMatrixData, + isImportDisabled = false, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -120,6 +122,7 @@ function Matrix({ redo={redo} canUndo={canUndo} canRedo={canRedo} + isImportDisabled={isImportDisabled} /> diff --git a/webapp/src/components/common/MatrixGrid/MatrixActions.tsx b/webapp/src/components/common/MatrixGrid/MatrixActions.tsx index 46cae239cc..d822c13f69 100644 --- a/webapp/src/components/common/MatrixGrid/MatrixActions.tsx +++ b/webapp/src/components/common/MatrixGrid/MatrixActions.tsx @@ -33,6 +33,7 @@ interface MatrixActionsProps { redo: VoidFunction; canUndo: boolean; canRedo: boolean; + isImportDisabled?: boolean; } function MatrixActions({ @@ -47,6 +48,7 @@ function MatrixActions({ redo, canUndo, canRedo, + isImportDisabled = false, }: MatrixActionsProps) { const { t } = useTranslation(); @@ -99,7 +101,7 @@ function MatrixActions({ ButtonProps={{ startIcon: , }} - disabled={isSubmitting} + disabled={isSubmitting || isImportDisabled} > {t("global.import")} From d84b6e1c8147484ee1767515507530e8d9934300 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 23 Oct 2024 14:33:09 +0200 Subject: [PATCH 32/43] fix(ui): correct display condition in `Matrix` to handle empty subarray presence correctly --- .../components/App/Singlestudy/explore/Debug/Data/Matrix.tsx | 1 - webapp/src/components/common/MatrixGrid/Matrix.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx index f360f12a13..6e0c1b91c0 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx @@ -16,7 +16,6 @@ import Matrix from "../../../../../common/MatrixGrid/Matrix"; import type { DataCompProps } from "../utils"; function DebugMatrix({ studyId, filename, filePath, canEdit }: DataCompProps) { - console.log("canEdit", canEdit); return ; } diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/MatrixGrid/Matrix.tsx index c10dd05f7b..101cb961b8 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/MatrixGrid/Matrix.tsx @@ -102,7 +102,7 @@ function Matrix({ return ; } - if (!data || data.length === 0) { + if (!data[0]?.length) { return ; } From a5365e7e06342dd9ad83c44d009e2eba6b1c7f88 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 23 Oct 2024 14:45:50 +0200 Subject: [PATCH 33/43] feat(ui): enable aggregates on thermal and renewables clusters --- .../explore/Modelization/Areas/Renewables/Matrix.tsx | 1 + .../Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx index c6bbac8c3e..70429f1883 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx @@ -25,6 +25,7 @@ function RenewablesMatrix({ areaId, clusterId }: Props) { ); } diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx index 60a15a02c6..a2a0924fa7 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx @@ -58,6 +58,7 @@ function ThermalMatrices({ study, areaId, clusterId }: Props) { { url: `input/thermal/series/${areaId}/${clusterId}/series`, titleKey: "availability", + aggregates: "stats" as const, // avg, min, max }, { url: `input/thermal/series/${areaId}/${clusterId}/fuelCost`, @@ -107,13 +108,14 @@ function ThermalMatrices({ study, areaId, clusterId }: Props) { {filteredMatrices.map( - ({ url, titleKey, columns }) => + ({ url, titleKey, columns, aggregates }) => value === titleKey && ( ), )} From 5a95b8c7556a2c92f21d2ebfdcc63b8b2ed37892 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 24 Oct 2024 10:52:55 +0200 Subject: [PATCH 34/43] feat(ui): add `useMatrixPortal`to manage matrices on split views feat(ui): update `useMatrixPortal` and improve test coverage --- .../common/MatrixGrid/index.test.tsx | 201 +++++++++++++++++- .../components/common/MatrixGrid/index.tsx | 70 +++--- .../src/components/common/MatrixGrid/types.ts | 5 + .../common/MatrixGrid/useGridCellContent.ts | 4 +- .../components/common/MatrixGrid/useMatrix.ts | 87 ++++---- .../MatrixGrid/useMatrixPortal.test.tsx | 156 ++++++++++++++ .../common/MatrixGrid/useMatrixPortal.ts | 76 +++++++ .../common/MatrixGrid/utils.test.ts | 70 +++--- .../src/components/common/MatrixGrid/utils.ts | 33 ++- 9 files changed, 579 insertions(+), 123 deletions(-) create mode 100644 webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx create mode 100644 webapp/src/components/common/MatrixGrid/useMatrixPortal.ts diff --git a/webapp/src/components/common/MatrixGrid/index.test.tsx b/webapp/src/components/common/MatrixGrid/index.test.tsx index fbbeaa4a2c..467f2c913c 100644 --- a/webapp/src/components/common/MatrixGrid/index.test.tsx +++ b/webapp/src/components/common/MatrixGrid/index.test.tsx @@ -18,6 +18,8 @@ import Box from "@mui/material/Box"; import { mockGetBoundingClientRect } from "../../../tests/mocks/mockGetBoundingClientRect"; import { type EnhancedGridColumn, Column } from "./types"; import { mockHTMLCanvasElement } from "../../../tests/mocks/mockHTMLCanvasElement"; +import SplitView from "../SplitView"; +import userEvent from "@testing-library/user-event"; beforeEach(() => { mockHTMLCanvasElement(); @@ -69,7 +71,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 0, }, { id: "col2", @@ -77,7 +78,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 1, }, { id: "col3", @@ -85,7 +85,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 2, }, ]; @@ -120,7 +119,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 0, }, { id: "col2", @@ -128,7 +126,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 1, }, { id: "col3", @@ -136,7 +133,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 2, }, ]; @@ -173,7 +169,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 0, }, { id: "col2", @@ -181,7 +176,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 1, }, { id: "col3", @@ -189,7 +183,6 @@ describe("MatrixGrid rendering", () => { width: 100, type: Column.Number, editable: true, - order: 2, }, ]; @@ -231,4 +224,194 @@ describe("MatrixGrid rendering", () => { throw new Error("Expected an HTMLElement but received a different node."); } }); + + describe("MatrixGrid portal management", () => { + const sampleData = [ + [1, 2], + [4, 5], + ]; + + const sampleColumns: EnhancedGridColumn[] = [ + { + id: "col1", + title: "Column 1", + width: 100, + type: Column.Number, + editable: true, + }, + { + id: "col2", + title: "Column 2", + width: 100, + type: Column.Number, + editable: true, + }, + ]; + + test("should create portal when MatrixGrid mounts", () => { + render( + , + ); + + const portal = document.getElementById("portal"); + expect(portal).toBeInTheDocument(); + expect(portal?.style.display).toBe("none"); + }); + + test("should show/hide portal on mouse enter/leave", async () => { + const user = userEvent.setup(); + const { container } = render( + , + ); + + const matrix = container.querySelector(".matrix-container"); + expect(matrix).toBeInTheDocument(); + + // Mouse enter + await user.hover(matrix!); + const portalAfterEnter = document.getElementById("portal"); + expect(portalAfterEnter?.style.display).toBe("block"); + + // Mouse leave + await user.unhover(matrix!); + const portalAfterLeave = document.getElementById("portal"); + expect(portalAfterLeave?.style.display).toBe("none"); + }); + + test("should handle portal in split view with multiple matrices", async () => { + const user = userEvent.setup(); + + render( + + + + + + + + + + , + ); + + const matrices = document.querySelectorAll(".matrix-container"); + expect(matrices.length).toBe(2); + + // Test first matrix + await user.hover(matrices[0]); + expect(document.getElementById("portal")?.style.display).toBe("block"); + + // Test second matrix while first is still hovered + await user.hover(matrices[1]); + expect(document.getElementById("portal")?.style.display).toBe("block"); + + // Leave second matrix + await user.unhover(matrices[1]); + const portalAfterSecondLeave = document.getElementById("portal"); + expect(portalAfterSecondLeave?.style.display).toBe("none"); + }); + + test("should maintain portal when switching between matrices", async () => { + const user = userEvent.setup(); + + render( + + + + + + + + + + , + ); + + const matrices = document.querySelectorAll(".matrix-container"); + + // Rapid switching between matrices + await user.hover(matrices[0]); + expect(document.getElementById("portal")?.style.display).toBe("block"); + + await user.hover(matrices[1]); + expect(document.getElementById("portal")?.style.display).toBe("block"); + + await user.hover(matrices[0]); + expect(document.getElementById("portal")?.style.display).toBe("block"); + + // Final cleanup + await user.unhover(matrices[0]); + expect(document.getElementById("portal")?.style.display).toBe("none"); + }); + + test("should handle unmounting matrices in split view", () => { + const { unmount } = render( + + + + + + + + + + , + ); + + const portal = document.getElementById("portal"); + expect(portal).toBeInTheDocument(); + + unmount(); + expect(document.getElementById("portal")).toBeInTheDocument(); + expect(document.getElementById("portal")?.style.display).toBe("none"); + }); + }); }); diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx index b68f608605..51ce67b3a0 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/MatrixGrid/index.tsx @@ -27,6 +27,7 @@ import { useMemo, useState } from "react"; import { EnhancedGridColumn, GridUpdate, MatrixAggregates } from "./types"; import { darkTheme, readOnlyDarkTheme } from "./utils"; import { useColumnMapping } from "./useColumnMapping"; +import { useMatrixPortal } from "./useMatrixPortal"; export interface MatrixGridProps { data: number[][]; @@ -65,6 +66,15 @@ function MatrixGrid({ const { gridToData } = useColumnMapping(columns); + // Due to a current limitation of Glide Data Grid, only one id="portal" is active on the DOM + // This is an issue on splited matrices, the second matrix does not have an id="portal" + // Causing the overlay editor to not behave correctly on click + // This hook manage portal creation and cleanup for matrices in split views + // TODO: add a prop to detect matrices in split views and enable this conditionnaly + // !Workaround: a proper solution should be replacing this in the future + const { containerRef, handleMouseEnter, handleMouseLeave } = + useMatrixPortal(); + const theme = useMemo(() => { if (isReadOnly) { return { @@ -159,32 +169,40 @@ function MatrixGrid({ return ( <> - -
+
+ +
); } diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index aa6044fca1..cda4e32a13 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -90,6 +90,11 @@ export interface CustomColumnOptions { width?: number; } +export interface FormatNumberOptions { + value?: number; + maxDecimals?: number; +} + export interface EnhancedGridColumn extends BaseGridColumn { id: string; width?: number; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts index 2257fa518d..a5b1843cf8 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts @@ -65,7 +65,7 @@ const cellContentGenerators: Record = { return { kind: GridCellKind.Number, data: value, - displayData: formatNumber(value), // Format thousands and decimal separator + displayData: formatNumber({ value, maxDecimals: 6 }), readonly: !column.editable, allowOverlay: true, decimalSeparator: ".", @@ -78,7 +78,7 @@ const cellContentGenerators: Record = { return { kind: GridCellKind.Number, data: value, - displayData: formatNumber(value ?? 0), // Format thousands and decimal separator + displayData: formatNumber({ value, maxDecimals: 3 }), readonly: !column.editable, allowOverlay: false, decimalSeparator: ".", diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts index 225b1146e0..29908d97a2 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts @@ -91,57 +91,48 @@ export function useMatrix( // 2. When there are unsaved changes in the matrix usePrompt(t("form.changeNotSaved"), currentState.pendingUpdates.length > 0); - const fetchMatrix = useCallback( - async (loadingState = true) => { - // !NOTE This is a temporary solution to ensure the matrix is up to date - // TODO: Remove this once the matrix API is updated to return the correct data - if (loadingState) { - setIsLoading(true); - } - - try { - const [matrix, index] = await Promise.all([ - fetchMatrixData - ? // If a custom fetch function is provided, use it - fetchMatrixData(studyId) - : getStudyData(studyId, url, 1), - getStudyMatrixIndex(studyId, url), - ]); - - setState({ - data: matrix.data, - aggregates: calculateMatrixAggregates(matrix.data, aggregateTypes), - pendingUpdates: [], - updateCount: 0, - }); - setColumnCount(matrix.columns.length); - setIndex(index); - setIsLoading(false); - - return { - matrix, - index, - }; - } catch (error) { - setError(new Error(t("data.error.matrix"))); - enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); - } finally { - setIsLoading(false); - } - }, - [ - aggregateTypes, - enqueueErrorSnackbar, - fetchMatrixData, - setState, - studyId, - url, - ], - ); + const fetchMatrix = async (loadingState = true) => { + // !NOTE This is a temporary solution to ensure the matrix is up to date + // TODO: Remove this once the matrix API is updated to return the correct data + if (loadingState) { + setIsLoading(true); + } + + try { + const [matrix, index] = await Promise.all([ + fetchMatrixData + ? // If a custom fetch function is provided, use it + fetchMatrixData(studyId) + : getStudyData(studyId, url, 1), + getStudyMatrixIndex(studyId, url), + ]); + + setState({ + data: matrix.data, + aggregates: calculateMatrixAggregates(matrix.data, aggregateTypes), + pendingUpdates: [], + updateCount: 0, + }); + setColumnCount(matrix.columns.length); + setIndex(index); + setIsLoading(false); + + return { + matrix, + index, + }; + } catch (error) { + setError(new Error(t("data.error.matrix"))); + enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); + } finally { + setIsLoading(false); + } + }; useEffect(() => { fetchMatrix(); - }, [fetchMatrix]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [studyId, url, aggregateTypes, fetchMatrixData]); const dateTime = useMemo(() => { return index ? generateDateTime(index) : []; diff --git a/webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx b/webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx new file mode 100644 index 0000000000..638214349a --- /dev/null +++ b/webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2024, 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 { describe, it, expect, beforeEach } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { useMatrixPortal } from "./useMatrixPortal"; + +// Test component without ref +function NonRefComponent() { + return
Test Container
; +} + +// Test component with ref +function TestComponent() { + const { containerRef, handleMouseEnter, handleMouseLeave } = + useMatrixPortal(); + return ( +
+ Test Container +
+ ); +} + +describe("useMatrixPortal", () => { + beforeEach(() => { + cleanup(); // Clean up after each test + const existingPortal = document.getElementById("portal"); + if (existingPortal) { + existingPortal.remove(); + } + }); + + it("should create hidden portal initially", () => { + render(); + const portal = document.getElementById("portal"); + expect(portal).toBeInTheDocument(); + expect(portal?.style.display).toBe("none"); + }); + + it("should show portal with correct styles when mouse enters", async () => { + const user = userEvent.setup(); + render(); + + const container = screen.getByTestId("ref-container"); + await user.hover(container); + + const portal = document.getElementById("portal"); + expect(portal).toBeInTheDocument(); + expect(portal?.style.display).toBe("block"); + expect(portal?.style.position).toBe("fixed"); + expect(portal?.style.left).toBe("0px"); + expect(portal?.style.top).toBe("0px"); + expect(portal?.style.zIndex).toBe("9999"); + }); + + it("should hide portal when mouse leaves", async () => { + const user = userEvent.setup(); + render(); + + const container = screen.getByTestId("ref-container"); + + // Show portal + await user.hover(container); + expect(document.getElementById("portal")?.style.display).toBe("block"); + + // Hide portal + await user.unhover(container); + expect(document.getElementById("portal")?.style.display).toBe("none"); + }); + + it("should keep portal hidden if containerRef is null", async () => { + const user = userEvent.setup(); + + // First render test component to create portal + render(); + const portal = document.getElementById("portal"); + expect(portal?.style.display).toBe("none"); + + cleanup(); // Clean up the test component + + // Then render component without ref + render(); + const container = screen.getByTestId("non-ref-container"); + await user.hover(container); + + // Portal should stay hidden + expect(portal?.style.display).toBe("none"); + }); + + it("should handle multiple mouse enter/leave cycles", async () => { + const user = userEvent.setup(); + render(); + + const container = screen.getByTestId("ref-container"); + const portal = document.getElementById("portal"); + + // First cycle + await user.hover(container); + expect(portal?.style.display).toBe("block"); + + await user.unhover(container); + expect(portal?.style.display).toBe("none"); + + // Second cycle + await user.hover(container); + expect(portal?.style.display).toBe("block"); + + await user.unhover(container); + expect(portal?.style.display).toBe("none"); + }); + + it("should handle rapid mouse events", async () => { + const user = userEvent.setup(); + render(); + + const container = screen.getByTestId("ref-container"); + const portal = document.getElementById("portal"); + + // Rapid sequence + await user.hover(container); + await user.unhover(container); + await user.hover(container); + + expect(portal?.style.display).toBe("block"); + }); + + it("should maintain portal existence across multiple component instances", () => { + // Render first instance + const { unmount } = render(); + expect(document.getElementById("portal")).toBeInTheDocument(); + + // Unmount first instance + unmount(); + + // Render second instance + render(); + expect(document.getElementById("portal")).toBeInTheDocument(); + }); +}); diff --git a/webapp/src/components/common/MatrixGrid/useMatrixPortal.ts b/webapp/src/components/common/MatrixGrid/useMatrixPortal.ts new file mode 100644 index 0000000000..c6174d39d2 --- /dev/null +++ b/webapp/src/components/common/MatrixGrid/useMatrixPortal.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024, 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 { useEffect, useRef, useState } from "react"; + +/** + * Custom hook to manage portal creation and cleanup for matrices in split views. + * This hook is specifically designed to work around a limitation where multiple + * Glide Data Grid instances compete for a single portal with id="portal". + * + * @returns Hook handlers and ref to be applied to the matrix container + * + * @example + * ```tsx + * function MatrixGrid() { + * const { containerRef, handleMouseEnter, handleMouseLeave } = useMatrixPortal(); + * + * return ( + *
+ * + *
+ * ); + * } + * ``` + */ +export function useMatrixPortal() { + const [isActive, setIsActive] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + let portal = document.getElementById("portal"); + + if (!portal) { + portal = document.createElement("div"); + portal.id = "portal"; + portal.style.position = "fixed"; + portal.style.left = "0"; + portal.style.top = "0"; + portal.style.zIndex = "9999"; + portal.style.display = "none"; + document.body.appendChild(portal); + } + + // Update visibility based on active state + if (containerRef.current && isActive) { + portal.style.display = "block"; + } else { + portal.style.display = "none"; + } + }, [isActive]); + + const handleMouseEnter = () => { + setIsActive(true); + }; + + const handleMouseLeave = () => { + setIsActive(false); + }; + + return { + containerRef, + handleMouseEnter, + handleMouseLeave, + }; +} diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index c2054c34fd..f451164ad6 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -310,22 +310,6 @@ describe("calculateMatrixAggregates", () => { }); }); -describe("formatNumber", () => { - test("formats numbers correctly", () => { - expect(formatNumber(1234567.89)).toBe("1 234 567.89"); - expect(formatNumber(1000000)).toBe("1 000 000"); - expect(formatNumber(1234.5678)).toBe("1 234.5678"); - expect(formatNumber(undefined)).toBe(""); - }); - - test("handles edge cases", () => { - expect(formatNumber(0)).toBe("0"); - expect(formatNumber(-1234567.89)).toBe("-1 234 567.89"); - expect(formatNumber(0.00001)).toBe("0.00001"); - expect(formatNumber(1e20)).toBe("100 000 000 000 000 000 000"); - }); -}); - describe("generateCustomColumns", () => { it("should generate custom columns with correct properties", () => { const titles = ["Custom 1", "Custom 2", "Custom 3"]; @@ -457,31 +441,53 @@ describe("calculateMatrixAggregates", () => { }); describe("formatNumber", () => { - it("should format integer numbers correctly", () => { - expect(formatNumber(1234567)).toBe("1 234 567"); - expect(formatNumber(1000000)).toBe("1 000 000"); - expect(formatNumber(1)).toBe("1"); + test("should format integer numbers correctly", () => { + expect(formatNumber({ value: 1234567 })).toBe("1 234 567"); + expect(formatNumber({ value: 1000000 })).toBe("1 000 000"); + expect(formatNumber({ value: 1 })).toBe("1"); }); - it("should format decimal numbers correctly", () => { - expect(formatNumber(1234.56)).toBe("1 234.56"); - expect(formatNumber(1000000.123)).toBe("1 000 000.123"); + test("should format decimal numbers correctly", () => { + expect(formatNumber({ value: 1234.56, maxDecimals: 2 })).toBe("1 234.56"); + expect(formatNumber({ value: 1000000.123, maxDecimals: 3 })).toBe( + "1 000 000.123", + ); }); - it("should handle negative numbers", () => { - expect(formatNumber(-1234567)).toBe("-1 234 567"); - expect(formatNumber(-1234.56)).toBe("-1 234.56"); + test("should format load factors correctly with 6 decimal places", () => { + expect(formatNumber({ value: 0.123456, maxDecimals: 6 })).toBe("0.123456"); + expect(formatNumber({ value: 0.999999, maxDecimals: 6 })).toBe("0.999999"); + expect(formatNumber({ value: 0.000001, maxDecimals: 6 })).toBe("0.000001"); }); - it("should handle zero", () => { - expect(formatNumber(0)).toBe("0"); + test("should format statistics correctly with 3 decimal places", () => { + expect(formatNumber({ value: 1.23456, maxDecimals: 3 })).toBe("1.235"); // rounding + expect(formatNumber({ value: 0.001234, maxDecimals: 3 })).toBe("0.001"); + expect(formatNumber({ value: 0.1234567, maxDecimals: 3 })).toBe("0.123"); // truncation }); - it("should handle undefined", () => { - expect(formatNumber(undefined)).toBe(""); + test("should handle negative numbers", () => { + expect(formatNumber({ value: -1234567 })).toBe("-1 234 567"); + expect(formatNumber({ value: -1234.56, maxDecimals: 2 })).toBe("-1 234.56"); }); - it("should handle large numbers", () => { - expect(formatNumber(1e15)).toBe("1 000 000 000 000 000"); + test("should handle zero", () => { + expect(formatNumber({ value: 0 })).toBe("0"); + }); + + test("should handle undefined", () => { + expect(formatNumber({ value: undefined, maxDecimals: 3 })).toBe(""); + }); + + test("should handle large numbers", () => { + expect(formatNumber({ value: 1e15 })).toBe("1 000 000 000 000 000"); + }); + + test("should handle edge cases", () => { + expect(formatNumber({ value: 0, maxDecimals: 2 })).toBe("0"); + expect(formatNumber({ value: -0.123456, maxDecimals: 6 })).toBe( + "-0.123456", + ); + expect(formatNumber({ value: 1e20 })).toBe("100 000 000 000 000 000 000"); }); }); diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/MatrixGrid/utils.ts index 37254f0211..9b45a91745 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/MatrixGrid/utils.ts @@ -26,6 +26,7 @@ import { DateTimeMetadataDTO, Aggregate, Column, + FormatNumberOptions, } from "./types"; import { type FirstWeekContainsDate, @@ -106,17 +107,38 @@ export const aggregatesTheme: Partial = { //////////////////////////////////////////////////////////////// /** - * Formats a number by adding spaces as thousand separators. + * Formats a number by adding thousand separators. * - * @param num - The number to format. + * This function is particularly useful for displaying load factors, + * which are numbers between 0 and 1. For load factors, a maximum of + * 6 decimal places should be displayed. For statistics, a maximum of + * 3 decimal places is recommended. + * + * @param options - The options for formatting the number. + * @param options.value - The number to format. + * @param options.maxDecimals - The maximum number of decimal places to keep. * @returns The formatted number as a string. */ -export function formatNumber(num: number | undefined): string { - if (num === undefined) { +export function formatNumber({ + value, + maxDecimals = 0, +}: FormatNumberOptions): string { + if (value === undefined) { return ""; } - const [integerPart, decimalPart] = num.toString().split("."); + // Determine if we need to apply maxDecimals + const shouldFormatDecimals = + value % 1 !== 0 && + maxDecimals > 0 && + value.toString().split(".")[1].length > maxDecimals; + + // Use toFixed only if we need to control decimals + const formattedValue = shouldFormatDecimals + ? value.toFixed(maxDecimals) + : value.toString(); + + const [integerPart, decimalPart] = formattedValue.split("."); const formattedInteger = integerPart .split("") @@ -128,7 +150,6 @@ export function formatNumber(num: number | undefined): string { return digit + acc; }, ""); - // Return formatted number, preserving decimal part if it exists return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger; } From 7889c669a856448bf72d4b2a985857efe6d138c8 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 24 Oct 2024 11:16:17 +0200 Subject: [PATCH 35/43] fix(ui): disable percent display on allocation and correlation matrices --- .../App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index c6438d1125..b7d00a68de 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -205,7 +205,6 @@ export const MATRICES: Matrices = { fetchFn: getAllocationMatrix, enableDateTimeColumn: false, readOnly: true, - showPercent: true, }, [HydroMatrix.Correlation]: { title: "Correlation", @@ -213,7 +212,6 @@ export const MATRICES: Matrices = { fetchFn: getCorrelationMatrix, enableDateTimeColumn: false, readOnly: true, - showPercent: true, }, }; From d0e4200d8186c71116e7740ee35b7543d6d24081 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 24 Oct 2024 16:54:57 +0200 Subject: [PATCH 36/43] test(ui): improve `Matrix` test suite organization and maintainability test(ui): improve MatrixActions test suite organization test(ui): improve and fix type issues in useColumnMapping tests test(ui): improve useGridCellContent test organization test(ui): fix async handling in useMatrix tests test(ui): improve utils test suite organisation and typing --- .../common/MatrixGrid/MatrixActions.test.tsx | 190 ++--- .../common/MatrixGrid/index.test.tsx | 469 ++++-------- .../src/components/common/MatrixGrid/types.ts | 9 + .../MatrixGrid/useColumnMapping.test.ts | 228 +++--- .../MatrixGrid/useGridCellContent.test.ts | 691 ++++++------------ .../common/MatrixGrid/useMatrix.test.tsx | 337 ++++----- .../common/MatrixGrid/utils.test.ts | 677 +++++++---------- 7 files changed, 977 insertions(+), 1624 deletions(-) diff --git a/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx b/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx index f7b3802fef..487959168d 100644 --- a/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx +++ b/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx @@ -13,8 +13,8 @@ */ import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import MatrixActions from "./MatrixActions"; vi.mock("../buttons/SplitButton", () => ({ @@ -54,111 +54,127 @@ describe("MatrixActions", () => { canRedo: true, }; + type RenderOptions = Partial; + + const renderMatrixActions = (props: RenderOptions = {}) => { + return render(); + }; + + const getButton = (label: string) => { + const element = screen.getByText(label); + const button = element.closest("button"); + + if (!button) { + throw new Error(`Button with label "${label}" not found`); + } + + return button; + }; + + const getActionButton = (label: string) => { + const element = screen.getByLabelText(label); + const button = element.querySelector("button"); + + if (!button) { + throw new Error(`Action button "${label}" not found`); + } + + return button; + }; + beforeEach(() => { vi.clearAllMocks(); }); - it("renders all buttons and controls", () => { - render(); + describe("rendering", () => { + test("renders all buttons and controls", () => { + renderMatrixActions(); - expect(screen.getByLabelText("global.undo")).toBeDefined(); - expect(screen.getByLabelText("global.redo")).toBeDefined(); - expect(screen.getByText("(0)")).toBeDefined(); - expect(screen.getByText("global.import")).toBeDefined(); - expect(screen.getByText("global.export")).toBeDefined(); + expect(screen.getByLabelText("global.undo")).toBeInTheDocument(); + expect(screen.getByLabelText("global.redo")).toBeInTheDocument(); + expect(screen.getByText("(0)")).toBeInTheDocument(); + expect(screen.getByText("global.import")).toBeInTheDocument(); + expect(screen.getByText("global.export")).toBeInTheDocument(); + }); }); - it("disables undo button when canUndo is false", () => { - render(); - expect( - screen.getByLabelText("global.undo").querySelector("button"), - ).toBeDisabled(); - }); + describe("undo/redo functionality", () => { + test("manages undo button state correctly", () => { + const { rerender } = renderMatrixActions(); + expect(getActionButton("global.undo")).not.toBeDisabled(); - it("disables redo button when canRedo is false", () => { - render(); - expect( - screen.getByLabelText("global.redo").querySelector("button"), - ).toBeDisabled(); - }); + rerender(); + expect(getActionButton("global.undo")).toBeDisabled(); + }); - it("calls undo function when undo button is clicked", () => { - render(); - const undoButton = screen - .getByLabelText("global.undo") - .querySelector("button"); - if (undoButton) { - fireEvent.click(undoButton); - expect(defaultProps.undo).toHaveBeenCalled(); - } else { - throw new Error("Undo button not found"); - } - }); + test("manages redo button state correctly", () => { + const { rerender } = renderMatrixActions(); + expect(getActionButton("global.redo")).not.toBeDisabled(); - it("calls redo function when redo button is clicked", () => { - render(); - const redoButton = screen - .getByLabelText("global.redo") - .querySelector("button"); - if (redoButton) { - fireEvent.click(redoButton); - expect(defaultProps.redo).toHaveBeenCalled(); - } else { - throw new Error("Redo button not found"); - } - }); + rerender(); + expect(getActionButton("global.redo")).toBeDisabled(); + }); - it("disables save button when pendingUpdatesCount is 0", () => { - render(); - expect(screen.getByText("(0)").closest("button")).toBeDisabled(); - }); + test("handles undo/redo button clicks", async () => { + const user = userEvent.setup(); + renderMatrixActions(); - it("enables save button when pendingUpdatesCount is greater than 0", () => { - render(); - expect(screen.getByText("(1)").closest("button")).not.toBeDisabled(); - }); + await user.click(getActionButton("global.undo")); + expect(defaultProps.undo).toHaveBeenCalledTimes(1); - it("calls onSave function when save button is clicked", () => { - render(); - const saveButton = screen.getByText("(1)").closest("button"); - if (saveButton) { - fireEvent.click(saveButton); - expect(defaultProps.onSave).toHaveBeenCalled(); - } else { - throw new Error("Save button not found"); - } + await user.click(getActionButton("global.redo")); + expect(defaultProps.redo).toHaveBeenCalledTimes(1); + }); }); - it("shows loading state on save button when isSubmitting is true", () => { - render( - , - ); - expect(screen.getByText("(1)").closest("button")).toBeDisabled(); + describe("save functionality", () => { + test("manages save button state based on pending updates", () => { + const { rerender } = renderMatrixActions(); + expect(getButton("(0)")).toBeDisabled(); + + rerender(); + expect(getButton("(1)")).not.toBeDisabled(); + }); + + test("handles save button click", async () => { + const user = userEvent.setup(); + renderMatrixActions({ pendingUpdatesCount: 1 }); + + await user.click(getButton("(1)")); + expect(defaultProps.onSave).toHaveBeenCalledTimes(1); + }); + + test("disables save button during submission", () => { + renderMatrixActions({ + isSubmitting: true, + pendingUpdatesCount: 1, + }); + expect(getButton("(1)")).toBeDisabled(); + }); }); - it("calls onImport function when import button is clicked", () => { - render(); - fireEvent.click(screen.getByText("global.import")); - expect(defaultProps.onImport).toHaveBeenCalled(); - }); + describe("import/export functionality", () => { + test("handles import button click", async () => { + const user = userEvent.setup(); + renderMatrixActions(); - it("disables import button when isSubmitting is true", () => { - render(); - expect(screen.getByText("global.import")).toBeDisabled(); - }); + await user.click(getButton("global.import")); + expect(defaultProps.onImport).toHaveBeenCalledTimes(1); + }); + + test("manages button states during submission", () => { + renderMatrixActions({ isSubmitting: true }); - it("passes correct props to DownloadMatrixButton", () => { - const { rerender } = render(); - expect(screen.getByText("global.export")).not.toBeDisabled(); + expect(getButton("global.import")).toBeDisabled(); + expect(getButton("global.export")).toBeDisabled(); + }); - rerender(); - expect(screen.getByText("global.export")).toBeDisabled(); + test("manages export button state based on disabled prop", () => { + const { rerender } = renderMatrixActions(); + expect(getButton("global.export")).not.toBeDisabled(); - rerender(); - expect(screen.getByText("global.export")).toBeDisabled(); + rerender(); + expect(getButton("global.export")).toBeDisabled(); + }); }); }); diff --git a/webapp/src/components/common/MatrixGrid/index.test.tsx b/webapp/src/components/common/MatrixGrid/index.test.tsx index 467f2c913c..81cf7f8ebd 100644 --- a/webapp/src/components/common/MatrixGrid/index.test.tsx +++ b/webapp/src/components/common/MatrixGrid/index.test.tsx @@ -13,27 +13,56 @@ */ import { render } from "@testing-library/react"; -import MatrixGrid, { MatrixGridProps } from "."; +import userEvent from "@testing-library/user-event"; import Box from "@mui/material/Box"; +import MatrixGrid from "."; +import SplitView from "../SplitView"; +import { type EnhancedGridColumn, Column, RenderMatrixOptions } from "./types"; import { mockGetBoundingClientRect } from "../../../tests/mocks/mockGetBoundingClientRect"; -import { type EnhancedGridColumn, Column } from "./types"; import { mockHTMLCanvasElement } from "../../../tests/mocks/mockHTMLCanvasElement"; -import SplitView from "../SplitView"; -import userEvent from "@testing-library/user-event"; -beforeEach(() => { +const setupMocks = () => { mockHTMLCanvasElement(); mockGetBoundingClientRect(); vi.clearAllMocks(); -}); - -function renderMatrixGrid( - width: string, - height: string, - data: MatrixGridProps["data"], - columns: EnhancedGridColumn[], - rows: number, -) { +}; + +const COLUMNS: EnhancedGridColumn[] = [ + { + id: "col1", + title: "Column 1", + width: 100, + type: Column.Number, + editable: true, + }, + { + id: "col2", + title: "Column 2", + width: 100, + type: Column.Number, + editable: true, + }, + { + id: "col3", + title: "Column 3", + width: 100, + type: Column.Number, + editable: true, + }, +]; + +const DATA = [ + [1, 2, 3], + [4, 5, 6], +]; + +const renderMatrixGrid = ({ + width = "450px", + height = "500px", + data = DATA, + columns = COLUMNS, + rows = 2, +}: RenderMatrixOptions = {}) => { return render( , ); -} +}; -function assertDimensions( +const assertDimensions = ( element: HTMLElement, expectedWidth: number, expectedHeight: number, -) { +) => { const rect = element.getBoundingClientRect(); expect(rect.width).toBe(expectedWidth); expect(rect.height).toBe(expectedHeight); -} - -describe("MatrixGrid rendering", () => { - test("MatrixGrid should be rendered within a 450x500px container and match these dimensions", () => { - const data = [ - [1, 2, 3], - [4, 5, 6], - ]; - - const columns = [ - { - id: "col1", - title: "Column 1", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col2", - title: "Column 2", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col3", - title: "Column 3", - width: 100, - type: Column.Number, - editable: true, - }, - ]; - - const rows = 2; - - // Render the MatrixGrid inside a parent container with specific dimensions - const { container } = renderMatrixGrid( - "450px", // Use inline style for exact measurement - "500px", - data, - columns, - rows, - ); - - const matrix = container.firstChild; - - if (matrix instanceof HTMLElement) { +}; + +const getMatrixElement = (container: HTMLElement) => { + const matrix = container.firstChild; + + if (!(matrix instanceof HTMLElement)) { + throw new Error("Expected an HTMLElement but received a different node."); + } + + return matrix; +}; + +describe("MatrixGrid", () => { + beforeEach(setupMocks); + + describe("rendering", () => { + test("should match container dimensions", () => { + const { container } = renderMatrixGrid(); + const matrix = getMatrixElement(container); + expect(matrix).toBeInTheDocument(); assertDimensions(matrix, 450, 500); - } else { - throw new Error("Expected an HTMLElement but received a different node."); - } - }); + }); + + test("should render with empty data", () => { + const { container } = renderMatrixGrid({ data: [], rows: 0 }); + const matrix = getMatrixElement(container); - test("MatrixGrid should render correctly with no data", () => { - const data: MatrixGridProps["data"] = []; - - const columns = [ - { - id: "col1", - title: "Column 1", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col2", - title: "Column 2", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col3", - title: "Column 3", - width: 100, - type: Column.Number, - editable: true, - }, - ]; - - const rows = 0; - - const { container } = renderMatrixGrid( - "450px", - "500px", - data, - columns, - rows, - ); - - const matrix = container.firstChild; - - if (matrix instanceof HTMLElement) { expect(matrix).toBeInTheDocument(); assertDimensions(matrix, 450, 500); - } else { - throw new Error("Expected an HTMLElement but received a different node."); - } - }); + }); - test("MatrixGrid should match the provided dimensions when resized", () => { - const data = [ - [1, 2, 3], - [4, 5, 6], - ]; - - const columns = [ - { - id: "col1", - title: "Column 1", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col2", - title: "Column 2", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col3", - title: "Column 3", - width: 100, - type: Column.Number, - editable: true, - }, - ]; - - const rows = 2; - - const { container, rerender } = renderMatrixGrid( - "450px", - "500px", - data, - columns, - rows, - ); - - let matrix = container.firstChild; - - if (matrix instanceof HTMLElement) { + test("should update dimensions when resized", () => { + const { container, rerender } = renderMatrixGrid(); + let matrix = getMatrixElement(container); assertDimensions(matrix, 450, 500); - } else { - throw new Error("Expected an HTMLElement but received a different node."); - } - - rerender( - - - , - ); - - matrix = container.firstChild; - - if (matrix instanceof HTMLElement) { + + rerender( + + + , + ); + + matrix = getMatrixElement(container); assertDimensions(matrix, 300, 400); - } else { - throw new Error("Expected an HTMLElement but received a different node."); - } + }); }); - describe("MatrixGrid portal management", () => { - const sampleData = [ - [1, 2], - [4, 5], - ]; - - const sampleColumns: EnhancedGridColumn[] = [ - { - id: "col1", - title: "Column 1", - width: 100, - type: Column.Number, - editable: true, - }, - { - id: "col2", - title: "Column 2", - width: 100, - type: Column.Number, - editable: true, - }, - ]; - - test("should create portal when MatrixGrid mounts", () => { - render( - , + describe("portal management", () => { + const renderSplitView = () => { + return render( + + + {[0, 1].map((index) => ( + + + + ))} + + , ); + }; + + const getPortal = () => document.getElementById("portal"); - const portal = document.getElementById("portal"); - expect(portal).toBeInTheDocument(); - expect(portal?.style.display).toBe("none"); + test("should manage portal visibility on mount", () => { + renderMatrixGrid(); + expect(getPortal()).toBeInTheDocument(); + expect(getPortal()?.style.display).toBe("none"); }); - test("should show/hide portal on mouse enter/leave", async () => { + test("should toggle portal visibility on mouse events", async () => { const user = userEvent.setup(); - const { container } = render( - , - ); - + const { container } = renderMatrixGrid(); const matrix = container.querySelector(".matrix-container"); expect(matrix).toBeInTheDocument(); - // Mouse enter await user.hover(matrix!); - const portalAfterEnter = document.getElementById("portal"); - expect(portalAfterEnter?.style.display).toBe("block"); + expect(getPortal()?.style.display).toBe("block"); - // Mouse leave await user.unhover(matrix!); - const portalAfterLeave = document.getElementById("portal"); - expect(portalAfterLeave?.style.display).toBe("none"); + expect(getPortal()?.style.display).toBe("none"); }); - test("should handle portal in split view with multiple matrices", async () => { + test("should handle portal in split view", async () => { const user = userEvent.setup(); - - render( - - - - - - - - - - , - ); - + renderSplitView(); const matrices = document.querySelectorAll(".matrix-container"); - expect(matrices.length).toBe(2); - // Test first matrix + // Test portal behavior with multiple matrices await user.hover(matrices[0]); - expect(document.getElementById("portal")?.style.display).toBe("block"); + expect(getPortal()?.style.display).toBe("block"); - // Test second matrix while first is still hovered await user.hover(matrices[1]); - expect(document.getElementById("portal")?.style.display).toBe("block"); + expect(getPortal()?.style.display).toBe("block"); - // Leave second matrix await user.unhover(matrices[1]); - const portalAfterSecondLeave = document.getElementById("portal"); - expect(portalAfterSecondLeave?.style.display).toBe("none"); + expect(getPortal()?.style.display).toBe("none"); }); - test("should maintain portal when switching between matrices", async () => { + test("should maintain portal state when switching between matrices", async () => { const user = userEvent.setup(); - - render( - - - - - - - - - - , - ); - + renderSplitView(); const matrices = document.querySelectorAll(".matrix-container"); - // Rapid switching between matrices - await user.hover(matrices[0]); - expect(document.getElementById("portal")?.style.display).toBe("block"); - - await user.hover(matrices[1]); - expect(document.getElementById("portal")?.style.display).toBe("block"); - - await user.hover(matrices[0]); - expect(document.getElementById("portal")?.style.display).toBe("block"); + for (const matrix of [matrices[0], matrices[1], matrices[0]]) { + await user.hover(matrix); + expect(getPortal()?.style.display).toBe("block"); + } - // Final cleanup await user.unhover(matrices[0]); - expect(document.getElementById("portal")?.style.display).toBe("none"); + expect(getPortal()?.style.display).toBe("none"); }); - test("should handle unmounting matrices in split view", () => { - const { unmount } = render( - - - - - - - - - - , - ); - - const portal = document.getElementById("portal"); - expect(portal).toBeInTheDocument(); + test("should handle unmounting correctly", () => { + const { unmount } = renderSplitView(); + expect(getPortal()).toBeInTheDocument(); unmount(); - expect(document.getElementById("portal")).toBeInTheDocument(); - expect(document.getElementById("portal")?.style.display).toBe("none"); + expect(getPortal()).toBeInTheDocument(); + expect(getPortal()?.style.display).toBe("none"); }); }); }); diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts index cda4e32a13..4e6d60d6e8 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/MatrixGrid/types.ts @@ -17,6 +17,7 @@ import { EditableGridCell, Item, } from "@glideapps/glide-data-grid"; +import { MatrixGridProps } from "."; //////////////////////////////////////////////////////////////// // Enums @@ -137,3 +138,11 @@ export interface MatrixUpdateDTO { coordinates: number[][]; // Array of [col, row] pairs operation: MatrixUpdate; } + +export interface RenderMatrixOptions { + width?: string; + height?: string; + data?: MatrixGridProps["data"]; + columns?: EnhancedGridColumn[]; + rows?: number; +} diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts b/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts index 2dc3ebd7f2..45a8913826 100644 --- a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts +++ b/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts @@ -13,132 +13,138 @@ */ import { renderHook } from "@testing-library/react"; -import { describe, test, expect } from "vitest"; import { useColumnMapping } from "./useColumnMapping"; import { EnhancedGridColumn, Column } from "./types"; +import type { ColumnType } from "./types"; +import { Item } from "@glideapps/glide-data-grid"; describe("useColumnMapping", () => { - const testColumns: EnhancedGridColumn[] = [ - { - id: "text", - title: "Text", - type: Column.Text, - width: 100, - editable: false, - }, - { - id: "date", - title: "Date", - type: Column.DateTime, - width: 100, - editable: false, - }, - { - id: "num1", - title: "Number 1", - type: Column.Number, - width: 100, - editable: true, - }, - { - id: "num2", - title: "Number 2", - type: Column.Number, - width: 100, - editable: true, - }, - { - id: "agg", - title: "Aggregate", - type: Column.Aggregate, - width: 100, - editable: false, - }, - ]; - - test("should create gridToData and dataToGrid functions", () => { - const { result } = renderHook(() => useColumnMapping(testColumns)); - expect(result.current.gridToData).toBeDefined(); - expect(result.current.dataToGrid).toBeDefined(); + // Test data factories + const createColumn = ( + id: string, + type: ColumnType, + editable = false, + ): EnhancedGridColumn => ({ + id, + title: id.charAt(0).toUpperCase() + id.slice(1), + type, + width: 100, + editable, }); - describe("gridToData", () => { - test("should return null for non-data columns", () => { - const { result } = renderHook(() => useColumnMapping(testColumns)); - expect(result.current.gridToData([0, 0])).toBeNull(); // Text column - expect(result.current.gridToData([1, 0])).toBeNull(); // DateTime column - expect(result.current.gridToData([4, 0])).toBeNull(); // Aggregate column + const createNumericColumn = (id: string) => + createColumn(id, Column.Number, true); + + const COLUMNS = { + mixed: [ + createColumn("text", Column.Text), + createColumn("date", Column.DateTime), + createNumericColumn("num1"), + createNumericColumn("num2"), + createColumn("agg", Column.Aggregate), + ], + nonData: [ + createColumn("text", Column.Text), + createColumn("date", Column.DateTime), + ], + dataOnly: [createNumericColumn("num1"), createNumericColumn("num2")], + }; + + // Helper function to create properly typed coordinates + const createCoordinate = (col: number, row: number): Item => + [col, row] as Item; + + // Helper function to render the hook + const renderColumnMapping = (columns: EnhancedGridColumn[]) => + renderHook(() => useColumnMapping(columns)); + + describe("hook initialization", () => { + test("should create mapping functions", () => { + const { result } = renderColumnMapping(COLUMNS.mixed); + + expect(result.current.gridToData).toBeInstanceOf(Function); + expect(result.current.dataToGrid).toBeInstanceOf(Function); }); - test("should map grid coordinates to data coordinates for data columns", () => { - const { result } = renderHook(() => useColumnMapping(testColumns)); - expect(result.current.gridToData([2, 0])).toEqual([0, 0]); // First Number column - expect(result.current.gridToData([3, 1])).toEqual([1, 1]); // Second Number column + test("should memoize the result", () => { + const { result, rerender } = renderHook( + (props) => useColumnMapping(props.columns), + { initialProps: { columns: COLUMNS.mixed } }, + ); + + const initialResult = result.current; + rerender({ columns: COLUMNS.mixed }); + expect(result.current).toBe(initialResult); }); }); - describe("dataToGrid", () => { - test("should map data coordinates to grid coordinates", () => { - const { result } = renderHook(() => useColumnMapping(testColumns)); - expect(result.current.dataToGrid([0, 0])).toEqual([2, 0]); // First data column - expect(result.current.dataToGrid([1, 1])).toEqual([3, 1]); // Second data column + describe("gridToData mapping", () => { + describe("with mixed columns", () => { + test("should return null for non-data columns", () => { + const { result } = renderColumnMapping(COLUMNS.mixed); + + const nonDataCoordinates: Item[] = [ + createCoordinate(0, 0), // Text column + createCoordinate(1, 0), // DateTime column + createCoordinate(4, 0), // Aggregate column + ]; + + nonDataCoordinates.forEach((coord) => { + expect(result.current.gridToData(coord)).toBeNull(); + }); + }); + + test("should map data columns correctly", () => { + const { result } = renderColumnMapping(COLUMNS.mixed); + + const mappings = [ + { grid: createCoordinate(2, 0), expected: createCoordinate(0, 0) }, // First Number column + { grid: createCoordinate(3, 1), expected: createCoordinate(1, 1) }, // Second Number column + ]; + + mappings.forEach(({ grid, expected }) => { + expect(result.current.gridToData(grid)).toEqual(expected); + }); + }); }); - }); - test("should handle columns with only non-data types", () => { - const nonDataColumns: EnhancedGridColumn[] = [ - { - id: "text", - title: "Text", - type: Column.Text, - width: 100, - editable: false, - }, - { - id: "date", - title: "Date", - type: Column.DateTime, - width: 100, - editable: false, - }, - ]; - const { result } = renderHook(() => useColumnMapping(nonDataColumns)); - expect(result.current.gridToData([0, 0])).toBeNull(); - expect(result.current.gridToData([1, 0])).toBeNull(); - expect(result.current.dataToGrid([0, 0])).toEqual([undefined, 0]); // No data columns, so this should return an invalid grid coordinate - }); + describe("with specific column configurations", () => { + test("should handle non-data columns only", () => { + const { result } = renderColumnMapping(COLUMNS.nonData); + const coord = createCoordinate(0, 0); + + expect(result.current.gridToData(coord)).toBeNull(); + expect(result.current.gridToData(createCoordinate(1, 0))).toBeNull(); + expect(result.current.dataToGrid(coord)).toEqual([undefined, 0]); + }); + + test("should handle data columns only", () => { + const { result } = renderColumnMapping(COLUMNS.dataOnly); - test("should handle columns with only data types", () => { - const dataOnlyColumns: EnhancedGridColumn[] = [ - { - id: "num1", - title: "Number 1", - type: Column.Number, - width: 100, - editable: true, - }, - { - id: "num2", - title: "Number 2", - type: Column.Number, - width: 100, - editable: true, - }, - ]; - const { result } = renderHook(() => useColumnMapping(dataOnlyColumns)); - expect(result.current.gridToData([0, 0])).toEqual([0, 0]); - expect(result.current.gridToData([1, 1])).toEqual([1, 1]); - expect(result.current.dataToGrid([0, 0])).toEqual([0, 0]); - expect(result.current.dataToGrid([1, 1])).toEqual([1, 1]); + const mappings = [ + { grid: createCoordinate(0, 0), data: createCoordinate(0, 0) }, + { grid: createCoordinate(1, 1), data: createCoordinate(1, 1) }, + ]; + + mappings.forEach(({ grid, data }) => { + expect(result.current.gridToData(grid)).toEqual(data); + expect(result.current.dataToGrid(data)).toEqual(grid); + }); + }); + }); }); - test("should memoize the result", () => { - const { result, rerender } = renderHook( - (props) => useColumnMapping(props.columns), - { initialProps: { columns: testColumns } }, - ); - const initialResult = result.current; - rerender({ columns: testColumns }); - expect(result.current).toBe(initialResult); + describe("dataToGrid mapping", () => { + test("should map data coordinates to grid coordinates", () => { + const { result } = renderColumnMapping(COLUMNS.mixed); + const mappings = [ + { data: createCoordinate(0, 0), expected: createCoordinate(2, 0) }, // First data column + { data: createCoordinate(1, 1), expected: createCoordinate(3, 1) }, // Second data column + ]; + + mappings.forEach(({ data, expected }) => { + expect(result.current.dataToGrid(data)).toEqual(expected); + }); + }); }); }); diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts index ad28cfed59..0a1c75d87c 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts @@ -14,533 +14,254 @@ import { renderHook } from "@testing-library/react"; import { useGridCellContent } from "./useGridCellContent"; -import { Column, MatrixAggregates, type EnhancedGridColumn } from "./types"; +import { + Column, + type ColumnType, + type MatrixAggregates, + type EnhancedGridColumn, +} from "./types"; import { useColumnMapping } from "./useColumnMapping"; - -function renderGridCellContent( - data: number[][], - columns: EnhancedGridColumn[], - dateTime?: string[], - aggregates?: MatrixAggregates, - rowHeaders?: string[], -) { - const { result: mappingResult } = renderHook(() => useColumnMapping(columns)); - const { gridToData } = mappingResult.current; - - const { result } = renderHook(() => - useGridCellContent( - data, - columns, - gridToData, - dateTime, - aggregates, - rowHeaders, - ), - ); - - return result.current; -} +import { type Item } from "@glideapps/glide-data-grid"; describe("useGridCellContent", () => { - describe("returns correct cell content for Aggregate columns", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "total", - title: "Total", - type: Column.Aggregate, - width: 100, - editable: false, - }, - ]; - - const data = [ - [10, 20, 30], - [15, 25, 35], - [5, 15, 25], - ]; - - const aggregates = { - min: [5, 15, 25], - max: [15, 25, 35], - avg: [10, 20, 30], - total: [30, 60, 90], - }; - - // Test each row for correct numeric cell content - test.each([ - [0, 30], // Total of first row - [1, 60], // Total of second row - [2, 90], // Total of third row - ])( - "ensures the correct numeric cell content is returned for aggregates at row %i", - (row, expectedData) => { - const getCellContent = renderGridCellContent( - data, - columns, - undefined, - aggregates, - ); - - const cell = getCellContent([0, row]); // Accessing the only column - - if ("data" in cell) { - expect(cell.kind).toBe("number"); - expect(cell.data).toBe(expectedData); - } else { - throw new Error(`Expected a number cell with data at row [${row}]`); - } - }, - ); + // Test data factories + const createColumn = ( + id: string, + type: ColumnType, + editable = false, + ): EnhancedGridColumn => ({ + id, + title: id.charAt(0).toUpperCase() + id.slice(1), + type, + width: type === Column.DateTime ? 150 : 50, + editable, }); - test("returns correct content for DateTime, Number, and Aggregate columns", () => { - const columns = [ - { - id: "date", - title: "Date", - type: Column.DateTime, - width: 150, - editable: false, - }, - { - id: "ts1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, - }, - { - id: "ts2", - title: "TS 2", - type: Column.Number, - width: 50, - editable: true, - }, - { - id: "total", - title: "Total", - type: Column.Aggregate, - width: 100, - editable: false, - }, - ]; - - const dateTime = ["2021-01-01T00:00:00Z", "2021-01-02T00:00:00Z"]; - - const data = [ - [100, 200], - [150, 250], - ]; - - const aggregates = { - min: [100, 200], - max: [150, 250], - avg: [125, 225], - total: [250, 450], - }; + const createCoordinate = (col: number, row: number): Item => + [col, row] as Item; + + interface RenderOptions { + data: number[][]; + columns: EnhancedGridColumn[]; + dateTime?: string[]; + aggregates?: MatrixAggregates; + rowHeaders?: string[]; + } + + const renderGridCellContent = ({ + data, + columns, + dateTime, + aggregates, + rowHeaders, + }: RenderOptions) => { + const { result: mappingResult } = renderHook(() => + useColumnMapping(columns), + ); - const getCellContent = renderGridCellContent( - data, - columns, - dateTime, - aggregates, + const { gridToData } = mappingResult.current; + + const { result } = renderHook(() => + useGridCellContent( + data, + columns, + gridToData, + dateTime, + aggregates, + rowHeaders, + ), ); - const numberCell = getCellContent([1, 0]); + return result.current; + }; - if (numberCell.kind === "number" && "data" in numberCell) { - expect(numberCell.data).toBe(100); + const assertNumberCell = ( + cell: ReturnType>, + expectedValue: number | undefined, + message: string, + ) => { + if (cell.kind === "number" && "data" in cell) { + expect(cell.data).toBe(expectedValue); } else { - throw new Error("Expected a Number cell with data"); + throw new Error(message); } - - const aggregateCell = getCellContent([3, 0]); - - if (aggregateCell.kind === "number" && "data" in aggregateCell) { - expect(aggregateCell.data).toBe(250); + }; + + const assertTextCell = ( + cell: ReturnType>, + expectedValue: string, + message: string, + ) => { + if (cell.kind === "text" && "displayData" in cell) { + expect(cell.displayData).toBe(expectedValue); } else { - throw new Error("Expected an Aggregate cell with data"); + throw new Error(message); } - }); -}); - -describe("useGridCellContent with mixed column types", () => { - test("handles non-data columns correctly and accesses data columns properly", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "rowHeader", - title: "Row", - type: Column.Text, - width: 100, - editable: false, - }, - { - id: "date", - title: "Date", - type: Column.DateTime, - width: 150, - editable: false, - }, - { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, + }; + + describe("Aggregate columns", () => { + const DATA = { + columns: [createColumn("total", Column.Aggregate)], + data: [ + [10, 20, 30], + [15, 25, 35], + [5, 15, 25], + ], + aggregates: { + min: [5, 15, 25], + max: [15, 25, 35], + avg: [10, 20, 30], + total: [30, 60, 90], }, - { - id: "data2", - title: "TS 2", - type: Column.Number, - width: 50, - editable: true, - }, - { - id: "total", - title: "Total", - type: Column.Aggregate, - width: 100, - editable: false, - }, - ]; - - const rowHeaders = ["Row 1", "Row 2"]; - const dateTime = ["2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"]; - const data = [ - [100, 200], - [150, 250], - ]; - const aggregates = { - min: [100, 200], - max: [150, 250], - avg: [125, 225], - total: [250, 450], }; - const getCellContent = renderGridCellContent( - data, - columns, - dateTime, - aggregates, - rowHeaders, - ); - - // Test row header (Text column) - const rowHeaderCell = getCellContent([0, 0]); - - if (rowHeaderCell.kind === "text" && "displayData" in rowHeaderCell) { - expect(rowHeaderCell.displayData).toBe("Row 1"); - } else { - throw new Error("Expected a text cell with data for row header"); - } - - // Test first data column (Number column) - const firstDataCell = getCellContent([2, 0]); - - if (firstDataCell.kind === "number" && "data" in firstDataCell) { - expect(firstDataCell.data).toBe(100); - } else { - throw new Error("Expected a number cell with data for first data column"); - } - - // Test second data column (Number column) - const secondDataCell = getCellContent([3, 0]); - - if (secondDataCell.kind === "number" && "data" in secondDataCell) { - expect(secondDataCell.data).toBe(200); - } else { - throw new Error( - "Expected a number cell with data for second data column", + test.each([ + [0, 30], + [1, 60], + [2, 90], + ])("returns correct aggregate for row %i", (row, expected) => { + const getCellContent = renderGridCellContent(DATA); + const cell = getCellContent(createCoordinate(0, row)); + assertNumberCell( + cell, + expected, + `Expected aggregate value ${expected} at row ${row}`, ); - } - - // Test aggregate column - const aggregateCell = getCellContent([4, 0]); - - if (aggregateCell.kind === "number" && "data" in aggregateCell) { - expect(aggregateCell.data).toBe(250); - } else { - throw new Error("Expected a number cell with data for aggregate column"); - } + }); }); - test("correctly handles data columns when non-data columns are removed", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, + describe("Mixed column types", () => { + const DATA = { + columns: [ + createColumn("rowHeader", Column.Text), + createColumn("date", Column.DateTime), + createColumn("data1", Column.Number, true), + createColumn("data2", Column.Number, true), + createColumn("total", Column.Aggregate), + ], + data: [ + [100, 200], + [150, 250], + ], + dateTime: ["2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"], + rowHeaders: ["Row 1", "Row 2"], + aggregates: { + min: [100, 200], + max: [150, 250], + avg: [125, 225], + total: [250, 450], }, - { - id: "data2", - title: "TS 2", - type: Column.Number, - width: 50, - editable: true, - }, - { - id: "data3", - title: "TS 3", - type: Column.Number, - width: 50, - editable: true, - }, - ]; - - const data = [ - [100, 200, 300], - [150, 250, 350], - ]; - - const getCellContent = renderGridCellContent(data, columns); - - // Test all data columns - for (let i = 0; i < 3; i++) { - const cell = getCellContent([i, 0]); - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(data[0][i]); - } else { - throw new Error(`Expected a number cell with data for column ${i}`); - } - } - }); -}); - -describe("useGridCellContent additional tests", () => { - test("handles empty data array correctly", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, - }, - ]; - const data: number[][] = []; - - const getCellContent = renderGridCellContent(data, columns); - - const cell = getCellContent([0, 0]); - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBeUndefined(); - } else { - throw new Error("Expected a number cell with undefined data"); - } - }); - - test("handles column access out of bounds", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, - }, - ]; - const data = [[100]]; - - const getCellContent = renderGridCellContent(data, columns); - - const cell = getCellContent([1, 0]); // Accessing column index 1 which doesn't exist - expect(cell.kind).toBe("text"); - if ("displayData" in cell) { - expect(cell.displayData).toBe("N/A"); - } else { - throw new Error("Expected a text cell with 'N/A' displayData"); - } - }); - - test("handles row access out of bounds", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, - }, - ]; - const data = [[100]]; + }; - const getCellContent = renderGridCellContent(data, columns); + test("handles all column types correctly", () => { + const getCellContent = renderGridCellContent(DATA); - const cell = getCellContent([0, 1]); // Accessing row index 1 which doesn't exist - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBeUndefined(); - } else { - throw new Error("Expected a number cell with undefined data"); - } - }); - - test("handles missing aggregates correctly", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "total", - title: "Total", - type: Column.Aggregate, - width: 100, - editable: false, - }, - ]; - const data = [[100]]; - // No aggregates provided + // Text column (Row header) + assertTextCell( + getCellContent(createCoordinate(0, 0)), + "Row 1", + "Expected row header text", + ); - const getCellContent = renderGridCellContent(data, columns); + // Number columns + assertNumberCell( + getCellContent(createCoordinate(2, 0)), + 100, + "Expected first data column value", + ); + assertNumberCell( + getCellContent(createCoordinate(3, 0)), + 200, + "Expected second data column value", + ); - const cell = getCellContent([0, 0]); - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBeUndefined(); - } else { - throw new Error( - "Expected a number cell with undefined data for missing aggregate", + // Aggregate column + assertNumberCell( + getCellContent(createCoordinate(4, 0)), + 250, + "Expected aggregate value", ); - } + }); }); - test("handles mixed editable and non-editable columns", () => { - const columns: EnhancedGridColumn[] = [ - { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, - }, - { - id: "data2", - title: "TS 2", - type: Column.Number, - width: 50, - editable: false, - }, - ]; - const data = [[100, 200]]; - - const getCellContent = renderGridCellContent(data, columns); - - const editableCell = getCellContent([0, 0]); - const nonEditableCell = getCellContent([1, 0]); - - if (editableCell.kind === "number" && nonEditableCell.kind === "number") { - expect(editableCell.readonly).toBe(false); - expect(nonEditableCell.readonly).toBe(true); - } else { - throw new Error("Expected number cells with correct readonly property"); - } + describe("Edge cases", () => { + test("handles empty data array", () => { + const options = { + columns: [createColumn("data1", Column.Number, true)], + data: [], + }; + + const getCellContent = renderGridCellContent(options); + const cell = getCellContent(createCoordinate(0, 0)); + assertNumberCell(cell, undefined, "Expected undefined for empty data"); + }); + + test("handles out of bounds access", () => { + const options = { + columns: [createColumn("data1", Column.Number, true)], + data: [[100]], + }; + + const getCellContent = renderGridCellContent(options); + + // Column out of bounds + const colCell = getCellContent(createCoordinate(1, 0)); + assertTextCell(colCell, "N/A", "Expected N/A for column out of bounds"); + + // Row out of bounds + const rowCell = getCellContent(createCoordinate(0, 1)); + assertNumberCell( + rowCell, + undefined, + "Expected undefined for row out of bounds", + ); + }); }); - test("formats number cells correctly", () => { - const columns: EnhancedGridColumn[] = [ + describe("Number formatting", () => { + const formatTestCases = [ { - id: "data1", - title: "TS 1", - type: Column.Number, - width: 50, - editable: true, + desc: "formats regular numbers", + value: 1234567.89, + expected: "1 234 567.89", }, - ]; - - const data = [[1234567.89]]; - - const getCellContent = renderGridCellContent(data, columns); - const cell = getCellContent([0, 0]); - - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(1234567.89); - expect(cell.displayData).toBe("1 234 567.89"); - } else { - throw new Error("Expected a number cell with formatted display data"); - } - }); - - test("handles very large and very small numbers correctly", () => { - const columns: EnhancedGridColumn[] = [ { - id: "large", - title: "Large", - type: Column.Number, - width: 50, - editable: true, + desc: "handles very large numbers", + value: 1e20, + expected: "100 000 000 000 000 000 000", }, { - id: "small", - title: "Small", - type: Column.Number, - width: 50, - editable: true, + desc: "handles very small numbers", + value: 0.00001, + expected: "0.00001", }, - ]; - - const data = [[1e20, 0.00001]]; - - const getCellContent = renderGridCellContent(data, columns); - - const largeCell = getCellContent([0, 0]); - if (largeCell.kind === "number" && "data" in largeCell) { - expect(largeCell.data).toBe(1e20); - expect(largeCell.displayData).toBe("100 000 000 000 000 000 000"); - } else { - throw new Error("Expected a number cell for large number"); - } - - const smallCell = getCellContent([1, 0]); - if (smallCell.kind === "number" && "data" in smallCell) { - expect(smallCell.data).toBe(0.00001); - expect(smallCell.displayData).toBe("0.00001"); - } else { - throw new Error("Expected a number cell for small number"); - } - }); - - test("handles negative numbers correctly", () => { - const columns: EnhancedGridColumn[] = [ { - id: "negative", - title: "Negative", - type: Column.Number, - width: 50, - editable: true, + desc: "handles negative numbers", + value: -1234567.89, + expected: "-1 234 567.89", }, - ]; - - const data = [[-1234567.89]]; - - const getCellContent = renderGridCellContent(data, columns); - const cell = getCellContent([0, 0]); - - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(-1234567.89); - expect(cell.displayData).toBe("-1 234 567.89"); - } else { - throw new Error("Expected a number cell with formatted negative number"); - } - }); - - test("handles zero correctly", () => { - const columns: EnhancedGridColumn[] = [ { - id: "zero", - title: "Zero", - type: Column.Number, - width: 50, - editable: true, + desc: "handles zero", + value: 0, + expected: "0", }, ]; - const data = [[0]]; + test.each(formatTestCases)("$desc", ({ value, expected }) => { + const options = { + columns: [createColumn("number", Column.Number, true)], + data: [[value]], + }; - const getCellContent = renderGridCellContent(data, columns); - const cell = getCellContent([0, 0]); + const getCellContent = renderGridCellContent(options); + const cell = getCellContent(createCoordinate(0, 0)); - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(0); - expect(cell.displayData).toBe("0"); - } else { - throw new Error("Expected a number cell for zero"); - } + if (cell.kind === "number" && "data" in cell) { + expect(cell.data).toBe(value); + expect(cell.displayData).toBe(expected); + } else { + throw new Error(`Expected formatted number cell for value ${value}`); + } + }); }); }); diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx index 4a6c8b4e68..09bdce7eff 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx +++ b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx @@ -27,48 +27,34 @@ import { import { GridUpdate, MatrixDataDTO } from "./types"; import { GridCellKind } from "@glideapps/glide-data-grid"; +// Mock external dependencies vi.mock("../../../services/api/matrix"); vi.mock("../../../services/api/study"); vi.mock("../../../services/api/studies/raw"); vi.mock("../../../hooks/usePrompt"); describe("useMatrix", () => { - const mockStudyId = "study123"; - const mockUrl = "https://studies/study123/matrix"; - - const mockMatrixData: MatrixDataDTO = { - data: [ - [1, 2], - [3, 4], - ], - columns: [0, 1], - index: [0, 1], + // Test constants and fixtures + const DATA = { + studyId: "study123", + url: "https://studies/study123/matrix", + matrixData: { + data: [ + [1, 2], + [3, 4], + ], + columns: [0, 1], + index: [0, 1], + } as MatrixDataDTO, + matrixIndex: { + start_date: "2023-01-01", + steps: 2, + first_week_size: 7, + level: StudyOutputDownloadLevelDTO.DAILY, + } as MatrixIndex, }; - const mockMatrixIndex: MatrixIndex = { - start_date: "2023-01-01", - steps: 2, - first_week_size: 7, - level: StudyOutputDownloadLevelDTO.DAILY, - }; - - // Helper function to set up the hook and wait for initial loading - const setupHook = async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); - - const { result } = renderHook(() => - useMatrix(mockStudyId, mockUrl, true, true, true), - ); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - return result; - }; - - // Helper function to create a grid update object + // Helper functions const createGridUpdate = ( row: number, col: number, @@ -83,221 +69,174 @@ describe("useMatrix", () => { }, }); - beforeEach(() => { - vi.clearAllMocks(); - }); + interface SetupOptions { + mockData?: MatrixDataDTO; + mockIndex?: MatrixIndex; + } - test("should fetch matrix data and index on mount", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); + const setupHook = async ({ + mockData = DATA.matrixData, + mockIndex = DATA.matrixIndex, + }: SetupOptions = {}) => { + vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockData); + vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockIndex); - const result = await setupHook(); + const hook = renderHook(() => + useMatrix(DATA.studyId, DATA.url, true, true, true), + ); await waitFor(() => { - expect(result.current.isLoading).toBe(false); + expect(hook.result.current.isLoading).toBe(false); }); - expect(result.current.data).toEqual(mockMatrixData.data); - expect(result.current.columns.length).toBeGreaterThan(0); - expect(result.current.dateTime.length).toBeGreaterThan(0); - }); - - test("should handle cell edit", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); - - const result = await setupHook(); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); + return hook; + }; + const performEdit = async ( + hook: Awaited>, + updates: GridUpdate | GridUpdate[], + ) => { act(() => { - result.current.handleCellEdit(createGridUpdate(0, 1, 5)); + if (Array.isArray(updates)) { + hook.result.current.handleMultipleCellsEdit(updates); + } else { + hook.result.current.handleCellEdit(updates); + } }); + }; - expect(result.current.data[1][0]).toBe(5); - expect(result.current.pendingUpdatesCount).toBe(1); + beforeEach(() => { + vi.clearAllMocks(); }); - test("should handle multiple cells edit", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); - - const result = await setupHook(); + describe("Initialization", () => { + test("should fetch and initialize matrix data", async () => { + const hook = await setupHook(); - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - act(() => { - result.current.handleMultipleCellsEdit([ - createGridUpdate(0, 1, 5), - createGridUpdate(1, 0, 6), - ]); + expect(hook.result.current.data).toEqual(DATA.matrixData.data); + expect(hook.result.current.columns.length).toBeGreaterThan(0); + expect(hook.result.current.dateTime.length).toBeGreaterThan(0); + expect(hook.result.current.isLoading).toBe(false); }); - - expect(result.current.data[1][0]).toBe(5); - expect(result.current.data[0][1]).toBe(6); - expect(result.current.pendingUpdatesCount).toBe(1); }); - test("should handle save updates", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); - vi.mocked(apiMatrix.updateMatrix).mockResolvedValue(undefined); + describe("Edit operations", () => { + test("should handle single cell edit", async () => { + const hook = await setupHook(); + const update = createGridUpdate(0, 1, 5); - const result = await setupHook(); + await performEdit(hook, update); - await waitFor(() => { - expect(result.current.isLoading).toBe(false); + expect(hook.result.current.data[1][0]).toBe(5); + expect(hook.result.current.pendingUpdatesCount).toBe(1); }); - act(() => { - result.current.handleCellEdit(createGridUpdate(0, 1, 5)); - }); + test("should handle multiple cell edits", async () => { + const hook = await setupHook(); + const updates = [createGridUpdate(0, 1, 5), createGridUpdate(1, 0, 6)]; - await act(async () => { - await result.current.handleSaveUpdates(); - }); - - const expectedEdit: MatrixEditDTO = { - coordinates: [[1, 0]], - operation: { - operation: Operator.EQ, - value: 5, - }, - }; - - expect(apiMatrix.updateMatrix).toHaveBeenCalledWith(mockStudyId, mockUrl, [ - expectedEdit, - ]); - expect(result.current.pendingUpdatesCount).toBe(0); - }); - - test("should handle file import", async () => { - const mockFile = new File([""], "test.csv", { type: "text/csv" }); - vi.mocked(rawStudy.importFile).mockResolvedValue(); - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex); + await performEdit(hook, updates); - const result = await setupHook(); - - await act(async () => { - await result.current.handleImport(mockFile); - }); - - expect(rawStudy.importFile).toHaveBeenCalledWith({ - file: mockFile, - studyId: mockStudyId, - path: mockUrl, + expect(hook.result.current.data[1][0]).toBe(5); + expect(hook.result.current.data[0][1]).toBe(6); + expect(hook.result.current.pendingUpdatesCount).toBe(1); }); - }); - describe("Undo and Redo functionality", () => { - test("should have correct initial undo/redo states", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue( - mockMatrixIndex, - ); + test("should save updates correctly", async () => { + vi.mocked(apiMatrix.updateMatrix).mockResolvedValue(undefined); + const hook = await setupHook(); - const result = await setupHook(); + await performEdit(hook, createGridUpdate(0, 1, 5)); - await waitFor(() => { - expect(result.current.isLoading).toBe(false); + await act(async () => { + await hook.result.current.handleSaveUpdates(); }); - expect(result.current.canUndo).toBe(false); - expect(result.current.canRedo).toBe(false); - }); + const expectedEdit: MatrixEditDTO = { + coordinates: [[1, 0]], + operation: { operation: Operator.EQ, value: 5 }, + }; - test("should update canUndo and canRedo states correctly after edits", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue( - mockMatrixIndex, + expect(apiMatrix.updateMatrix).toHaveBeenCalledWith( + DATA.studyId, + DATA.url, + [expectedEdit], ); + expect(hook.result.current.pendingUpdatesCount).toBe(0); + }); + }); - const result = await setupHook(); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - - act(() => { - result.current.handleCellEdit(createGridUpdate(0, 1, 5)); - }); + describe("File operations", () => { + test("should handle file import", async () => { + const mockFile = new File([""], "test.csv", { type: "text/csv" }); + vi.mocked(rawStudy.importFile).mockResolvedValue(); - expect(result.current.canUndo).toBe(true); - expect(result.current.canRedo).toBe(false); + const hook = await setupHook(); - act(() => { - result.current.undo(); + await act(async () => { + await hook.result.current.handleImport(mockFile); }); - expect(result.current.canUndo).toBe(false); - expect(result.current.canRedo).toBe(true); - - act(() => { - result.current.redo(); + expect(rawStudy.importFile).toHaveBeenCalledWith({ + file: mockFile, + studyId: DATA.studyId, + path: DATA.url, }); - - expect(result.current.canUndo).toBe(true); - expect(result.current.canRedo).toBe(false); }); + }); - test("should reset redo state after a new edit", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue( - mockMatrixIndex, - ); - - const result = await setupHook(); - - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); + describe("Undo/Redo functionality", () => { + test("should initialize with correct undo/redo states", async () => { + const hook = await setupHook(); - act(() => { - result.current.handleCellEdit(createGridUpdate(0, 1, 5)); - }); + expect(hook.result.current.canUndo).toBe(false); + expect(hook.result.current.canRedo).toBe(false); + }); - act(() => { - result.current.undo(); - }); + test("should update states after edit operations", async () => { + const hook = await setupHook(); + const update = createGridUpdate(0, 1, 5); - expect(result.current.canRedo).toBe(true); + // Initial edit + await performEdit(hook, update); + expect(hook.result.current.canUndo).toBe(true); + expect(hook.result.current.canRedo).toBe(false); - act(() => { - result.current.handleCellEdit(createGridUpdate(1, 0, 6)); - }); + // Undo + act(() => hook.result.current.undo()); + expect(hook.result.current.canUndo).toBe(false); + expect(hook.result.current.canRedo).toBe(true); - expect(result.current.canUndo).toBe(true); - expect(result.current.canRedo).toBe(false); + // Redo + act(() => hook.result.current.redo()); + expect(hook.result.current.canUndo).toBe(true); + expect(hook.result.current.canRedo).toBe(false); }); - test("should handle undo to initial state", async () => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue( - mockMatrixIndex, - ); + test("should clear redo history after new edit", async () => { + const hook = await setupHook(); - const result = await setupHook(); + // Create edit history + await performEdit(hook, createGridUpdate(0, 1, 5)); + act(() => hook.result.current.undo()); + expect(hook.result.current.canRedo).toBe(true); - await waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); + // New edit should clear redo history + await performEdit(hook, createGridUpdate(1, 0, 6)); + expect(hook.result.current.canUndo).toBe(true); + expect(hook.result.current.canRedo).toBe(false); + }); - act(() => { - result.current.handleCellEdit(createGridUpdate(0, 1, 5)); - }); + test("should restore initial state after full undo", async () => { + const hook = await setupHook(); + const initialData = [...DATA.matrixData.data]; - act(() => { - result.current.undo(); - }); + await performEdit(hook, createGridUpdate(0, 1, 5)); + act(() => hook.result.current.undo()); - expect(result.current.data).toEqual(mockMatrixData.data); - expect(result.current.canUndo).toBe(false); - expect(result.current.canRedo).toBe(true); + expect(hook.result.current.data).toEqual(initialData); + expect(hook.result.current.canUndo).toBe(false); + expect(hook.result.current.canRedo).toBe(true); }); }); }); diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts index f451164ad6..f39fb513e0 100644 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ b/webapp/src/components/common/MatrixGrid/utils.test.ts @@ -12,27 +12,17 @@ * This file is part of the Antares project. */ -import { - MatrixIndex, - StudyOutputDownloadLevelDTO, -} from "../../../common/types"; -import { - Aggregate, - AggregateType, - Column, - DateTimeMetadataDTO, - TimeFrequency, -} from "./types"; +import { Aggregate, Column, TimeFrequency } from "./types"; import { calculateMatrixAggregates, formatNumber, generateCustomColumns, - generateDataColumns, generateDateTime, generateTimeSeriesColumns, getAggregateTypes, } from "./utils"; +// Mock date-fns for consistent date formatting vi.mock("date-fns", async () => { const actual = (await vi.importActual( "date-fns", @@ -49,445 +39,312 @@ vi.mock("date-fns", async () => { }; }); -describe("generateDateTime", () => { - beforeAll(() => { - // Set a fixed date for consistent testing - vi.useFakeTimers(); - vi.setSystemTime(new Date("2023-01-01 00:00:00")); - }); - - it("generates correct annual format", () => { - const config: DateTimeMetadataDTO = { +describe("Matrix Utils", () => { + // Test data and helpers + const TEST_DATA = { + dateConfig: { start_date: "2023-01-01 00:00:00", steps: 3, first_week_size: 7, - level: TimeFrequency.Annual, - }; - const result = generateDateTime(config); - expect(result).toEqual([ - "global.time.annual", - "global.time.annual", - "global.time.annual", - ]); - }); - - it("generates correct monthly format", () => { - const config: DateTimeMetadataDTO = { - start_date: "2023-01-01 00:00:00", - steps: 3, - first_week_size: 7, - level: TimeFrequency.Monthly, - }; - const result = generateDateTime(config); - expect(result).toEqual(["Jan", "Feb", "Mar"]); - }); - - it("generates correct weekly format with first_week_size 1", () => { - const config: DateTimeMetadataDTO = { - start_date: "2023-01-01 00:00:00", - steps: 3, - first_week_size: 1, - level: TimeFrequency.Weekly, - }; - const result = generateDateTime(config); - expect(result).toEqual(["W 01", "W 02", "W 03"]); - }); - - it("generates correct daily format", () => { - const config: DateTimeMetadataDTO = { - start_date: "2023-01-01 00:00:00", - steps: 3, - first_week_size: 7, - level: TimeFrequency.Daily, - }; - const result = generateDateTime(config); - expect(result).toEqual(["Sun 1 Jan", "Mon 2 Jan", "Tue 3 Jan"]); - }); - - it("generates correct hourly format", () => { - const config: DateTimeMetadataDTO = { - start_date: "2039-07-01 00:00:00", - steps: 3, - first_week_size: 7, - level: TimeFrequency.Hourly, - }; - const result = generateDateTime(config); - expect(result).toEqual([ - "Fri 1 Jul 00:00", - "Fri 1 Jul 01:00", - "Fri 1 Jul 02:00", - ]); - }); - - test("generates correct number of dates", () => { - const metadata: MatrixIndex = { - start_date: "2023-01-01 00:00:00", - steps: 5, - first_week_size: 7, - level: StudyOutputDownloadLevelDTO.DAILY, - }; - const result = generateDateTime(metadata); - expect(result).toHaveLength(5); - }); -}); + }, + matrix: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + }; -describe("generateTimeSeriesColumns", () => { - test("generates correct number of columns", () => { - const result = generateTimeSeriesColumns({ count: 5 }); - expect(result).toHaveLength(5); + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2023-01-01 00:00:00")); }); - test("generates columns with default options", () => { - const result = generateTimeSeriesColumns({ count: 3 }); - expect(result).toEqual([ + describe("DateTime Generation", () => { + const dateTimeTestCases = [ { - id: "data1", - title: "TS 1", - type: Column.Number, - style: "normal", - editable: true, + name: "annual format", + config: { ...TEST_DATA.dateConfig, level: TimeFrequency.Annual }, + expected: [ + "global.time.annual", + "global.time.annual", + "global.time.annual", + ], }, { - id: "data2", - title: "TS 2", - type: Column.Number, - style: "normal", - editable: true, + name: "monthly format", + config: { ...TEST_DATA.dateConfig, level: TimeFrequency.Monthly }, + expected: ["Jan", "Feb", "Mar"], }, { - id: "data3", - title: "TS 3", - type: Column.Number, - style: "normal", - editable: true, + name: "weekly format", + config: { + ...TEST_DATA.dateConfig, + level: TimeFrequency.Weekly, + first_week_size: 1, + }, + expected: ["W 01", "W 02", "W 03"], }, - ]); - }); - - test("generates columns with custom options", () => { - const result = generateTimeSeriesColumns({ - count: 2, - startIndex: 10, - prefix: "Data", - editable: false, - }); - expect(result).toEqual([ { - id: "data10", - title: "Data 10", - type: Column.Number, - style: "normal", - editable: false, + name: "daily format", + config: { ...TEST_DATA.dateConfig, level: TimeFrequency.Daily }, + expected: ["Sun 1 Jan", "Mon 2 Jan", "Tue 3 Jan"], }, { - id: "data11", - title: "Data 11", - type: Column.Number, - style: "normal", - editable: false, + name: "hourly format", + config: { + start_date: "2039-07-01 00:00:00", + steps: 3, + first_week_size: 7, + level: TimeFrequency.Hourly, + }, + expected: ["Fri 1 Jul 00:00", "Fri 1 Jul 01:00", "Fri 1 Jul 02:00"], }, - ]); - }); - - test("handles zero count", () => { - const result = generateTimeSeriesColumns({ count: 0 }); - expect(result).toEqual([]); - }); - - test("handles large count", () => { - const result = generateTimeSeriesColumns({ count: 1000 }); - expect(result).toHaveLength(1000); - expect(result[999].id).toBe("data1000"); - expect(result[999].title).toBe("TS 1000"); - }); - - test("maintains consistent type and style", () => { - const result = generateTimeSeriesColumns({ count: 1000 }); - result.forEach((column) => { - expect(column.type).toBe(Column.Number); - expect(column.style).toBe("normal"); - }); - }); -}); - -describe("calculateMatrixAggregates", () => { - it("should calculate correct aggregates for a simple matrix", () => { - const matrix = [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], ]; - const result = calculateMatrixAggregates(matrix, [ - "min", - "max", - "avg", - "total", - ]); - expect(result.min).toEqual([1, 4, 7]); - expect(result.max).toEqual([3, 6, 9]); - expect(result.avg).toEqual([2, 5, 8]); - expect(result.total).toEqual([6, 15, 24]); + test.each(dateTimeTestCases)( + "generates correct $name", + ({ config, expected }) => { + const result = generateDateTime(config); + expect(result).toEqual(expected); + }, + ); }); - it("should handle decimal numbers correctly by rounding", () => { - const matrix = [ - [1.1, 2.2, 3.3], - [4.4, 5.5, 6.6], + describe("Time Series Column Generation", () => { + const columnTestCases = [ + { + name: "default options", + input: { count: 3 }, + expectedLength: 3, + validate: (result: ReturnType) => { + expect(result[0]).toEqual({ + id: "data1", + title: "TS 1", + type: Column.Number, + style: "normal", + editable: true, + }); + }, + }, + { + name: "custom options", + input: { count: 2, startIndex: 10, prefix: "Data", editable: false }, + expectedLength: 2, + validate: (result: ReturnType) => { + expect(result[0]).toEqual({ + id: "data10", + title: "Data 10", + type: Column.Number, + style: "normal", + editable: false, + }); + }, + }, + { + name: "zero count", + input: { count: 0 }, + expectedLength: 0, + validate: (result: ReturnType) => { + expect(result).toEqual([]); + }, + }, + { + name: "large count", + input: { count: 1000 }, + expectedLength: 1000, + validate: (result: ReturnType) => { + expect(result[999].id).toBe("data1000"); + expect(result[999].title).toBe("TS 1000"); + }, + }, ]; - const result = calculateMatrixAggregates(matrix, [ - "min", - "max", - "avg", - "total", - ]); - expect(result.min).toEqual([1.1, 4.4]); - expect(result.max).toEqual([3.3, 6.6]); - expect(result.avg).toEqual([2, 6]); - expect(result.total).toEqual([7, 17]); + test.each(columnTestCases)( + "handles $name correctly", + ({ input, expectedLength, validate }) => { + const result = generateTimeSeriesColumns(input); + expect(result).toHaveLength(expectedLength); + validate(result); + }, + ); }); - it("should handle negative numbers", () => { - const matrix = [ - [-1, -2, -3], - [-4, 0, 4], + describe("Matrix Aggregates Calculation", () => { + const aggregateTestCases = [ + { + name: "simple matrix with all aggregates", + matrix: TEST_DATA.matrix, + aggregates: "all" as const, + expected: { + min: [1, 4, 7], + max: [3, 6, 9], + avg: [2, 5, 8], + total: [6, 15, 24], + }, + }, + { + name: "decimal numbers", + matrix: [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 6.6], + ], + aggregates: "all" as const, + expected: { + min: [1.1, 4.4], + max: [3.3, 6.6], + avg: [2, 6], + total: [7, 17], + }, + }, + { + name: "negative numbers", + matrix: [ + [-1, -2, -3], + [-4, 0, 4], + ], + aggregates: "all" as const, + expected: { + min: [-3, -4], + max: [-1, 4], + avg: [-2, 0], + total: [-6, 0], + }, + }, ]; - const result = calculateMatrixAggregates(matrix, [ - "min", - "max", - "avg", - "total", - ]); - - expect(result.min).toEqual([-3, -4]); - expect(result.max).toEqual([-1, 4]); - expect(result.avg).toEqual([-2, 0]); - expect(result.total).toEqual([-6, 0]); - }); - it("should handle single-element rows", () => { - const matrix = [[1], [2], [3]]; - const result = calculateMatrixAggregates(matrix, [ - "min", - "max", - "avg", - "total", - ]); - - expect(result.min).toEqual([1, 2, 3]); - expect(result.max).toEqual([1, 2, 3]); - expect(result.avg).toEqual([1, 2, 3]); - expect(result.total).toEqual([1, 2, 3]); + test.each(aggregateTestCases)( + "calculates $name correctly", + ({ matrix, aggregates, expected }) => { + const aggregatesTypes = getAggregateTypes(aggregates); + const result = calculateMatrixAggregates(matrix, aggregatesTypes); + expect(result).toEqual(expected); + }, + ); }); - it("should handle large numbers", () => { - const matrix = [ - [1000000, 2000000, 3000000], - [4000000, 5000000, 6000000], - ]; - const result = calculateMatrixAggregates(matrix, [ - "min", - "max", - "avg", - "total", - ]); - - expect(result.min).toEqual([1000000, 4000000]); - expect(result.max).toEqual([3000000, 6000000]); - expect(result.avg).toEqual([2000000, 5000000]); - expect(result.total).toEqual([6000000, 15000000]); - }); + describe("Number Formatting", () => { + interface FormatTestCase { + description: string; + value: number | undefined; + maxDecimals?: number; + expected: string; + } - it("should round average correctly", () => { - const matrix = [ - [1, 2, 4], - [10, 20, 39], + const formatTestCases: Array<{ name: string; cases: FormatTestCase[] }> = [ + { + name: "integer numbers", + cases: [ + { + description: "formats large number", + value: 1234567, + expected: "1 234 567", + }, + { + description: "formats million", + value: 1000000, + expected: "1 000 000", + }, + { description: "formats single digit", value: 1, expected: "1" }, + ], + }, + { + name: "decimal numbers", + cases: [ + { + description: "formats with 2 decimals", + value: 1234.56, + maxDecimals: 2, + expected: "1 234.56", + }, + { + description: "formats with 3 decimals", + value: 1000000.123, + maxDecimals: 3, + expected: "1 000 000.123", + }, + ], + }, + { + name: "special cases", + cases: [ + { + description: "handles undefined", + value: undefined, + maxDecimals: 3, + expected: "", + }, + { description: "handles zero", value: 0, expected: "0" }, + { + description: "handles negative", + value: -1234567, + expected: "-1 234 567", + }, + { + description: "handles very large number", + value: 1e20, + expected: "100 000 000 000 000 000 000", + }, + ], + }, ]; - const result = calculateMatrixAggregates(matrix, ["avg"]); - - expect(result.avg).toEqual([2, 23]); - }); -}); -describe("generateCustomColumns", () => { - it("should generate custom columns with correct properties", () => { - const titles = ["Custom 1", "Custom 2", "Custom 3"]; - const width = 100; - const result = generateCustomColumns({ titles, width }); - - expect(result).toHaveLength(3); - result.forEach((column, index) => { - expect(column).toEqual({ - id: `custom${index + 1}`, - title: titles[index], - type: Column.Number, - style: "normal", - width: 100, - editable: true, + describe.each(formatTestCases)("$name", ({ cases }) => { + test.each(cases)("$description", ({ value, maxDecimals, expected }) => { + expect(formatNumber({ value, maxDecimals })).toBe(expected); }); }); }); - it("should handle empty titles array", () => { - const result = generateCustomColumns({ titles: [], width: 100 }); - expect(result).toEqual([]); - }); -}); + describe("Custom Column Generation", () => { + test("generates columns with correct properties", () => { + const titles = ["Custom 1", "Custom 2", "Custom 3"]; + const width = 100; -describe("generateDataColumns", () => { - it("should generate custom columns when provided", () => { - const customColumns = ["Custom 1", "Custom 2"]; - const result = generateDataColumns(false, 5, customColumns, 100); + const result = generateCustomColumns({ titles, width }); + expect(result).toHaveLength(3); - expect(result).toHaveLength(2); - expect(result[0].title).toBe("Custom 1"); - expect(result[1].title).toBe("Custom 2"); - }); - - it("should generate time series columns when enabled", () => { - const result = generateDataColumns(true, 3); - - expect(result).toHaveLength(3); - expect(result[0].title).toBe("TS 1"); - expect(result[1].title).toBe("TS 2"); - expect(result[2].title).toBe("TS 3"); - }); - - it("should return empty array when custom columns not provided and time series disabled", () => { - const result = generateDataColumns(false, 5); - expect(result).toEqual([]); - }); -}); - -describe("getAggregateTypes", () => { - it('should return correct aggregate types for "stats" config', () => { - const result = getAggregateTypes("stats"); - expect(result).toEqual([Aggregate.Avg, Aggregate.Min, Aggregate.Max]); - }); - - it('should return correct aggregate types for "all" config', () => { - const result = getAggregateTypes("all"); - expect(result).toEqual([ - Aggregate.Min, - Aggregate.Max, - Aggregate.Avg, - Aggregate.Total, - ]); - }); - - it("should return provided aggregate types for array config", () => { - const config: AggregateType[] = [Aggregate.Min, Aggregate.Max]; - const result = getAggregateTypes(config); - expect(result).toEqual(config); - }); - - it("should return empty array for invalid config", () => { - // @ts-expect-error : we are testing an invalid value - const result = getAggregateTypes("invalid"); - expect(result).toEqual([]); - }); -}); - -describe("calculateMatrixAggregates", () => { - const matrix = [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - ]; - - it("should calculate min aggregate correctly", () => { - const result = calculateMatrixAggregates(matrix, [Aggregate.Min]); - expect(result).toEqual({ min: [1, 4, 7] }); - }); - - it("should calculate max aggregate correctly", () => { - const result = calculateMatrixAggregates(matrix, [Aggregate.Max]); - expect(result).toEqual({ max: [3, 6, 9] }); - }); - - it("should calculate avg aggregate correctly", () => { - const result = calculateMatrixAggregates(matrix, [Aggregate.Avg]); - expect(result).toEqual({ avg: [2, 5, 8] }); - }); - - it("should calculate total aggregate correctly", () => { - const result = calculateMatrixAggregates(matrix, [Aggregate.Total]); - expect(result).toEqual({ total: [6, 15, 24] }); - }); - - it("should calculate multiple aggregates correctly", () => { - const result = calculateMatrixAggregates(matrix, [ - Aggregate.Min, - Aggregate.Max, - Aggregate.Avg, - Aggregate.Total, - ]); - expect(result).toEqual({ - min: [1, 4, 7], - max: [3, 6, 9], - avg: [2, 5, 8], - total: [6, 15, 24], + result.forEach((column, index) => { + expect(column).toEqual({ + id: `custom${index + 1}`, + title: titles[index], + type: Column.Number, + style: "normal", + width, + editable: true, + }); + }); }); }); - it("should handle empty matrix", () => { - const result = calculateMatrixAggregates( - [], - [Aggregate.Min, Aggregate.Max, Aggregate.Avg, Aggregate.Total], - ); - expect(result).toEqual({}); - }); -}); - -describe("formatNumber", () => { - test("should format integer numbers correctly", () => { - expect(formatNumber({ value: 1234567 })).toBe("1 234 567"); - expect(formatNumber({ value: 1000000 })).toBe("1 000 000"); - expect(formatNumber({ value: 1 })).toBe("1"); - }); - - test("should format decimal numbers correctly", () => { - expect(formatNumber({ value: 1234.56, maxDecimals: 2 })).toBe("1 234.56"); - expect(formatNumber({ value: 1000000.123, maxDecimals: 3 })).toBe( - "1 000 000.123", - ); - }); - - test("should format load factors correctly with 6 decimal places", () => { - expect(formatNumber({ value: 0.123456, maxDecimals: 6 })).toBe("0.123456"); - expect(formatNumber({ value: 0.999999, maxDecimals: 6 })).toBe("0.999999"); - expect(formatNumber({ value: 0.000001, maxDecimals: 6 })).toBe("0.000001"); - }); - - test("should format statistics correctly with 3 decimal places", () => { - expect(formatNumber({ value: 1.23456, maxDecimals: 3 })).toBe("1.235"); // rounding - expect(formatNumber({ value: 0.001234, maxDecimals: 3 })).toBe("0.001"); - expect(formatNumber({ value: 0.1234567, maxDecimals: 3 })).toBe("0.123"); // truncation - }); - - test("should handle negative numbers", () => { - expect(formatNumber({ value: -1234567 })).toBe("-1 234 567"); - expect(formatNumber({ value: -1234.56, maxDecimals: 2 })).toBe("-1 234.56"); - }); - - test("should handle zero", () => { - expect(formatNumber({ value: 0 })).toBe("0"); - }); - - test("should handle undefined", () => { - expect(formatNumber({ value: undefined, maxDecimals: 3 })).toBe(""); - }); - - test("should handle large numbers", () => { - expect(formatNumber({ value: 1e15 })).toBe("1 000 000 000 000 000"); - }); + describe("Aggregate Type Configuration", () => { + const aggregateConfigCases = [ + { + name: "stats configuration", + aggregates: "stats" as const, + expected: [Aggregate.Avg, Aggregate.Min, Aggregate.Max], + }, + { + name: "all configuration", + aggregates: "all" as const, + expected: [ + Aggregate.Min, + Aggregate.Max, + Aggregate.Avg, + Aggregate.Total, + ], + }, + { + name: "custom configuration", + aggregates: [Aggregate.Min, Aggregate.Max], + expected: [Aggregate.Min, Aggregate.Max], + }, + ]; - test("should handle edge cases", () => { - expect(formatNumber({ value: 0, maxDecimals: 2 })).toBe("0"); - expect(formatNumber({ value: -0.123456, maxDecimals: 6 })).toBe( - "-0.123456", + test.each(aggregateConfigCases)( + "handles $name correctly", + ({ aggregates, expected }) => { + expect(getAggregateTypes(aggregates)).toEqual(expected); + }, ); - expect(formatNumber({ value: 1e20 })).toBe("100 000 000 000 000 000 000"); }); }); From 5b79b237c5d2b705f4f0efcc6bcdb1072059cfed Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 24 Oct 2024 16:56:47 +0200 Subject: [PATCH 37/43] refactor(ui): remove `MatrixInput` --- .../common/MatrixInput/MatrixAssignDialog.tsx | 210 --------------- .../components/common/MatrixInput/index.tsx | 240 ------------------ .../components/common/MatrixInput/style.ts | 60 ----- 3 files changed, 510 deletions(-) delete mode 100644 webapp/src/components/common/MatrixInput/MatrixAssignDialog.tsx delete mode 100644 webapp/src/components/common/MatrixInput/index.tsx delete mode 100644 webapp/src/components/common/MatrixInput/style.ts diff --git a/webapp/src/components/common/MatrixInput/MatrixAssignDialog.tsx b/webapp/src/components/common/MatrixInput/MatrixAssignDialog.tsx deleted file mode 100644 index 4ca1978912..0000000000 --- a/webapp/src/components/common/MatrixInput/MatrixAssignDialog.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Copyright (c) 2024, 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, Button, Divider, Typography } from "@mui/material"; -import { AxiosError } from "axios"; -import { useSnackbar } from "notistack"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { MatrixInfoDTO, StudyMetadata } from "../../../common/types"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import { getMatrix, getMatrixList } from "../../../services/api/matrix"; -import { appendCommands } from "../../../services/api/variant"; -import DataPropsView from "../../App/Data/DataPropsView"; -import { CommandEnum } from "../../App/Singlestudy/Commands/Edition/commandTypes"; -import ButtonBack from "../ButtonBack"; -import BasicDialog, { BasicDialogProps } from "../dialogs/BasicDialog"; -import EditableMatrix from "../EditableMatrix"; -import FileTable from "../FileTable"; -import UsePromiseCond from "../utils/UsePromiseCond"; -import SplitView from "../SplitView"; - -interface Props { - studyId: StudyMetadata["id"]; - path: string; - open: BasicDialogProps["open"]; - onClose: VoidFunction; -} - -function MatrixAssignDialog(props: Props) { - const [t] = useTranslation(); - const { studyId, path, open, onClose } = props; - const [selectedItem, setSelectedItem] = useState(""); - const [currentMatrix, setCurrentMatrix] = useState(); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); - const { enqueueSnackbar } = useSnackbar(); - - const resList = usePromiseWithSnackbarError(() => getMatrixList(), { - errorMessage: t("data.error.matrixList"), - }); - - const resMatrix = usePromiseWithSnackbarError( - async () => { - if (currentMatrix) { - const res = await getMatrix(currentMatrix.id); - return res; - } - }, - { - errorMessage: t("data.error.matrix"), - deps: [currentMatrix], - }, - ); - - useEffect(() => { - setCurrentMatrix(undefined); - }, [selectedItem]); - - const dataSet = resList.data?.find((item) => item.id === selectedItem); - const matrices = dataSet?.matrices; - const matrixName = `${t("global.matrices")} - ${dataSet?.name}`; - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleMatrixClick = async (id: string) => { - if (matrices) { - setCurrentMatrix({ - id, - name: matrices.find((o) => o.id === id)?.name || "", - }); - } - }; - - const handleAssignation = async (matrixId: string) => { - try { - await appendCommands(studyId, [ - { - action: CommandEnum.REPLACE_MATRIX, - args: { - target: path, - matrix: matrixId, - }, - }, - ]); - enqueueSnackbar(t("data.success.matrixAssignation"), { - variant: "success", - }); - onClose(); - } catch (e) { - enqueueErrorSnackbar(t("data.error.matrixAssignation"), e as AxiosError); - } - }; - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - - return ( - {t("global.close")}} - contentProps={{ - sx: { width: "1200px", height: "700px" }, - }} - > - - dataset && ( - - - - {selectedItem && !currentMatrix && ( - - - {matrixName} - - - } - content={matrices || []} - onRead={handleMatrixClick} - onAssign={handleAssignation} - /> - )} - - matrix && ( - <> - - - {matrixName} - - - - setCurrentMatrix(undefined)} - /> - - - - - - ) - } - /> - - - ) - } - /> - - ); -} - -export default MatrixAssignDialog; diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx deleted file mode 100644 index 4034517235..0000000000 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Copyright (c) 2024, 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 { useTranslation } from "react-i18next"; -import { useSnackbar } from "notistack"; -import { useState } from "react"; -import { AxiosError } from "axios"; -import { Typography, Box, Divider } from "@mui/material"; -import FileDownloadIcon from "@mui/icons-material/FileDownload"; -import GridOffIcon from "@mui/icons-material/GridOff"; -import { - MatrixEditDTO, - MatrixStats, - StudyMetadata, -} from "../../../common/types"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import { getStudyData } from "../../../services/api/study"; -import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError"; -import { editMatrix, getStudyMatrixIndex } from "../../../services/api/matrix"; -import { Root, Content, Header } from "./style"; -import SimpleLoader from "../loaders/SimpleLoader"; -import EmptyView from "../page/SimpleContent"; -import EditableMatrix from "../EditableMatrix"; -import ImportDialog from "../dialogs/ImportDialog"; -import MatrixAssignDialog from "./MatrixAssignDialog"; -import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; -import SplitButton from "../buttons/SplitButton"; -import DownloadMatrixButton from "../buttons/DownloadMatrixButton.tsx"; -import { importFile } from "../../../services/api/studies/raw/index.ts"; - -interface Props { - study: StudyMetadata | StudyMetadata["id"]; - url: string; - columnsNames?: string[] | readonly string[]; - rowNames?: string[]; - title?: string; - computStats: MatrixStats; - fetchFn?: fetchMatrixFn; - disableEdit?: boolean; - disableImport?: boolean; - showPercent?: boolean; -} - -function MatrixInput({ - study, - url, - columnsNames, - rowNames: initialRowNames, - title, - computStats, - fetchFn, - disableEdit = false, - disableImport = false, - showPercent, -}: Props) { - const { enqueueSnackbar } = useSnackbar(); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); - const [t] = useTranslation(); - const [openImportDialog, setOpenImportDialog] = useState(false); - const [openMatrixAsignDialog, setOpenMatrixAsignDialog] = useState(false); - const studyId = typeof study === "string" ? study : study.id; - - const { - data: matrixData, - isLoading, - reload: reloadMatrix, - } = usePromiseWithSnackbarError(fetchMatrixData, { - errorMessage: t("data.error.matrix"), - deps: [studyId, url, fetchFn], - }); - - const { data: matrixIndex } = usePromiseWithSnackbarError( - async () => { - if (fetchFn) { - return matrixData?.index; - } - return getStudyMatrixIndex(studyId, url); - }, - { - errorMessage: t("matrix.error.failedToretrieveIndex"), - deps: [study, url, fetchFn, matrixData], - }, - ); - - /** - * If fetchFn is provided, custom row names (area names) are used from the matrixData's index property. - * Otherwise, default row numbers and timestamps are displayed using initialRowNames. - */ - const rowNames = fetchFn ? matrixIndex : initialRowNames; - const columnsLength = matrixData?.columns?.length ?? 0; - - //////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////// - - async function fetchMatrixData() { - const res = fetchFn - ? await fetchFn(studyId) - : await getStudyData(studyId, url); - if (typeof res === "string") { - const fixed = res - .replace(/NaN/g, '"NaN"') - .replace(/Infinity/g, '"Infinity"'); - return JSON.parse(fixed); - } - return res; - } - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleUpdate = async (change: MatrixEditDTO[], source: string) => { - if (source !== "loadData" && source !== "updateData") { - try { - if (change.length > 0) { - const sanitizedUrl = url.startsWith("/") ? url.substring(1) : url; - await editMatrix(studyId, sanitizedUrl, change); - enqueueSnackbar(t("matrix.success.matrixUpdate"), { - variant: "success", - }); - } - } catch (e) { - enqueueErrorSnackbar(t("matrix.error.matrixUpdate"), e as AxiosError); - } - } - }; - - const handleImport = async (file: File) => { - await importFile({ file, studyId, path: url }); - reloadMatrix(); - }; - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - - return ( - - -
- - {title || t("xpansion.timeSeries")} - - - {!disableImport && ( - { - if (index === 0) { - setOpenImportDialog(true); - } else { - setOpenMatrixAsignDialog(true); - } - }} - size="small" - ButtonProps={{ - startIcon: , - }} - > - {t("global.import")} - - )} - - -
- - {isLoading && } - {!isLoading && columnsLength >= 1 && matrixIndex ? ( - - ) : ( - !isLoading && ( - - ) - )} -
- {openImportDialog && ( - setOpenImportDialog(false)} - onImport={handleImport} - accept={{ "text/*": [".csv", ".tsv", ".txt"] }} - /> - )} - {openMatrixAsignDialog && ( - { - setOpenMatrixAsignDialog(false); - reloadMatrix(); - }} - /> - )} -
- ); -} - -export default MatrixInput; diff --git a/webapp/src/components/common/MatrixInput/style.ts b/webapp/src/components/common/MatrixInput/style.ts deleted file mode 100644 index 148f1b191d..0000000000 --- a/webapp/src/components/common/MatrixInput/style.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2024, 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 { styled, Box, Button } from "@mui/material"; - -export const Root = styled(Box)(({ theme }) => ({ - flex: 1, - height: "100%", - width: "100%", - display: "flex", - flexFlow: "column nowrap", - justifyContent: "flex-start", - alignItems: "center", -})); - -export const Header = styled(Box)(({ theme }) => ({ - width: "100%", - display: "flex", - flexFlow: "row wrap", - gap: theme.spacing(1), - justifyContent: "space-between", - alignItems: "flex-end", -})); - -export const Content = styled(Box)(({ theme }) => ({ - boxSizing: "border-box", - flex: 1, - width: "100%", - display: "flex", - flexFlow: "column nowrap", - justifyContent: "flex-start", - alignItems: "flex-start", - overflow: "auto", - position: "relative", -})); - -export const StyledButton = styled(Button)(({ theme }) => ({ - backgroundColor: "rgba(180, 180, 180, 0.09)", - color: "white", - borderRight: "none !important", - "&:hover": { - color: "white", - backgroundColor: theme.palette.secondary.main, - }, - "&:disabled": { - backgroundColor: theme.palette.secondary.dark, - color: "white !important", - }, -})); From c377f3be1f0158a0c7b474043ba212edb1d16715 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 25 Oct 2024 16:41:10 +0200 Subject: [PATCH 38/43] refactor(ui): improves `Matrix` folder and files overall organization --- .../Singlestudy/explore/Debug/Data/Matrix.tsx | 2 +- .../Areas/Hydro/Allocation/utils.ts | 2 +- .../Areas/Hydro/Correlation/utils.ts | 2 +- .../Modelization/Areas/Hydro/HydroMatrix.tsx | 2 +- .../explore/Modelization/Areas/Hydro/utils.ts | 2 +- .../explore/Modelization/Areas/Load.tsx | 2 +- .../explore/Modelization/Areas/MiscGen.tsx | 2 +- .../Modelization/Areas/Renewables/Matrix.tsx | 2 +- .../explore/Modelization/Areas/Reserve.tsx | 2 +- .../explore/Modelization/Areas/Solar.tsx | 2 +- .../Modelization/Areas/Storages/Matrix.tsx | 2 +- .../Modelization/Areas/Thermal/Matrix.tsx | 2 +- .../explore/Modelization/Areas/Wind.tsx | 2 +- .../BindingConstView/Matrix.tsx | 2 +- .../Modelization/Links/LinkView/LinkForm.tsx | 2 +- .../Links/LinkView/LinkMatrixView.tsx | 2 +- .../explore/Results/ResultDetails/index.tsx | 6 +- .../MatrixActions}/MatrixActions.test.tsx | 2 +- .../components/MatrixActions/index.tsx} | 8 +- .../MatrixGrid/MatrixGrid.test.tsx} | 19 +- .../components}/MatrixGrid/index.tsx | 14 +- .../Matrix/components/MatrixGrid/styles.ts | 72 ++++ .../common/Matrix/core/__tests__/fixtures.ts | 214 +++++++++++ .../common/Matrix/core/__tests__/types.ts | 20 + .../Matrix/core/__tests__/utils.test.ts | 138 +++++++ .../common/Matrix/core/constants.ts | 116 ++++++ .../{MatrixGrid => Matrix/core}/types.ts | 50 +-- .../{MatrixGrid => Matrix/core}/utils.ts | 156 +------- .../useColumnMapping/__tests__/fixtures.ts | 31 ++ .../__tests__}/useColumnMapping.test.ts | 45 +-- .../hooks/useColumnMapping/__tests__/utils.ts | 40 ++ .../hooks/useColumnMapping/index.ts} | 3 +- .../__tests__/assertions.ts | 51 +++ .../useGridCellContent/__tests__/fixtures.ts | 105 ++++++ .../__tests__/useGridCellContent.test.ts | 124 +++++++ .../useGridCellContent/__tests__/utils.ts | 47 +++ .../hooks/useGridCellContent/index.ts} | 17 +- .../Matrix/hooks/useGridCellContent/types.ts | 51 +++ .../hooks/useMatrix/index.ts} | 24 +- .../hooks/useMatrix}/useMatrix.test.tsx | 155 ++++---- .../hooks/useMatrixPortal/index.ts} | 0 .../useMatrixPortal}/useMatrixPortal.test.tsx | 21 +- .../Matrix.tsx => Matrix/index.tsx} | 10 +- .../{MatrixGrid/style.ts => Matrix/styles.ts} | 0 .../MatrixGrid/useGridCellContent.test.ts | 267 ------------- .../common/MatrixGrid/utils.test.ts | 350 ------------------ webapp/src/services/api/matrix.ts | 2 +- .../validation/{ => __tests__}/array.test.ts | 2 +- .../validation/{ => __tests__}/number.test.ts | 2 +- .../validation/{ => __tests__}/string.test.ts | 2 +- 50 files changed, 1189 insertions(+), 1007 deletions(-) rename webapp/src/components/common/{MatrixGrid => Matrix/components/MatrixActions}/MatrixActions.test.tsx (99%) rename webapp/src/components/common/{MatrixGrid/MatrixActions.tsx => Matrix/components/MatrixActions/index.tsx} (91%) rename webapp/src/components/common/{MatrixGrid/index.test.tsx => Matrix/components/MatrixGrid/MatrixGrid.test.tsx} (91%) rename webapp/src/components/common/{ => Matrix/components}/MatrixGrid/index.tsx (93%) create mode 100644 webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts create mode 100644 webapp/src/components/common/Matrix/core/__tests__/fixtures.ts create mode 100644 webapp/src/components/common/Matrix/core/__tests__/types.ts create mode 100644 webapp/src/components/common/Matrix/core/__tests__/utils.test.ts create mode 100644 webapp/src/components/common/Matrix/core/constants.ts rename webapp/src/components/common/{MatrixGrid => Matrix/core}/types.ts (70%) rename webapp/src/components/common/{MatrixGrid => Matrix/core}/utils.ts (68%) create mode 100644 webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts rename webapp/src/components/common/{MatrixGrid => Matrix/hooks/useColumnMapping/__tests__}/useColumnMapping.test.ts (75%) create mode 100644 webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts rename webapp/src/components/common/{MatrixGrid/useColumnMapping.ts => Matrix/hooks/useColumnMapping/index.ts} (96%) create mode 100644 webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/assertions.ts create mode 100644 webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts create mode 100644 webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts create mode 100644 webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/utils.ts rename webapp/src/components/common/{MatrixGrid/useGridCellContent.ts => Matrix/hooks/useGridCellContent/index.ts} (95%) create mode 100644 webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts rename webapp/src/components/common/{MatrixGrid/useMatrix.ts => Matrix/hooks/useMatrix/index.ts} (92%) rename webapp/src/components/common/{MatrixGrid => Matrix/hooks/useMatrix}/useMatrix.test.tsx (69%) rename webapp/src/components/common/{MatrixGrid/useMatrixPortal.ts => Matrix/hooks/useMatrixPortal/index.ts} (100%) rename webapp/src/components/common/{MatrixGrid => Matrix/hooks/useMatrixPortal}/useMatrixPortal.test.tsx (85%) rename webapp/src/components/common/{MatrixGrid/Matrix.tsx => Matrix/index.tsx} (95%) rename webapp/src/components/common/{MatrixGrid/style.ts => Matrix/styles.ts} (100%) delete mode 100644 webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts delete mode 100644 webapp/src/components/common/MatrixGrid/utils.test.ts rename webapp/src/utils/validation/{ => __tests__}/array.test.ts (98%) rename webapp/src/utils/validation/{ => __tests__}/number.test.ts (98%) rename webapp/src/utils/validation/{ => __tests__}/string.test.ts (98%) diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx index 6e0c1b91c0..8c791964ab 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import Matrix from "../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../common/Matrix"; import type { DataCompProps } from "../utils"; function DebugMatrix({ studyId, filename, filePath, canEdit }: DataCompProps) { diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts index 7bb47baffb..96d48cb669 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts @@ -14,7 +14,7 @@ import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; -import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; +import { MatrixDataDTO } from "../../../../../../../common/Matrix/core/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts index 5806dd3e9b..0c538682bc 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts @@ -14,7 +14,7 @@ import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; -import { MatrixDataDTO } from "../../../../../../../common/MatrixGrid/types"; +import { MatrixDataDTO } from "../../../../../../../common/Matrix/core/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index ff54f64df7..372ae3cfce 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -15,7 +15,7 @@ import useAppSelector from "../../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../../redux/selectors"; import { MATRICES, HydroMatrixType } from "./utils"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; import { Box } from "@mui/material"; interface Props { diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index b7d00a68de..735fecd13f 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -15,7 +15,7 @@ import { MatrixDataDTO, AggregateConfig, -} from "../../../../../../common/MatrixGrid/types"; +} from "../../../../../../common/Matrix/core/types"; import { SplitViewProps } from "../../../../../../common/SplitView"; import { getAllocationMatrix } from "./Allocation/utils"; import { getCorrelationMatrix } from "./Correlation/utils"; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx index 2f83633bb7..9581fae79d 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Load.tsx @@ -14,7 +14,7 @@ import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Matrix from "../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../common/Matrix"; function Load() { const currentArea = useAppSelector(getCurrentAreaId); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx index ccdba1652e..8cdf4946af 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/MiscGen.tsx @@ -14,7 +14,7 @@ import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Matrix from "../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../common/Matrix"; function MiscGen() { const currentArea = useAppSelector(getCurrentAreaId); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx index 70429f1883..47ff9dcf82 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Matrix.tsx @@ -13,7 +13,7 @@ */ import { Cluster } from "../../../../../../../common/types"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; interface Props { areaId: string; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx index da9d01870e..926286613e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Reserve.tsx @@ -14,7 +14,7 @@ import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Matrix from "../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../common/Matrix"; function Reserve() { const currentArea = useAppSelector(getCurrentAreaId); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx index 487f4b4e70..7a2a3a8284 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Solar.tsx @@ -14,7 +14,7 @@ import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Matrix from "../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../common/Matrix"; function Solar() { const currentArea = useAppSelector(getCurrentAreaId); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx index 1062c86138..424d482b57 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Matrix.tsx @@ -23,7 +23,7 @@ import { } from "../../../../../../../common/types"; import { Storage } from "./utils"; import SplitView from "../../../../../../common/SplitView"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; interface Props { study: StudyMetadata; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx index a2a0924fa7..8d46249fac 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx @@ -19,7 +19,7 @@ import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; import { Cluster, StudyMetadata } from "../../../../../../../common/types"; import { COMMON_MATRIX_COLS, TS_GEN_MATRIX_COLS } from "./utils"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; interface Props { study: StudyMetadata; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx index 40e7277c0f..68291addcf 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Wind.tsx @@ -14,7 +14,7 @@ import useAppSelector from "../../../../../../redux/hooks/useAppSelector"; import { getCurrentAreaId } from "../../../../../../redux/selectors"; -import Matrix from "../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../common/Matrix"; function Wind() { const currentArea = useAppSelector(getCurrentAreaId); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/Matrix.tsx index 666f3fb82c..03f7bfddf3 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/BindingConstView/Matrix.tsx @@ -20,7 +20,7 @@ import { Box, Button } from "@mui/material"; import BasicDialog, { BasicDialogProps, } from "../../../../../../common/dialogs/BasicDialog"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; interface Props { study: StudyMetadata; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx index 88d5e389e2..9721b9b82c 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkForm.tsx @@ -27,7 +27,7 @@ import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; import LinkMatrixView from "./LinkMatrixView"; import OutputFilters from "../../../common/OutputFilters"; import { useFormContextPlus } from "../../../../../../common/Form"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; interface Props { link: LinkElement; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx index 503b6c3bd1..ce0960ac7d 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Links/LinkView/LinkMatrixView.tsx @@ -19,7 +19,7 @@ import Box from "@mui/material/Box"; import { useTranslation } from "react-i18next"; import { MatrixItem, StudyMetadata } from "../../../../../../../common/types"; import SplitView from "../../../../../../common/SplitView"; -import Matrix from "../../../../../../common/MatrixGrid/Matrix"; +import Matrix from "../../../../../../common/Matrix"; interface Props { study: StudyMetadata; diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index fbcfedfe9c..ea82685854 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -53,12 +53,12 @@ import UsePromiseCond, { } from "../../../../../common/utils/UsePromiseCond"; import useStudySynthesis from "../../../../../../redux/hooks/useStudySynthesis"; import ButtonBack from "../../../../../common/ButtonBack"; -import MatrixGrid from "../../../../../common/MatrixGrid/index.tsx"; +import MatrixGrid from "../../../../../common/Matrix/components/MatrixGrid/index.tsx"; import { generateCustomColumns, generateDateTime, -} from "../../../../../common/MatrixGrid/utils.ts"; -import { Column } from "../../../../../common/MatrixGrid/types.ts"; +} from "../../../../../common/Matrix/core/utils.ts"; +import { Column } from "@/components/common/Matrix/core/constants.ts"; import SplitView from "../../../../../common/SplitView/index.tsx"; import ResultFilters from "./ResultFilters.tsx"; import { toError } from "../../../../../../utils/fnUtils.ts"; diff --git a/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx b/webapp/src/components/common/Matrix/components/MatrixActions/MatrixActions.test.tsx similarity index 99% rename from webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx rename to webapp/src/components/common/Matrix/components/MatrixActions/MatrixActions.test.tsx index 487959168d..f2077c21aa 100644 --- a/webapp/src/components/common/MatrixGrid/MatrixActions.test.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixActions/MatrixActions.test.tsx @@ -15,7 +15,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import MatrixActions from "./MatrixActions"; +import MatrixActions from "."; vi.mock("../buttons/SplitButton", () => ({ default: ({ diff --git a/webapp/src/components/common/MatrixGrid/MatrixActions.tsx b/webapp/src/components/common/Matrix/components/MatrixActions/index.tsx similarity index 91% rename from webapp/src/components/common/MatrixGrid/MatrixActions.tsx rename to webapp/src/components/common/Matrix/components/MatrixActions/index.tsx index d822c13f69..b5faa9b785 100644 --- a/webapp/src/components/common/MatrixGrid/MatrixActions.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixActions/index.tsx @@ -13,13 +13,11 @@ */ import { Box, Divider, IconButton, Tooltip } from "@mui/material"; -import SplitButton from "../buttons/SplitButton"; -import FileDownload from "@mui/icons-material/FileDownload"; +import SplitButton from "@/components/common/buttons/SplitButton"; +import DownloadMatrixButton from "@/components/common/buttons/DownloadMatrixButton"; +import { FileDownload, Save, Undo, Redo } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { LoadingButton } from "@mui/lab"; -import Save from "@mui/icons-material/Save"; -import { Undo, Redo } from "@mui/icons-material"; -import DownloadMatrixButton from "../buttons/DownloadMatrixButton"; interface MatrixActionsProps { onImport: VoidFunction; diff --git a/webapp/src/components/common/MatrixGrid/index.test.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx similarity index 91% rename from webapp/src/components/common/MatrixGrid/index.test.tsx rename to webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx index 81cf7f8ebd..5a9325f7f9 100644 --- a/webapp/src/components/common/MatrixGrid/index.test.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx @@ -15,11 +15,20 @@ import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import Box from "@mui/material/Box"; -import MatrixGrid from "."; -import SplitView from "../SplitView"; -import { type EnhancedGridColumn, Column, RenderMatrixOptions } from "./types"; -import { mockGetBoundingClientRect } from "../../../tests/mocks/mockGetBoundingClientRect"; -import { mockHTMLCanvasElement } from "../../../tests/mocks/mockHTMLCanvasElement"; +import MatrixGrid, { MatrixGridProps } from "."; +import SplitView from "../../../SplitView"; +import type { EnhancedGridColumn } from "../../core/types"; +import { mockGetBoundingClientRect } from "../../../../../tests/mocks/mockGetBoundingClientRect"; +import { mockHTMLCanvasElement } from "../../../../../tests/mocks/mockHTMLCanvasElement"; +import { Column } from "../../core/constants"; + +interface RenderMatrixOptions { + width?: string; + height?: string; + data?: MatrixGridProps["data"]; + columns?: EnhancedGridColumn[]; + rows?: number; +} const setupMocks = () => { mockHTMLCanvasElement(); diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx similarity index 93% rename from webapp/src/components/common/MatrixGrid/index.tsx rename to webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx index 51ce67b3a0..73395dd7bc 100644 --- a/webapp/src/components/common/MatrixGrid/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx @@ -22,12 +22,16 @@ import DataEditor, { GridSelection, Item, } from "@glideapps/glide-data-grid"; -import { useGridCellContent } from "./useGridCellContent"; +import { useGridCellContent } from "../../hooks/useGridCellContent"; import { useMemo, useState } from "react"; -import { EnhancedGridColumn, GridUpdate, MatrixAggregates } from "./types"; -import { darkTheme, readOnlyDarkTheme } from "./utils"; -import { useColumnMapping } from "./useColumnMapping"; -import { useMatrixPortal } from "./useMatrixPortal"; +import { + type EnhancedGridColumn, + type GridUpdate, + type MatrixAggregates, +} from "../../core/types"; +import { useColumnMapping } from "../../hooks/useColumnMapping"; +import { useMatrixPortal } from "../../hooks/useMatrixPortal"; +import { darkTheme, readOnlyDarkTheme } from "./styles"; export interface MatrixGridProps { data: number[][]; diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts b/webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts new file mode 100644 index 0000000000..ba084c94cb --- /dev/null +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/styles.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024, 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 { Theme } from "@glideapps/glide-data-grid"; + +export const darkTheme: Theme = { + accentColor: "rgba(255, 184, 0, 0.9)", + accentLight: "rgba(255, 184, 0, 0.2)", + accentFg: "#FFFFFF", + textDark: "#FFFFFF", + textMedium: "#C1C3D9", + textLight: "#A1A5B9", + textBubble: "#FFFFFF", + bgIconHeader: "#1E1F2E", + fgIconHeader: "#FFFFFF", + textHeader: "#FFFFFF", + textGroupHeader: "#C1C3D9", + bgCell: "#262737", // main background color + bgCellMedium: "#2E2F42", + bgHeader: "#1E1F2E", + bgHeaderHasFocus: "#2E2F42", + bgHeaderHovered: "#333447", + bgBubble: "#333447", + bgBubbleSelected: "#3C3E57", + bgSearchResult: "#6366F133", + borderColor: "rgba(255, 255, 255, 0.12)", + drilldownBorder: "rgba(255, 255, 255, 0.35)", + linkColor: "#818CF8", + headerFontStyle: "bold 11px", + baseFontStyle: "13px", + fontFamily: "Inter, sans-serif", + editorFontSize: "13px", + lineHeight: 1.5, + textHeaderSelected: "#FFFFFF", + cellHorizontalPadding: 8, + cellVerticalPadding: 5, + headerIconSize: 16, + markerFontStyle: "normal", +}; + +export const readOnlyDarkTheme: Partial = { + bgCell: "#1A1C2A", + bgCellMedium: "#22243A", + textDark: "#FAF9F6", + textMedium: "#808080", + textLight: "#606060", + accentColor: "#4A4C66", + accentLight: "rgba(74, 76, 102, 0.2)", + borderColor: "rgba(255, 255, 255, 0.08)", + drilldownBorder: "rgba(255, 255, 255, 0.2)", +}; + +export const aggregatesTheme: Partial = { + bgCell: "#3D3E5F", + bgCellMedium: "#383A5C", + textDark: "#FFFFFF", + fontFamily: "Inter, sans-serif", + baseFontStyle: "bold 13px", + editorFontSize: "13px", + headerFontStyle: "bold 11px", +}; diff --git a/webapp/src/components/common/Matrix/core/__tests__/fixtures.ts b/webapp/src/components/common/Matrix/core/__tests__/fixtures.ts new file mode 100644 index 0000000000..c6a728b816 --- /dev/null +++ b/webapp/src/components/common/Matrix/core/__tests__/fixtures.ts @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2024, 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 { Aggregate, TimeFrequency } from "../constants"; +import { FormatTestCase } from "./types"; + +export const BASE_DATA = { + dateConfig: { + start_date: "2023-01-01 00:00:00", + steps: 3, + first_week_size: 7, + }, + matrix: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], +}; + +export const DATE_TIME_TEST_CASES = [ + { + name: "annual format", + config: { ...BASE_DATA.dateConfig, level: TimeFrequency.Annual }, + expected: [ + "global.time.annual", + "global.time.annual", + "global.time.annual", + ], + }, + { + name: "monthly format", + config: { ...BASE_DATA.dateConfig, level: TimeFrequency.Monthly }, + expected: ["Jan", "Feb", "Mar"], + }, + { + name: "weekly format", + config: { + ...BASE_DATA.dateConfig, + level: TimeFrequency.Weekly, + first_week_size: 1, + }, + expected: ["W. 01", "W. 02", "W. 03"], + }, + { + name: "daily format", + config: { ...BASE_DATA.dateConfig, level: TimeFrequency.Daily }, + expected: ["Sun 1 Jan", "Mon 2 Jan", "Tue 3 Jan"], + }, + { + name: "hourly format", + config: { + start_date: "2039-07-01 00:00:00", + steps: 3, + first_week_size: 7, + level: TimeFrequency.Hourly, + }, + expected: ["Fri 1 Jul 00:00", "Fri 1 Jul 01:00", "Fri 1 Jul 02:00"], + }, +]; + +export const COLUMN_TEST_CASES = [ + { + name: "default options", + input: { count: 3 }, + expectedLength: 3, + }, + { + name: "custom options", + input: { count: 2, startIndex: 10, prefix: "Data", editable: false }, + expectedLength: 2, + }, + { + name: "zero count", + input: { count: 0 }, + expectedLength: 0, + }, + { + name: "large count", + input: { count: 1000 }, + expectedLength: 1000, + }, +]; + +export const AGGREGATE_TEST_CASES = [ + { + name: "simple matrix with all aggregates", + matrix: BASE_DATA.matrix, + aggregates: "all" as const, + expected: { + min: [1, 4, 7], + max: [3, 6, 9], + avg: [2, 5, 8], + total: [6, 15, 24], + }, + }, + { + name: "decimal numbers", + matrix: [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 6.6], + ], + aggregates: "all" as const, + expected: { + min: [1.1, 4.4], + max: [3.3, 6.6], + avg: [2, 6], + total: [7, 17], + }, + }, + { + name: "negative numbers", + matrix: [ + [-1, -2, -3], + [-4, 0, 4], + ], + aggregates: "all" as const, + expected: { + min: [-3, -4], + max: [-1, 4], + avg: [-2, 0], + total: [-6, 0], + }, + }, +]; + +export const FORMAT_TEST_CASES: Array<{ + name: string; + cases: FormatTestCase[]; +}> = [ + { + name: "integer numbers", + cases: [ + { + description: "formats large number", + value: 1234567, + expected: "1 234 567", + }, + { + description: "formats million", + value: 1000000, + expected: "1 000 000", + }, + { description: "formats single digit", value: 1, expected: "1" }, + ], + }, + { + name: "decimal numbers", + cases: [ + { + description: "formats with 2 decimals", + value: 1234.56, + maxDecimals: 2, + expected: "1 234.56", + }, + { + description: "formats with 3 decimals", + value: 1000000.123, + maxDecimals: 3, + expected: "1 000 000.123", + }, + ], + }, + { + name: "special cases", + cases: [ + { + description: "handles undefined", + value: undefined, + maxDecimals: 3, + expected: "", + }, + { description: "handles zero", value: 0, expected: "0" }, + { + description: "handles negative", + value: -1234567, + expected: "-1 234 567", + }, + { + description: "handles very large number", + value: 1e20, + expected: "100 000 000 000 000 000 000", + }, + ], + }, +]; + +export const AGGREGATE_CONFIG_CASES = [ + { + name: "stats configuration", + aggregates: "stats" as const, + expected: [Aggregate.Avg, Aggregate.Min, Aggregate.Max], + }, + { + name: "all configuration", + aggregates: "all" as const, + expected: [Aggregate.Min, Aggregate.Max, Aggregate.Avg, Aggregate.Total], + }, + { + name: "custom configuration", + aggregates: [Aggregate.Min, Aggregate.Max], + expected: [Aggregate.Min, Aggregate.Max], + }, +]; diff --git a/webapp/src/components/common/Matrix/core/__tests__/types.ts b/webapp/src/components/common/Matrix/core/__tests__/types.ts new file mode 100644 index 0000000000..72310190b6 --- /dev/null +++ b/webapp/src/components/common/Matrix/core/__tests__/types.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2024, 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. + */ + +export interface FormatTestCase { + description: string; + value: number | undefined; + maxDecimals?: number; + expected: string; +} diff --git a/webapp/src/components/common/Matrix/core/__tests__/utils.test.ts b/webapp/src/components/common/Matrix/core/__tests__/utils.test.ts new file mode 100644 index 0000000000..847541d3b3 --- /dev/null +++ b/webapp/src/components/common/Matrix/core/__tests__/utils.test.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2024, 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 { Column } from "../constants"; +import { + calculateMatrixAggregates, + formatNumber, + generateCustomColumns, + generateDateTime, + generateTimeSeriesColumns, + getAggregateTypes, +} from "../utils"; +import { + DATE_TIME_TEST_CASES, + COLUMN_TEST_CASES, + AGGREGATE_TEST_CASES, + FORMAT_TEST_CASES, + AGGREGATE_CONFIG_CASES, +} from "./fixtures"; + +describe("Matrix Utils", () => { + beforeAll(() => { + vi.mock("date-fns", async () => { + const actual = (await vi.importActual( + "date-fns", + )) as typeof import("date-fns"); + return { + ...actual, + format: vi.fn((date: Date, formatString: string) => { + if (formatString.includes("ww")) { + const weekNumber = actual.getWeek(date); + return `W. ${weekNumber.toString().padStart(2, "0")}`; + } + return actual.format(date, formatString); + }), + }; + }); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2023-01-01 00:00:00")); + }); + + describe("DateTime Generation", () => { + test.each(DATE_TIME_TEST_CASES)( + "generates correct $name", + ({ config, expected }) => { + const result = generateDateTime(config); + expect(result).toEqual(expected); + }, + ); + }); + + describe("Time Series Column Generation", () => { + test.each(COLUMN_TEST_CASES)( + "handles $name correctly", + ({ input, expectedLength }) => { + const result = generateTimeSeriesColumns(input); + expect(result).toHaveLength(expectedLength); + + if (expectedLength > 0) { + if (input.startIndex) { + expect(result[0].id).toBe(`data${input.startIndex}`); + expect(result[0].title).toBe( + `${input.prefix || "TS"} ${input.startIndex}`, + ); + } else { + expect(result[0]).toEqual({ + id: "data1", + title: "TS 1", + type: Column.Number, + style: "normal", + editable: input.editable ?? true, + }); + } + } + }, + ); + }); + + describe("Matrix Aggregates Calculation", () => { + test.each(AGGREGATE_TEST_CASES)( + "calculates $name correctly", + ({ matrix, aggregates, expected }) => { + const aggregatesTypes = getAggregateTypes(aggregates); + const result = calculateMatrixAggregates(matrix, aggregatesTypes); + expect(result).toEqual(expected); + }, + ); + }); + + describe("Number Formatting", () => { + describe.each(FORMAT_TEST_CASES)("$name", ({ cases }) => { + test.each(cases)("$description", ({ value, maxDecimals, expected }) => { + expect(formatNumber({ value, maxDecimals })).toBe(expected); + }); + }); + }); + + describe("Custom Column Generation", () => { + test("generates columns with correct properties", () => { + const titles = ["Custom 1", "Custom 2", "Custom 3"]; + const width = 100; + + const result = generateCustomColumns({ titles, width }); + expect(result).toHaveLength(3); + + result.forEach((column, index) => { + expect(column).toEqual({ + id: `custom${index + 1}`, + title: titles[index], + type: Column.Number, + style: "normal", + width, + editable: true, + }); + }); + }); + }); + + describe("Aggregate Type Configuration", () => { + test.each(AGGREGATE_CONFIG_CASES)( + "handles $name correctly", + ({ aggregates, expected }) => { + expect(getAggregateTypes(aggregates)).toEqual(expected); + }, + ); + }); +}); diff --git a/webapp/src/components/common/Matrix/core/constants.ts b/webapp/src/components/common/Matrix/core/constants.ts new file mode 100644 index 0000000000..026d91bcf3 --- /dev/null +++ b/webapp/src/components/common/Matrix/core/constants.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2024, 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 { + DateIncrementFunction, + FormatFunction, + TimeFrequencyType, +} from "./types"; +import { + addYears, + addMonths, + addWeeks, + startOfWeek, + FirstWeekContainsDate, + addDays, + addHours, + format, +} from "date-fns"; +import { t } from "i18next"; +import { getLocale } from "./utils"; + +//////////////////////////////////////////////////////////////// +// Enums +//////////////////////////////////////////////////////////////// + +export const Column = { + DateTime: "datetime", + Number: "number", + Text: "text", + Aggregate: "aggregate", +} as const; + +export const Operation = { + Add: "+", + Sub: "-", + Mul: "*", + Div: "/", + Abs: "ABS", + Eq: "=", +} as const; + +// !NOTE: Keep lowercase to match Glide Data Grid column ids +export const Aggregate = { + Min: "min", + Max: "max", + Avg: "avg", + Total: "total", +} as const; + +export const TimeFrequency = { + Annual: "annual", + Monthly: "monthly", + Weekly: "weekly", + Daily: "daily", + Hourly: "hourly", +} as const; + +//////////////////////////////////////////////////////////////// +// Constants +//////////////////////////////////////////////////////////////// + +/** + * Configuration object for different time frequencies + * + * This object defines how to increment and format dates for various time frequencies. + * The WEEKLY frequency is of particular interest as it implements custom week starts + * and handles ISO week numbering. + */ +export const TIME_FREQUENCY_CONFIG: Record< + TimeFrequencyType, + { + increment: DateIncrementFunction; + format: FormatFunction; + } +> = { + [TimeFrequency.Annual]: { + increment: addYears, + format: () => t("global.time.annual"), + }, + [TimeFrequency.Monthly]: { + increment: addMonths, + format: (date: Date) => format(date, "MMM", { locale: getLocale() }), + }, + [TimeFrequency.Weekly]: { + increment: addWeeks, + format: (date: Date, firstWeekSize: number) => { + const weekStart = startOfWeek(date, { locale: getLocale() }); + + return format(weekStart, `'${t("global.time.weekShort")}' ww`, { + locale: getLocale(), + weekStartsOn: firstWeekSize === 1 ? 0 : 1, + firstWeekContainsDate: firstWeekSize as FirstWeekContainsDate, + }); + }, + }, + [TimeFrequency.Daily]: { + increment: addDays, + format: (date: Date) => format(date, "EEE d MMM", { locale: getLocale() }), + }, + [TimeFrequency.Hourly]: { + increment: addHours, + format: (date: Date) => + format(date, "EEE d MMM HH:mm", { locale: getLocale() }), + }, +}; diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/Matrix/core/types.ts similarity index 70% rename from webapp/src/components/common/MatrixGrid/types.ts rename to webapp/src/components/common/Matrix/core/types.ts index 4e6d60d6e8..8e0b1f25c2 100644 --- a/webapp/src/components/common/MatrixGrid/types.ts +++ b/webapp/src/components/common/Matrix/core/types.ts @@ -17,47 +17,7 @@ import { EditableGridCell, Item, } from "@glideapps/glide-data-grid"; -import { MatrixGridProps } from "."; - -//////////////////////////////////////////////////////////////// -// Enums -//////////////////////////////////////////////////////////////// - -export const Column = { - DateTime: "datetime", - Number: "number", - Text: "text", - Aggregate: "aggregate", -} as const; - -export const Operation = { - Add: "+", - Sub: "-", - Mul: "*", - Div: "/", - Abs: "ABS", - Eq: "=", -} as const; - -// !NOTE: Keep lowercase to match Glide Data Grid column ids -export const Aggregate = { - Min: "min", - Max: "max", - Avg: "avg", - Total: "total", -} as const; - -export const TimeFrequency = { - Annual: "annual", - Monthly: "monthly", - Weekly: "weekly", - Daily: "daily", - Hourly: "hourly", -} as const; - -//////////////////////////////////////////////////////////////// -// Types -//////////////////////////////////////////////////////////////// +import { Aggregate, Column, Operation, TimeFrequency } from "./constants"; // Derived types export type ColumnType = (typeof Column)[keyof typeof Column]; @@ -138,11 +98,3 @@ export interface MatrixUpdateDTO { coordinates: number[][]; // Array of [col, row] pairs operation: MatrixUpdate; } - -export interface RenderMatrixOptions { - width?: string; - height?: string; - data?: MatrixGridProps["data"]; - columns?: EnhancedGridColumn[]; - rows?: number; -} diff --git a/webapp/src/components/common/MatrixGrid/utils.ts b/webapp/src/components/common/Matrix/core/utils.ts similarity index 68% rename from webapp/src/components/common/MatrixGrid/utils.ts rename to webapp/src/components/common/Matrix/core/utils.ts index 9b45a91745..a2738a7a1d 100644 --- a/webapp/src/components/common/MatrixGrid/utils.ts +++ b/webapp/src/components/common/Matrix/core/utils.ts @@ -13,98 +13,19 @@ */ import { - EnhancedGridColumn, - TimeSeriesColumnOptions, - CustomColumnOptions, - MatrixAggregates, - AggregateType, - AggregateConfig, - DateIncrementFunction, - FormatFunction, - TimeFrequency, - TimeFrequencyType, - DateTimeMetadataDTO, - Aggregate, - Column, - FormatNumberOptions, -} from "./types"; -import { - type FirstWeekContainsDate, - parseISO, - addHours, - addDays, - addWeeks, - addMonths, - addYears, - format, - startOfWeek, - Locale, -} from "date-fns"; + type EnhancedGridColumn, + type TimeSeriesColumnOptions, + type CustomColumnOptions, + type MatrixAggregates, + type AggregateType, + type AggregateConfig, + type DateTimeMetadataDTO, + type FormatNumberOptions, +} from "../core/types"; +import { parseISO, Locale } from "date-fns"; import { fr, enUS } from "date-fns/locale"; -import { getCurrentLanguage } from "../../../utils/i18nUtils"; -import { Theme } from "@glideapps/glide-data-grid"; -import { t } from "i18next"; - -export const darkTheme: Theme = { - accentColor: "rgba(255, 184, 0, 0.9)", - accentLight: "rgba(255, 184, 0, 0.2)", - accentFg: "#FFFFFF", - textDark: "#FFFFFF", - textMedium: "#C1C3D9", - textLight: "#A1A5B9", - textBubble: "#FFFFFF", - bgIconHeader: "#1E1F2E", - fgIconHeader: "#FFFFFF", - textHeader: "#FFFFFF", - textGroupHeader: "#C1C3D9", - bgCell: "#262737", // main background color - bgCellMedium: "#2E2F42", - bgHeader: "#1E1F2E", - bgHeaderHasFocus: "#2E2F42", - bgHeaderHovered: "#333447", - bgBubble: "#333447", - bgBubbleSelected: "#3C3E57", - bgSearchResult: "#6366F133", - borderColor: "rgba(255, 255, 255, 0.12)", - drilldownBorder: "rgba(255, 255, 255, 0.35)", - linkColor: "#818CF8", - headerFontStyle: "bold 11px", - baseFontStyle: "13px", - fontFamily: "Inter, sans-serif", - editorFontSize: "13px", - lineHeight: 1.5, - textHeaderSelected: "#FFFFFF", - cellHorizontalPadding: 8, - cellVerticalPadding: 5, - headerIconSize: 16, - markerFontStyle: "normal", -}; - -export const readOnlyDarkTheme: Partial = { - bgCell: "#1A1C2A", - bgCellMedium: "#22243A", - textDark: "#FAF9F6", - textMedium: "#808080", - textLight: "#606060", - accentColor: "#4A4C66", - accentLight: "rgba(74, 76, 102, 0.2)", - borderColor: "rgba(255, 255, 255, 0.08)", - drilldownBorder: "rgba(255, 255, 255, 0.2)", -}; - -export const aggregatesTheme: Partial = { - bgCell: "#3D3E5F", - bgCellMedium: "#383A5C", - textDark: "#FFFFFF", - fontFamily: "Inter, sans-serif", - baseFontStyle: "bold 13px", - editorFontSize: "13px", - headerFontStyle: "bold 11px", -}; - -//////////////////////////////////////////////////////////////// -// Functions -//////////////////////////////////////////////////////////////// +import { getCurrentLanguage } from "@/utils/i18nUtils"; +import { Aggregate, Column, TIME_FREQUENCY_CONFIG } from "./constants"; /** * Formats a number by adding thousand separators. @@ -153,55 +74,16 @@ export function formatNumber({ return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger; } -function getLocale(): Locale { - const lang = getCurrentLanguage(); - return lang && lang.startsWith("fr") ? fr : enUS; -} - /** - * Configuration object for different time frequencies + * Retrieves the current locale based on the user's language setting. * - * This object defines how to increment and format dates for various time frequencies. - * The WEEKLY frequency is of particular interest as it implements custom week starts - * and handles ISO week numbering. + * @returns Returns either the French (fr) or English US (enUS) locale object + * depending on whether the current language starts with "fr" */ -const TIME_FREQUENCY_CONFIG: Record< - TimeFrequencyType, - { - increment: DateIncrementFunction; - format: FormatFunction; - } -> = { - [TimeFrequency.Annual]: { - increment: addYears, - format: () => t("global.time.annual"), - }, - [TimeFrequency.Monthly]: { - increment: addMonths, - format: (date: Date) => format(date, "MMM", { locale: getLocale() }), - }, - [TimeFrequency.Weekly]: { - increment: addWeeks, - format: (date: Date, firstWeekSize: number) => { - const weekStart = startOfWeek(date, { locale: getLocale() }); - - return format(weekStart, `'${t("global.time.weekShort")}' ww`, { - locale: getLocale(), - weekStartsOn: firstWeekSize === 1 ? 0 : 1, - firstWeekContainsDate: firstWeekSize as FirstWeekContainsDate, - }); - }, - }, - [TimeFrequency.Daily]: { - increment: addDays, - format: (date: Date) => format(date, "EEE d MMM", { locale: getLocale() }), - }, - [TimeFrequency.Hourly]: { - increment: addHours, - format: (date: Date) => - format(date, "EEE d MMM HH:mm", { locale: getLocale() }), - }, -}; +export function getLocale(): Locale { + const lang = getCurrentLanguage(); + return lang && lang.startsWith("fr") ? fr : enUS; +} /** * Generates an array of formatted date/time strings based on the provided configuration diff --git a/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts new file mode 100644 index 0000000000..af74b0f068 --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024, 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 { Column } from "../../../core/constants"; +import { createColumn, createNumericColumn } from "./utils"; + +export const COLUMNS = { + mixed: [ + createColumn("text", Column.Text), + createColumn("date", Column.DateTime), + createNumericColumn("num1"), + createNumericColumn("num2"), + createColumn("agg", Column.Aggregate), + ], + nonData: [ + createColumn("text", Column.Text), + createColumn("date", Column.DateTime), + ], + dataOnly: [createNumericColumn("num1"), createNumericColumn("num2")], +}; diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/useColumnMapping.test.ts similarity index 75% rename from webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts rename to webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/useColumnMapping.test.ts index 45a8913826..8d16c51b0d 100644 --- a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/useColumnMapping.test.ts @@ -13,51 +13,12 @@ */ import { renderHook } from "@testing-library/react"; -import { useColumnMapping } from "./useColumnMapping"; -import { EnhancedGridColumn, Column } from "./types"; -import type { ColumnType } from "./types"; +import { useColumnMapping } from ".."; import { Item } from "@glideapps/glide-data-grid"; +import { createCoordinate, renderColumnMapping } from "./utils"; +import { COLUMNS } from "./fixtures"; describe("useColumnMapping", () => { - // Test data factories - const createColumn = ( - id: string, - type: ColumnType, - editable = false, - ): EnhancedGridColumn => ({ - id, - title: id.charAt(0).toUpperCase() + id.slice(1), - type, - width: 100, - editable, - }); - - const createNumericColumn = (id: string) => - createColumn(id, Column.Number, true); - - const COLUMNS = { - mixed: [ - createColumn("text", Column.Text), - createColumn("date", Column.DateTime), - createNumericColumn("num1"), - createNumericColumn("num2"), - createColumn("agg", Column.Aggregate), - ], - nonData: [ - createColumn("text", Column.Text), - createColumn("date", Column.DateTime), - ], - dataOnly: [createNumericColumn("num1"), createNumericColumn("num2")], - }; - - // Helper function to create properly typed coordinates - const createCoordinate = (col: number, row: number): Item => - [col, row] as Item; - - // Helper function to render the hook - const renderColumnMapping = (columns: EnhancedGridColumn[]) => - renderHook(() => useColumnMapping(columns)); - describe("hook initialization", () => { test("should create mapping functions", () => { const { result } = renderColumnMapping(COLUMNS.mixed); diff --git a/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts new file mode 100644 index 0000000000..8425d789ff --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024, 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 { Item } from "@glideapps/glide-data-grid"; +import { renderHook } from "@testing-library/react"; +import type { ColumnType, EnhancedGridColumn } from "../../../core/types"; +import { useColumnMapping } from ".."; +import { Column } from "../../../core/constants"; + +export const createCoordinate = (col: number, row: number): Item => + [col, row] as Item; + +export const createColumn = ( + id: string, + type: ColumnType, + editable = false, +): EnhancedGridColumn => ({ + id, + title: id.charAt(0).toUpperCase() + id.slice(1), + type, + width: 100, + editable, +}); + +export const createNumericColumn = (id: string) => + createColumn(id, Column.Number, true); + +export const renderColumnMapping = (columns: EnhancedGridColumn[]) => + renderHook(() => useColumnMapping(columns)); diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts similarity index 96% rename from webapp/src/components/common/MatrixGrid/useColumnMapping.ts rename to webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts index ffbf683965..1c10019ca7 100644 --- a/webapp/src/components/common/MatrixGrid/useColumnMapping.ts +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts @@ -14,7 +14,8 @@ import { useMemo } from "react"; import { Item } from "@glideapps/glide-data-grid"; -import { EnhancedGridColumn, Column } from "./types"; +import { EnhancedGridColumn } from "../../core/types"; +import { Column } from "../../core/constants"; /** * A custom hook that provides coordinate mapping functions for a grid with mixed column types. diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/assertions.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/assertions.ts new file mode 100644 index 0000000000..4311c9085d --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/assertions.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024, 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 { GridCell } from "@glideapps/glide-data-grid"; + +export const assertNumberCell = ( + cell: GridCell, + expectedValue: number | undefined, + message: string, +) => { + if (cell.kind === "number" && "data" in cell) { + expect(cell.data).toBe(expectedValue); + } else { + throw new Error(message); + } +}; + +export const assertTextCell = ( + cell: GridCell, + expectedValue: string, + message: string, +) => { + if (cell.kind === "text" && "displayData" in cell) { + expect(cell.displayData).toBe(expectedValue); + } else { + throw new Error(message); + } +}; + +export const assertDateCell = ( + cell: GridCell, + expectedValue: string, + message: string, +) => { + if (cell.kind === "text" && "displayData" in cell) { + expect(cell.displayData).toBe(expectedValue); + } else { + throw new Error(message); + } +}; diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts new file mode 100644 index 0000000000..e9331696cb --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2024, 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 { ColumnType, EnhancedGridColumn } from "../../../core/types"; +import { Column } from "../../../core/constants"; +import type { TestCase } from "../types"; + +export const createColumn = ( + id: string, + type: ColumnType, + editable = false, +): EnhancedGridColumn => ({ + id, + title: id.charAt(0).toUpperCase() + id.slice(1), + type, + width: type === Column.DateTime ? 150 : 50, + editable, +}); + +export const AGGREGATE_DATA = { + columns: [createColumn("total", Column.Aggregate)], + data: [ + [10, 20, 30], + [15, 25, 35], + [5, 15, 25], + ], + aggregates: { + min: [5, 15, 25], + max: [15, 25, 35], + avg: [10, 20, 30], + total: [30, 60, 90], + }, +}; + +export const MIXED_DATA = { + columns: [ + createColumn("rowHeader", Column.Text), + createColumn("date", Column.DateTime), + createColumn("data1", Column.Number, true), + createColumn("data2", Column.Number, true), + createColumn("total", Column.Aggregate), + ], + data: [ + [100, 200], + [150, 250], + ], + dateTime: ["2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"], + rowHeaders: ["Row 1", "Row 2"], + aggregates: { + min: [100, 200], + max: [150, 250], + avg: [125, 225], + total: [250, 450], + }, +}; + +export const EDGE_CASES = { + emptyData: { + columns: [createColumn("data1", Column.Number, true)], + data: [], + }, + singleCell: { + columns: [createColumn("data1", Column.Number, true)], + data: [[100]], + }, +}; + +export const FORMAT_TEST_CASES: TestCase[] = [ + { + desc: "formats regular numbers", + value: 1234567.89, + expected: "1 234 567.89", + }, + { + desc: "handles very large numbers", + value: 1e20, + expected: "100 000 000 000 000 000 000", + }, + { + desc: "handles very small numbers", + value: 0.00001, + expected: "0.00001", + }, + { + desc: "handles negative numbers", + value: -1234567.89, + expected: "-1 234 567.89", + }, + { + desc: "handles zero", + value: 0, + expected: "0", + }, +]; diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts new file mode 100644 index 0000000000..346d63b347 --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2024, 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 { Column } from "../../../core/constants"; +import { + createColumn, + AGGREGATE_DATA, + MIXED_DATA, + FORMAT_TEST_CASES, +} from "./fixtures"; +import { createCoordinate, renderGridCellContent } from "./utils"; +import { assertNumberCell, assertTextCell } from "./assertions"; + +describe("useGridCellContent", () => { + describe("Aggregate columns", () => { + test.each([ + [0, 30], + [1, 60], + [2, 90], + ])("returns correct aggregate for row %i", (row, expected) => { + const getCellContent = renderGridCellContent(AGGREGATE_DATA); + const cell = getCellContent(createCoordinate(0, row)); + assertNumberCell( + cell, + expected, + `Expected aggregate value ${expected} at row ${row}`, + ); + }); + }); + + describe("Mixed column types", () => { + test("handles all column types correctly", () => { + const getCellContent = renderGridCellContent(MIXED_DATA); + + assertTextCell( + getCellContent(createCoordinate(0, 0)), + "Row 1", + "Expected row header text", + ); + + assertNumberCell( + getCellContent(createCoordinate(2, 0)), + 100, + "Expected first data column value", + ); + + assertNumberCell( + getCellContent(createCoordinate(3, 0)), + 200, + "Expected second data column value", + ); + + assertNumberCell( + getCellContent(createCoordinate(4, 0)), + 250, + "Expected aggregate value", + ); + }); + }); + + describe("Edge cases", () => { + test("handles empty data array", () => { + const options = { + columns: [createColumn("data1", Column.Number, true)], + data: [], + }; + + const getCellContent = renderGridCellContent(options); + const cell = getCellContent(createCoordinate(0, 0)); + assertNumberCell(cell, undefined, "Expected undefined for empty data"); + }); + + test("handles out of bounds access", () => { + const options = { + columns: [createColumn("data1", Column.Number, true)], + data: [[100]], + }; + + const getCellContent = renderGridCellContent(options); + + // Column out of bounds + const colCell = getCellContent(createCoordinate(1, 0)); + assertTextCell(colCell, "N/A", "Expected N/A for column out of bounds"); + + // Row out of bounds + const rowCell = getCellContent(createCoordinate(0, 1)); + assertNumberCell( + rowCell, + undefined, + "Expected undefined for row out of bounds", + ); + }); + }); + + describe("Number formatting", () => { + test.each(FORMAT_TEST_CASES)("$desc", ({ value, expected }) => { + const options = { + columns: [createColumn("number", Column.Number, true)], + data: [[value]], + }; + + const getCellContent = renderGridCellContent(options); + const cell = getCellContent(createCoordinate(0, 0)); + + if (cell.kind === "number" && "data" in cell) { + expect(cell.data).toBe(value); + expect(cell.displayData).toBe(expected); + } else { + throw new Error(`Expected formatted number cell for value ${value}`); + } + }); + }); +}); diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/utils.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/utils.ts new file mode 100644 index 0000000000..a75d80f3ea --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/utils.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024, 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 { renderHook } from "@testing-library/react"; +import { type Item } from "@glideapps/glide-data-grid"; +import { useColumnMapping } from "../../useColumnMapping"; +import { useGridCellContent } from ".."; +import type { RenderOptions } from "../types"; + +export const createCoordinate = (col: number, row: number): Item => + [col, row] as Item; + +export const renderGridCellContent = ({ + data, + columns, + dateTime, + aggregates, + rowHeaders, +}: RenderOptions) => { + const { result: mappingResult } = renderHook(() => useColumnMapping(columns)); + + const { gridToData } = mappingResult.current; + + const { result } = renderHook(() => + useGridCellContent( + data, + columns, + gridToData, + dateTime, + aggregates, + rowHeaders, + ), + ); + + return result.current; +}; diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts similarity index 95% rename from webapp/src/components/common/MatrixGrid/useGridCellContent.ts rename to webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts index a5b1843cf8..5cbd61a440 100644 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts @@ -18,19 +18,10 @@ import { type EnhancedGridColumn, type ColumnType, MatrixAggregates, - Column, -} from "./types"; -import { formatNumber } from "./utils"; - -type CellContentGenerator = ( - row: number, - col: number, - column: EnhancedGridColumn, - data: number[][], - dateTime?: string[], - aggregates?: Partial, - rowHeaders?: string[], -) => GridCell; +} from "../../core/types"; +import { formatNumber } from "../../core/utils"; +import { Column } from "../../core/constants"; +import { type CellContentGenerator } from "./types"; /** * Map of cell content generators for each column type. diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts new file mode 100644 index 0000000000..445bf7ee3d --- /dev/null +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024, 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 { GridCell, Item } from "@glideapps/glide-data-grid"; +import type { EnhancedGridColumn, MatrixAggregates } from "../../core/types"; + +export type GridToDataFunction = (cell: Item) => Item | null; + +export interface UseGridCellContentOptions { + data: number[][]; + columns: EnhancedGridColumn[]; + gridToData: GridToDataFunction; + dateTime?: string[]; + aggregates?: MatrixAggregates; + rowHeaders?: string[]; +} + +export type CellContentGenerator = ( + row: number, + col: number, + column: EnhancedGridColumn, + data: number[][], + dateTime?: string[], + aggregates?: Partial, + rowHeaders?: string[], +) => GridCell; + +export interface RenderOptions { + data: number[][]; + columns: EnhancedGridColumn[]; + dateTime?: string[]; + aggregates?: MatrixAggregates; + rowHeaders?: string[]; +} + +export interface TestCase { + desc: string; + value: number; + expected: string; +} diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts similarity index 92% rename from webapp/src/components/common/MatrixGrid/useMatrix.ts rename to webapp/src/components/common/Matrix/hooks/useMatrix/index.ts index 29908d97a2..7164751225 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.ts +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts @@ -16,13 +16,13 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { AxiosError } from "axios"; import { enqueueSnackbar } from "notistack"; import { t } from "i18next"; -import { MatrixIndex } from "../../../common/types"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; +import { MatrixIndex } from "../../../../../common/types"; +import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar"; import { getStudyMatrixIndex, updateMatrix, -} from "../../../services/api/matrix"; -import { getStudyData } from "../../../services/api/study"; +} from "../../../../../services/api/matrix"; +import { getStudyData } from "../../../../../services/api/study"; import { EnhancedGridColumn, MatrixDataDTO, @@ -30,22 +30,20 @@ import { MatrixUpdateDTO, MatrixAggregates, AggregateConfig, - Column, - Operation, - Aggregate, -} from "./types"; +} from "../../core/types"; import { - aggregatesTheme, calculateMatrixAggregates, generateDataColumns, generateDateTime, getAggregateTypes, -} from "./utils"; +} from "../../core/utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; -import { importFile } from "../../../services/api/studies/raw"; -import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; -import usePrompt from "../../../hooks/usePrompt"; +import { importFile } from "../../../../../services/api/studies/raw"; +import { fetchMatrixFn } from "../../../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; +import usePrompt from "../../../../../hooks/usePrompt"; +import { Aggregate, Column, Operation } from "../../core/constants"; +import { aggregatesTheme } from "../../components/MatrixGrid/styles"; interface DataState { data: MatrixDataDTO["data"]; diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx similarity index 69% rename from webapp/src/components/common/MatrixGrid/useMatrix.test.tsx rename to webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx index 09bdce7eff..33c36fcbcf 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx @@ -13,98 +13,95 @@ */ import { renderHook, act, waitFor } from "@testing-library/react"; -import { vi, describe, expect, beforeEach } from "vitest"; -import { useMatrix } from "./useMatrix"; -import * as apiMatrix from "../../../services/api/matrix"; -import * as apiStudy from "../../../services/api/study"; -import * as rawStudy from "../../../services/api/studies/raw"; +import { useMatrix } from "../useMatrix"; +import * as apiMatrix from "@/services/api/matrix"; +import * as apiStudy from "@/services/api/study"; +import * as rawStudy from "@/services/api/studies/raw"; import { MatrixEditDTO, MatrixIndex, Operator, StudyOutputDownloadLevelDTO, -} from "../../../common/types"; -import { GridUpdate, MatrixDataDTO } from "./types"; +} from "@/common/types"; +import { GridUpdate, MatrixDataDTO } from "../../core/types"; import { GridCellKind } from "@glideapps/glide-data-grid"; -// Mock external dependencies -vi.mock("../../../services/api/matrix"); -vi.mock("../../../services/api/study"); -vi.mock("../../../services/api/studies/raw"); -vi.mock("../../../hooks/usePrompt"); - -describe("useMatrix", () => { - // Test constants and fixtures - const DATA = { - studyId: "study123", - url: "https://studies/study123/matrix", - matrixData: { - data: [ - [1, 2], - [3, 4], - ], - columns: [0, 1], - index: [0, 1], - } as MatrixDataDTO, - matrixIndex: { - start_date: "2023-01-01", - steps: 2, - first_week_size: 7, - level: StudyOutputDownloadLevelDTO.DAILY, - } as MatrixIndex, - }; - - // Helper functions - const createGridUpdate = ( - row: number, - col: number, - value: number, - ): GridUpdate => ({ - coordinates: [row, col], - value: { - kind: GridCellKind.Number, - data: value, - displayData: value.toString(), - allowOverlay: true, - }, - }); +vi.mock("@/services/api/matrix"); +vi.mock("@/services/api/study"); +vi.mock("@/services/api/studies/raw"); +vi.mock("@/hooks/usePrompt"); + +// TODO: refactor fixtures, utils functions, and types in dedicated files +const DATA = { + studyId: "study123", + url: "https://studies/study123/matrix", + matrixData: { + data: [ + [1, 2], + [3, 4], + ], + columns: [0, 1], + index: [0, 1], + } as MatrixDataDTO, + matrixIndex: { + start_date: "2023-01-01", + steps: 2, + first_week_size: 7, + level: StudyOutputDownloadLevelDTO.DAILY, + } as MatrixIndex, +}; + +const createGridUpdate = ( + row: number, + col: number, + value: number, +): GridUpdate => ({ + coordinates: [row, col], + value: { + kind: GridCellKind.Number, + data: value, + displayData: value.toString(), + allowOverlay: true, + }, +}); - interface SetupOptions { - mockData?: MatrixDataDTO; - mockIndex?: MatrixIndex; - } +interface SetupOptions { + mockData?: MatrixDataDTO; + mockIndex?: MatrixIndex; +} - const setupHook = async ({ - mockData = DATA.matrixData, - mockIndex = DATA.matrixIndex, - }: SetupOptions = {}) => { - vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockData); - vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockIndex); +const setupHook = async ({ + mockData = DATA.matrixData, + mockIndex = DATA.matrixIndex, +}: SetupOptions = {}) => { + vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockData); + vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockIndex); - const hook = renderHook(() => - useMatrix(DATA.studyId, DATA.url, true, true, true), - ); + const hook = renderHook(() => + useMatrix(DATA.studyId, DATA.url, true, true, true), + ); - await waitFor(() => { - expect(hook.result.current.isLoading).toBe(false); - }); + await waitFor(() => { + expect(hook.result.current.isLoading).toBe(false); + }); - return hook; - }; - - const performEdit = async ( - hook: Awaited>, - updates: GridUpdate | GridUpdate[], - ) => { - act(() => { - if (Array.isArray(updates)) { - hook.result.current.handleMultipleCellsEdit(updates); - } else { - hook.result.current.handleCellEdit(updates); - } - }); - }; + return hook; +}; + +const performEdit = async ( + hook: Awaited>, + updates: GridUpdate | GridUpdate[], +) => { + act(() => { + if (Array.isArray(updates)) { + hook.result.current.handleMultipleCellsEdit(updates); + } else { + hook.result.current.handleCellEdit(updates); + } + }); +}; +describe("useMatrix", () => { beforeEach(() => { vi.clearAllMocks(); }); diff --git a/webapp/src/components/common/MatrixGrid/useMatrixPortal.ts b/webapp/src/components/common/Matrix/hooks/useMatrixPortal/index.ts similarity index 100% rename from webapp/src/components/common/MatrixGrid/useMatrixPortal.ts rename to webapp/src/components/common/Matrix/hooks/useMatrixPortal/index.ts diff --git a/webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx b/webapp/src/components/common/Matrix/hooks/useMatrixPortal/useMatrixPortal.test.tsx similarity index 85% rename from webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx rename to webapp/src/components/common/Matrix/hooks/useMatrixPortal/useMatrixPortal.test.tsx index 638214349a..8b3bdfda32 100644 --- a/webapp/src/components/common/MatrixGrid/useMatrixPortal.test.tsx +++ b/webapp/src/components/common/Matrix/hooks/useMatrixPortal/useMatrixPortal.test.tsx @@ -12,17 +12,14 @@ * This file is part of the Antares project. */ -import { describe, it, expect, beforeEach } from "vitest"; import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { useMatrixPortal } from "./useMatrixPortal"; +import { useMatrixPortal } from "../useMatrixPortal"; -// Test component without ref function NonRefComponent() { return
Test Container
; } -// Test component with ref function TestComponent() { const { containerRef, handleMouseEnter, handleMouseLeave } = useMatrixPortal(); @@ -40,21 +37,21 @@ function TestComponent() { describe("useMatrixPortal", () => { beforeEach(() => { - cleanup(); // Clean up after each test + cleanup(); const existingPortal = document.getElementById("portal"); if (existingPortal) { existingPortal.remove(); } }); - it("should create hidden portal initially", () => { + test("should create hidden portal initially", () => { render(); const portal = document.getElementById("portal"); expect(portal).toBeInTheDocument(); expect(portal?.style.display).toBe("none"); }); - it("should show portal with correct styles when mouse enters", async () => { + test("should show portal with correct styles when mouse enters", async () => { const user = userEvent.setup(); render(); @@ -70,7 +67,7 @@ describe("useMatrixPortal", () => { expect(portal?.style.zIndex).toBe("9999"); }); - it("should hide portal when mouse leaves", async () => { + test("should hide portal when mouse leaves", async () => { const user = userEvent.setup(); render(); @@ -85,7 +82,7 @@ describe("useMatrixPortal", () => { expect(document.getElementById("portal")?.style.display).toBe("none"); }); - it("should keep portal hidden if containerRef is null", async () => { + test("should keep portal hidden if containerRef is null", async () => { const user = userEvent.setup(); // First render test component to create portal @@ -104,7 +101,7 @@ describe("useMatrixPortal", () => { expect(portal?.style.display).toBe("none"); }); - it("should handle multiple mouse enter/leave cycles", async () => { + test("should handle multiple mouse enter/leave cycles", async () => { const user = userEvent.setup(); render(); @@ -126,7 +123,7 @@ describe("useMatrixPortal", () => { expect(portal?.style.display).toBe("none"); }); - it("should handle rapid mouse events", async () => { + test("should handle rapid mouse events", async () => { const user = userEvent.setup(); render(); @@ -141,7 +138,7 @@ describe("useMatrixPortal", () => { expect(portal?.style.display).toBe("block"); }); - it("should maintain portal existence across multiple component instances", () => { + test("should maintain portal existence across multiple component instances", () => { // Render first instance const { unmount } = render(); expect(document.getElementById("portal")).toBeInTheDocument(); diff --git a/webapp/src/components/common/MatrixGrid/Matrix.tsx b/webapp/src/components/common/Matrix/index.tsx similarity index 95% rename from webapp/src/components/common/MatrixGrid/Matrix.tsx rename to webapp/src/components/common/Matrix/index.tsx index 101cb961b8..7c96919466 100644 --- a/webapp/src/components/common/MatrixGrid/Matrix.tsx +++ b/webapp/src/components/common/Matrix/index.tsx @@ -13,18 +13,18 @@ */ import { Divider, Skeleton } from "@mui/material"; -import MatrixGrid from "."; -import { useMatrix } from "./useMatrix"; +import MatrixGrid from "./components/MatrixGrid"; +import { useMatrix } from "./hooks/useMatrix"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import ImportDialog from "../dialogs/ImportDialog"; import { useOutletContext } from "react-router"; import { StudyMetadata } from "../../../common/types"; -import { MatrixContainer, MatrixHeader, MatrixTitle } from "./style"; -import MatrixActions from "./MatrixActions"; +import { MatrixContainer, MatrixHeader, MatrixTitle } from "./styles"; +import MatrixActions from "./components/MatrixActions"; import EmptyView from "../page/SimpleContent"; import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; -import { AggregateConfig } from "./types"; +import { AggregateConfig } from "./core/types"; interface MatrixProps { url: string; diff --git a/webapp/src/components/common/MatrixGrid/style.ts b/webapp/src/components/common/Matrix/styles.ts similarity index 100% rename from webapp/src/components/common/MatrixGrid/style.ts rename to webapp/src/components/common/Matrix/styles.ts diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts deleted file mode 100644 index 0a1c75d87c..0000000000 --- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Copyright (c) 2024, 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 { renderHook } from "@testing-library/react"; -import { useGridCellContent } from "./useGridCellContent"; -import { - Column, - type ColumnType, - type MatrixAggregates, - type EnhancedGridColumn, -} from "./types"; -import { useColumnMapping } from "./useColumnMapping"; -import { type Item } from "@glideapps/glide-data-grid"; - -describe("useGridCellContent", () => { - // Test data factories - const createColumn = ( - id: string, - type: ColumnType, - editable = false, - ): EnhancedGridColumn => ({ - id, - title: id.charAt(0).toUpperCase() + id.slice(1), - type, - width: type === Column.DateTime ? 150 : 50, - editable, - }); - - const createCoordinate = (col: number, row: number): Item => - [col, row] as Item; - - interface RenderOptions { - data: number[][]; - columns: EnhancedGridColumn[]; - dateTime?: string[]; - aggregates?: MatrixAggregates; - rowHeaders?: string[]; - } - - const renderGridCellContent = ({ - data, - columns, - dateTime, - aggregates, - rowHeaders, - }: RenderOptions) => { - const { result: mappingResult } = renderHook(() => - useColumnMapping(columns), - ); - - const { gridToData } = mappingResult.current; - - const { result } = renderHook(() => - useGridCellContent( - data, - columns, - gridToData, - dateTime, - aggregates, - rowHeaders, - ), - ); - - return result.current; - }; - - const assertNumberCell = ( - cell: ReturnType>, - expectedValue: number | undefined, - message: string, - ) => { - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(expectedValue); - } else { - throw new Error(message); - } - }; - - const assertTextCell = ( - cell: ReturnType>, - expectedValue: string, - message: string, - ) => { - if (cell.kind === "text" && "displayData" in cell) { - expect(cell.displayData).toBe(expectedValue); - } else { - throw new Error(message); - } - }; - - describe("Aggregate columns", () => { - const DATA = { - columns: [createColumn("total", Column.Aggregate)], - data: [ - [10, 20, 30], - [15, 25, 35], - [5, 15, 25], - ], - aggregates: { - min: [5, 15, 25], - max: [15, 25, 35], - avg: [10, 20, 30], - total: [30, 60, 90], - }, - }; - - test.each([ - [0, 30], - [1, 60], - [2, 90], - ])("returns correct aggregate for row %i", (row, expected) => { - const getCellContent = renderGridCellContent(DATA); - const cell = getCellContent(createCoordinate(0, row)); - assertNumberCell( - cell, - expected, - `Expected aggregate value ${expected} at row ${row}`, - ); - }); - }); - - describe("Mixed column types", () => { - const DATA = { - columns: [ - createColumn("rowHeader", Column.Text), - createColumn("date", Column.DateTime), - createColumn("data1", Column.Number, true), - createColumn("data2", Column.Number, true), - createColumn("total", Column.Aggregate), - ], - data: [ - [100, 200], - [150, 250], - ], - dateTime: ["2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"], - rowHeaders: ["Row 1", "Row 2"], - aggregates: { - min: [100, 200], - max: [150, 250], - avg: [125, 225], - total: [250, 450], - }, - }; - - test("handles all column types correctly", () => { - const getCellContent = renderGridCellContent(DATA); - - // Text column (Row header) - assertTextCell( - getCellContent(createCoordinate(0, 0)), - "Row 1", - "Expected row header text", - ); - - // Number columns - assertNumberCell( - getCellContent(createCoordinate(2, 0)), - 100, - "Expected first data column value", - ); - assertNumberCell( - getCellContent(createCoordinate(3, 0)), - 200, - "Expected second data column value", - ); - - // Aggregate column - assertNumberCell( - getCellContent(createCoordinate(4, 0)), - 250, - "Expected aggregate value", - ); - }); - }); - - describe("Edge cases", () => { - test("handles empty data array", () => { - const options = { - columns: [createColumn("data1", Column.Number, true)], - data: [], - }; - - const getCellContent = renderGridCellContent(options); - const cell = getCellContent(createCoordinate(0, 0)); - assertNumberCell(cell, undefined, "Expected undefined for empty data"); - }); - - test("handles out of bounds access", () => { - const options = { - columns: [createColumn("data1", Column.Number, true)], - data: [[100]], - }; - - const getCellContent = renderGridCellContent(options); - - // Column out of bounds - const colCell = getCellContent(createCoordinate(1, 0)); - assertTextCell(colCell, "N/A", "Expected N/A for column out of bounds"); - - // Row out of bounds - const rowCell = getCellContent(createCoordinate(0, 1)); - assertNumberCell( - rowCell, - undefined, - "Expected undefined for row out of bounds", - ); - }); - }); - - describe("Number formatting", () => { - const formatTestCases = [ - { - desc: "formats regular numbers", - value: 1234567.89, - expected: "1 234 567.89", - }, - { - desc: "handles very large numbers", - value: 1e20, - expected: "100 000 000 000 000 000 000", - }, - { - desc: "handles very small numbers", - value: 0.00001, - expected: "0.00001", - }, - { - desc: "handles negative numbers", - value: -1234567.89, - expected: "-1 234 567.89", - }, - { - desc: "handles zero", - value: 0, - expected: "0", - }, - ]; - - test.each(formatTestCases)("$desc", ({ value, expected }) => { - const options = { - columns: [createColumn("number", Column.Number, true)], - data: [[value]], - }; - - const getCellContent = renderGridCellContent(options); - const cell = getCellContent(createCoordinate(0, 0)); - - if (cell.kind === "number" && "data" in cell) { - expect(cell.data).toBe(value); - expect(cell.displayData).toBe(expected); - } else { - throw new Error(`Expected formatted number cell for value ${value}`); - } - }); - }); -}); diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts deleted file mode 100644 index f39fb513e0..0000000000 --- a/webapp/src/components/common/MatrixGrid/utils.test.ts +++ /dev/null @@ -1,350 +0,0 @@ -/** - * Copyright (c) 2024, 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 { Aggregate, Column, TimeFrequency } from "./types"; -import { - calculateMatrixAggregates, - formatNumber, - generateCustomColumns, - generateDateTime, - generateTimeSeriesColumns, - getAggregateTypes, -} from "./utils"; - -// Mock date-fns for consistent date formatting -vi.mock("date-fns", async () => { - const actual = (await vi.importActual( - "date-fns", - )) as typeof import("date-fns"); - return { - ...actual, - format: vi.fn((date: Date, formatString: string) => { - if (formatString.includes("ww")) { - const weekNumber = actual.getWeek(date); - return `W ${weekNumber.toString().padStart(2, "0")}`; - } - return actual.format(date, formatString); - }), - }; -}); - -describe("Matrix Utils", () => { - // Test data and helpers - const TEST_DATA = { - dateConfig: { - start_date: "2023-01-01 00:00:00", - steps: 3, - first_week_size: 7, - }, - matrix: [ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - ], - }; - - beforeAll(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2023-01-01 00:00:00")); - }); - - describe("DateTime Generation", () => { - const dateTimeTestCases = [ - { - name: "annual format", - config: { ...TEST_DATA.dateConfig, level: TimeFrequency.Annual }, - expected: [ - "global.time.annual", - "global.time.annual", - "global.time.annual", - ], - }, - { - name: "monthly format", - config: { ...TEST_DATA.dateConfig, level: TimeFrequency.Monthly }, - expected: ["Jan", "Feb", "Mar"], - }, - { - name: "weekly format", - config: { - ...TEST_DATA.dateConfig, - level: TimeFrequency.Weekly, - first_week_size: 1, - }, - expected: ["W 01", "W 02", "W 03"], - }, - { - name: "daily format", - config: { ...TEST_DATA.dateConfig, level: TimeFrequency.Daily }, - expected: ["Sun 1 Jan", "Mon 2 Jan", "Tue 3 Jan"], - }, - { - name: "hourly format", - config: { - start_date: "2039-07-01 00:00:00", - steps: 3, - first_week_size: 7, - level: TimeFrequency.Hourly, - }, - expected: ["Fri 1 Jul 00:00", "Fri 1 Jul 01:00", "Fri 1 Jul 02:00"], - }, - ]; - - test.each(dateTimeTestCases)( - "generates correct $name", - ({ config, expected }) => { - const result = generateDateTime(config); - expect(result).toEqual(expected); - }, - ); - }); - - describe("Time Series Column Generation", () => { - const columnTestCases = [ - { - name: "default options", - input: { count: 3 }, - expectedLength: 3, - validate: (result: ReturnType) => { - expect(result[0]).toEqual({ - id: "data1", - title: "TS 1", - type: Column.Number, - style: "normal", - editable: true, - }); - }, - }, - { - name: "custom options", - input: { count: 2, startIndex: 10, prefix: "Data", editable: false }, - expectedLength: 2, - validate: (result: ReturnType) => { - expect(result[0]).toEqual({ - id: "data10", - title: "Data 10", - type: Column.Number, - style: "normal", - editable: false, - }); - }, - }, - { - name: "zero count", - input: { count: 0 }, - expectedLength: 0, - validate: (result: ReturnType) => { - expect(result).toEqual([]); - }, - }, - { - name: "large count", - input: { count: 1000 }, - expectedLength: 1000, - validate: (result: ReturnType) => { - expect(result[999].id).toBe("data1000"); - expect(result[999].title).toBe("TS 1000"); - }, - }, - ]; - - test.each(columnTestCases)( - "handles $name correctly", - ({ input, expectedLength, validate }) => { - const result = generateTimeSeriesColumns(input); - expect(result).toHaveLength(expectedLength); - validate(result); - }, - ); - }); - - describe("Matrix Aggregates Calculation", () => { - const aggregateTestCases = [ - { - name: "simple matrix with all aggregates", - matrix: TEST_DATA.matrix, - aggregates: "all" as const, - expected: { - min: [1, 4, 7], - max: [3, 6, 9], - avg: [2, 5, 8], - total: [6, 15, 24], - }, - }, - { - name: "decimal numbers", - matrix: [ - [1.1, 2.2, 3.3], - [4.4, 5.5, 6.6], - ], - aggregates: "all" as const, - expected: { - min: [1.1, 4.4], - max: [3.3, 6.6], - avg: [2, 6], - total: [7, 17], - }, - }, - { - name: "negative numbers", - matrix: [ - [-1, -2, -3], - [-4, 0, 4], - ], - aggregates: "all" as const, - expected: { - min: [-3, -4], - max: [-1, 4], - avg: [-2, 0], - total: [-6, 0], - }, - }, - ]; - - test.each(aggregateTestCases)( - "calculates $name correctly", - ({ matrix, aggregates, expected }) => { - const aggregatesTypes = getAggregateTypes(aggregates); - const result = calculateMatrixAggregates(matrix, aggregatesTypes); - expect(result).toEqual(expected); - }, - ); - }); - - describe("Number Formatting", () => { - interface FormatTestCase { - description: string; - value: number | undefined; - maxDecimals?: number; - expected: string; - } - - const formatTestCases: Array<{ name: string; cases: FormatTestCase[] }> = [ - { - name: "integer numbers", - cases: [ - { - description: "formats large number", - value: 1234567, - expected: "1 234 567", - }, - { - description: "formats million", - value: 1000000, - expected: "1 000 000", - }, - { description: "formats single digit", value: 1, expected: "1" }, - ], - }, - { - name: "decimal numbers", - cases: [ - { - description: "formats with 2 decimals", - value: 1234.56, - maxDecimals: 2, - expected: "1 234.56", - }, - { - description: "formats with 3 decimals", - value: 1000000.123, - maxDecimals: 3, - expected: "1 000 000.123", - }, - ], - }, - { - name: "special cases", - cases: [ - { - description: "handles undefined", - value: undefined, - maxDecimals: 3, - expected: "", - }, - { description: "handles zero", value: 0, expected: "0" }, - { - description: "handles negative", - value: -1234567, - expected: "-1 234 567", - }, - { - description: "handles very large number", - value: 1e20, - expected: "100 000 000 000 000 000 000", - }, - ], - }, - ]; - - describe.each(formatTestCases)("$name", ({ cases }) => { - test.each(cases)("$description", ({ value, maxDecimals, expected }) => { - expect(formatNumber({ value, maxDecimals })).toBe(expected); - }); - }); - }); - - describe("Custom Column Generation", () => { - test("generates columns with correct properties", () => { - const titles = ["Custom 1", "Custom 2", "Custom 3"]; - const width = 100; - - const result = generateCustomColumns({ titles, width }); - expect(result).toHaveLength(3); - - result.forEach((column, index) => { - expect(column).toEqual({ - id: `custom${index + 1}`, - title: titles[index], - type: Column.Number, - style: "normal", - width, - editable: true, - }); - }); - }); - }); - - describe("Aggregate Type Configuration", () => { - const aggregateConfigCases = [ - { - name: "stats configuration", - aggregates: "stats" as const, - expected: [Aggregate.Avg, Aggregate.Min, Aggregate.Max], - }, - { - name: "all configuration", - aggregates: "all" as const, - expected: [ - Aggregate.Min, - Aggregate.Max, - Aggregate.Avg, - Aggregate.Total, - ], - }, - { - name: "custom configuration", - aggregates: [Aggregate.Min, Aggregate.Max], - expected: [Aggregate.Min, Aggregate.Max], - }, - ]; - - test.each(aggregateConfigCases)( - "handles $name correctly", - ({ aggregates, expected }) => { - expect(getAggregateTypes(aggregates)).toEqual(expected); - }, - ); - }); -}); diff --git a/webapp/src/services/api/matrix.ts b/webapp/src/services/api/matrix.ts index e7d6bee7a5..f50df6b926 100644 --- a/webapp/src/services/api/matrix.ts +++ b/webapp/src/services/api/matrix.ts @@ -24,7 +24,7 @@ import { } from "../../common/types"; import { FileDownloadTask } from "./downloads"; import { getConfig } from "../config"; -import { MatrixUpdateDTO } from "../../components/common/MatrixGrid/types"; +import { MatrixUpdateDTO } from "../../components/common/Matrix/core/types"; export const getMatrixList = async ( name = "", diff --git a/webapp/src/utils/validation/array.test.ts b/webapp/src/utils/validation/__tests__/array.test.ts similarity index 98% rename from webapp/src/utils/validation/array.test.ts rename to webapp/src/utils/validation/__tests__/array.test.ts index 406c853548..d05086e7a8 100644 --- a/webapp/src/utils/validation/array.test.ts +++ b/webapp/src/utils/validation/__tests__/array.test.ts @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { validateArray } from "./array"; +import { validateArray } from "../array"; vi.mock("i18next", () => ({ t: vi.fn((key) => key), diff --git a/webapp/src/utils/validation/number.test.ts b/webapp/src/utils/validation/__tests__/number.test.ts similarity index 98% rename from webapp/src/utils/validation/number.test.ts rename to webapp/src/utils/validation/__tests__/number.test.ts index eb19efcafe..a0377a921f 100644 --- a/webapp/src/utils/validation/number.test.ts +++ b/webapp/src/utils/validation/__tests__/number.test.ts @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { validateNumber } from "./number"; +import { validateNumber } from "../number"; // Mock i18next vi.mock("i18next", () => ({ diff --git a/webapp/src/utils/validation/string.test.ts b/webapp/src/utils/validation/__tests__/string.test.ts similarity index 98% rename from webapp/src/utils/validation/string.test.ts rename to webapp/src/utils/validation/__tests__/string.test.ts index dfc164cc36..c6aede434d 100644 --- a/webapp/src/utils/validation/string.test.ts +++ b/webapp/src/utils/validation/__tests__/string.test.ts @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { validatePassword, validateString } from "./string"; +import { validatePassword, validateString } from "../string"; vi.mock("i18next", () => ({ t: vi.fn((key, options) => { From d71e4a0e628860ac208cb45e037b3205d2514748 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Fri, 25 Oct 2024 17:16:36 +0200 Subject: [PATCH 39/43] build(vite): exclude test files from production builds --- webapp/vite.config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index 73b06c2ba0..ba4fe1e0f4 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -30,6 +30,13 @@ export default defineConfig(({ mode }) => { // Not working in dev without `JSON.stringify` __BUILD_TIMESTAMP__: JSON.stringify(Date.now()), }, + build: { + // Exclude test files and directories from production builds + // This improves build performance and reduces bundle size + rollupOptions: { + external: ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"], + }, + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), // Relative imports from the src directory From 212c777e8c7d10106e1a2dbbcc131bea39bc5a3d Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 29 Oct 2024 09:49:23 +0100 Subject: [PATCH 40/43] refactor(ui): rename core folder --- .../explore/Modelization/Areas/Hydro/Allocation/utils.ts | 2 +- .../explore/Modelization/Areas/Hydro/Correlation/utils.ts | 2 +- .../Singlestudy/explore/Modelization/Areas/Hydro/utils.ts | 2 +- .../App/Singlestudy/explore/Results/ResultDetails/index.tsx | 4 ++-- .../common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx | 4 ++-- .../common/Matrix/components/MatrixGrid/index.tsx | 2 +- .../Matrix/hooks/useColumnMapping/__tests__/fixtures.ts | 2 +- .../common/Matrix/hooks/useColumnMapping/__tests__/utils.ts | 4 ++-- .../common/Matrix/hooks/useColumnMapping/index.ts | 4 ++-- .../Matrix/hooks/useGridCellContent/__tests__/fixtures.ts | 4 ++-- .../useGridCellContent/__tests__/useGridCellContent.test.ts | 2 +- .../common/Matrix/hooks/useGridCellContent/index.ts | 6 +++--- .../common/Matrix/hooks/useGridCellContent/types.ts | 2 +- .../src/components/common/Matrix/hooks/useMatrix/index.ts | 6 +++--- .../common/Matrix/hooks/useMatrix/useMatrix.test.tsx | 2 +- webapp/src/components/common/Matrix/index.tsx | 2 +- .../common/Matrix/{core => shared}/__tests__/fixtures.ts | 0 .../common/Matrix/{core => shared}/__tests__/types.ts | 0 .../common/Matrix/{core => shared}/__tests__/utils.test.ts | 0 .../components/common/Matrix/{core => shared}/constants.ts | 0 .../src/components/common/Matrix/{core => shared}/types.ts | 0 .../src/components/common/Matrix/{core => shared}/utils.ts | 2 +- webapp/src/services/api/matrix.ts | 2 +- 23 files changed, 27 insertions(+), 27 deletions(-) rename webapp/src/components/common/Matrix/{core => shared}/__tests__/fixtures.ts (100%) rename webapp/src/components/common/Matrix/{core => shared}/__tests__/types.ts (100%) rename webapp/src/components/common/Matrix/{core => shared}/__tests__/utils.test.ts (100%) rename webapp/src/components/common/Matrix/{core => shared}/constants.ts (100%) rename webapp/src/components/common/Matrix/{core => shared}/types.ts (100%) rename webapp/src/components/common/Matrix/{core => shared}/utils.ts (99%) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts index 96d48cb669..5d5d054a11 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Allocation/utils.ts @@ -14,7 +14,7 @@ import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; -import { MatrixDataDTO } from "../../../../../../../common/Matrix/core/types"; +import { MatrixDataDTO } from "../../../../../../../common/Matrix/shared/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts index 0c538682bc..253963b1c6 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/Correlation/utils.ts @@ -14,7 +14,7 @@ import { StudyMetadata, Area } from "../../../../../../../../common/types"; import client from "../../../../../../../../services/api/client"; -import { MatrixDataDTO } from "../../../../../../../common/Matrix/core/types"; +import { MatrixDataDTO } from "../../../../../../../common/Matrix/shared/types"; import { AreaCoefficientItem } from "../utils"; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 735fecd13f..950dc891f2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -15,7 +15,7 @@ import { MatrixDataDTO, AggregateConfig, -} from "../../../../../../common/Matrix/core/types"; +} from "../../../../../../common/Matrix/shared/types"; import { SplitViewProps } from "../../../../../../common/SplitView"; import { getAllocationMatrix } from "./Allocation/utils"; import { getCorrelationMatrix } from "./Correlation/utils"; diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index ea82685854..b252e4196a 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -57,8 +57,8 @@ import MatrixGrid from "../../../../../common/Matrix/components/MatrixGrid/index import { generateCustomColumns, generateDateTime, -} from "../../../../../common/Matrix/core/utils.ts"; -import { Column } from "@/components/common/Matrix/core/constants.ts"; +} from "../../../../../common/Matrix/shared/utils.ts"; +import { Column } from "@/components/common/Matrix/shared/constants.ts"; import SplitView from "../../../../../common/SplitView/index.tsx"; import ResultFilters from "./ResultFilters.tsx"; import { toError } from "../../../../../../utils/fnUtils.ts"; diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx index 5a9325f7f9..7f3f44aa7b 100644 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/MatrixGrid.test.tsx @@ -17,10 +17,10 @@ import userEvent from "@testing-library/user-event"; import Box from "@mui/material/Box"; import MatrixGrid, { MatrixGridProps } from "."; import SplitView from "../../../SplitView"; -import type { EnhancedGridColumn } from "../../core/types"; +import type { EnhancedGridColumn } from "../../shared/types"; import { mockGetBoundingClientRect } from "../../../../../tests/mocks/mockGetBoundingClientRect"; import { mockHTMLCanvasElement } from "../../../../../tests/mocks/mockHTMLCanvasElement"; -import { Column } from "../../core/constants"; +import { Column } from "../../shared/constants"; interface RenderMatrixOptions { width?: string; diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx index 73395dd7bc..71b06e0976 100644 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx @@ -28,7 +28,7 @@ import { type EnhancedGridColumn, type GridUpdate, type MatrixAggregates, -} from "../../core/types"; +} from "../../shared/types"; import { useColumnMapping } from "../../hooks/useColumnMapping"; import { useMatrixPortal } from "../../hooks/useMatrixPortal"; import { darkTheme, readOnlyDarkTheme } from "./styles"; diff --git a/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts index af74b0f068..7573b84251 100644 --- a/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/fixtures.ts @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { Column } from "../../../core/constants"; +import { Column } from "../../../shared/constants"; import { createColumn, createNumericColumn } from "./utils"; export const COLUMNS = { diff --git a/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts index 8425d789ff..eaf97da516 100644 --- a/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/__tests__/utils.ts @@ -14,9 +14,9 @@ import { Item } from "@glideapps/glide-data-grid"; import { renderHook } from "@testing-library/react"; -import type { ColumnType, EnhancedGridColumn } from "../../../core/types"; +import type { ColumnType, EnhancedGridColumn } from "../../../shared/types"; import { useColumnMapping } from ".."; -import { Column } from "../../../core/constants"; +import { Column } from "../../../shared/constants"; export const createCoordinate = (col: number, row: number): Item => [col, row] as Item; diff --git a/webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts b/webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts index 1c10019ca7..b9ceb95ebf 100644 --- a/webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useColumnMapping/index.ts @@ -14,8 +14,8 @@ import { useMemo } from "react"; import { Item } from "@glideapps/glide-data-grid"; -import { EnhancedGridColumn } from "../../core/types"; -import { Column } from "../../core/constants"; +import { EnhancedGridColumn } from "../../shared/types"; +import { Column } from "../../shared/constants"; /** * A custom hook that provides coordinate mapping functions for a grid with mixed column types. diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts index e9331696cb..c13aa938c5 100644 --- a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/fixtures.ts @@ -12,8 +12,8 @@ * This file is part of the Antares project. */ -import type { ColumnType, EnhancedGridColumn } from "../../../core/types"; -import { Column } from "../../../core/constants"; +import type { ColumnType, EnhancedGridColumn } from "../../../shared/types"; +import { Column } from "../../../shared/constants"; import type { TestCase } from "../types"; export const createColumn = ( diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts index 346d63b347..e2103c53e7 100644 --- a/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/__tests__/useGridCellContent.test.ts @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { Column } from "../../../core/constants"; +import { Column } from "../../../shared/constants"; import { createColumn, AGGREGATE_DATA, diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts index 5cbd61a440..0c36df736b 100644 --- a/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts @@ -18,9 +18,9 @@ import { type EnhancedGridColumn, type ColumnType, MatrixAggregates, -} from "../../core/types"; -import { formatNumber } from "../../core/utils"; -import { Column } from "../../core/constants"; +} from "../../shared/types"; +import { formatNumber } from "../../shared/utils"; +import { Column } from "../../shared/constants"; import { type CellContentGenerator } from "./types"; /** diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts index 445bf7ee3d..9858e11beb 100644 --- a/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/types.ts @@ -13,7 +13,7 @@ */ import { GridCell, Item } from "@glideapps/glide-data-grid"; -import type { EnhancedGridColumn, MatrixAggregates } from "../../core/types"; +import type { EnhancedGridColumn, MatrixAggregates } from "../../shared/types"; export type GridToDataFunction = (cell: Item) => Item | null; diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts index 7164751225..b781a9400c 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts @@ -30,19 +30,19 @@ import { MatrixUpdateDTO, MatrixAggregates, AggregateConfig, -} from "../../core/types"; +} from "../../shared/types"; import { calculateMatrixAggregates, generateDataColumns, generateDateTime, getAggregateTypes, -} from "../../core/utils"; +} from "../../shared/utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; import { importFile } from "../../../../../services/api/studies/raw"; import { fetchMatrixFn } from "../../../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; import usePrompt from "../../../../../hooks/usePrompt"; -import { Aggregate, Column, Operation } from "../../core/constants"; +import { Aggregate, Column, Operation } from "../../shared/constants"; import { aggregatesTheme } from "../../components/MatrixGrid/styles"; interface DataState { diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx index 33c36fcbcf..7dabcb97a5 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx @@ -23,7 +23,7 @@ import { Operator, StudyOutputDownloadLevelDTO, } from "@/common/types"; -import { GridUpdate, MatrixDataDTO } from "../../core/types"; +import { GridUpdate, MatrixDataDTO } from "../../shared/types"; import { GridCellKind } from "@glideapps/glide-data-grid"; vi.mock("@/services/api/matrix"); diff --git a/webapp/src/components/common/Matrix/index.tsx b/webapp/src/components/common/Matrix/index.tsx index 7c96919466..014703be76 100644 --- a/webapp/src/components/common/Matrix/index.tsx +++ b/webapp/src/components/common/Matrix/index.tsx @@ -24,7 +24,7 @@ import { MatrixContainer, MatrixHeader, MatrixTitle } from "./styles"; import MatrixActions from "./components/MatrixActions"; import EmptyView from "../page/SimpleContent"; import { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; -import { AggregateConfig } from "./core/types"; +import { AggregateConfig } from "./shared/types"; interface MatrixProps { url: string; diff --git a/webapp/src/components/common/Matrix/core/__tests__/fixtures.ts b/webapp/src/components/common/Matrix/shared/__tests__/fixtures.ts similarity index 100% rename from webapp/src/components/common/Matrix/core/__tests__/fixtures.ts rename to webapp/src/components/common/Matrix/shared/__tests__/fixtures.ts diff --git a/webapp/src/components/common/Matrix/core/__tests__/types.ts b/webapp/src/components/common/Matrix/shared/__tests__/types.ts similarity index 100% rename from webapp/src/components/common/Matrix/core/__tests__/types.ts rename to webapp/src/components/common/Matrix/shared/__tests__/types.ts diff --git a/webapp/src/components/common/Matrix/core/__tests__/utils.test.ts b/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts similarity index 100% rename from webapp/src/components/common/Matrix/core/__tests__/utils.test.ts rename to webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts diff --git a/webapp/src/components/common/Matrix/core/constants.ts b/webapp/src/components/common/Matrix/shared/constants.ts similarity index 100% rename from webapp/src/components/common/Matrix/core/constants.ts rename to webapp/src/components/common/Matrix/shared/constants.ts diff --git a/webapp/src/components/common/Matrix/core/types.ts b/webapp/src/components/common/Matrix/shared/types.ts similarity index 100% rename from webapp/src/components/common/Matrix/core/types.ts rename to webapp/src/components/common/Matrix/shared/types.ts diff --git a/webapp/src/components/common/Matrix/core/utils.ts b/webapp/src/components/common/Matrix/shared/utils.ts similarity index 99% rename from webapp/src/components/common/Matrix/core/utils.ts rename to webapp/src/components/common/Matrix/shared/utils.ts index a2738a7a1d..c500093476 100644 --- a/webapp/src/components/common/Matrix/core/utils.ts +++ b/webapp/src/components/common/Matrix/shared/utils.ts @@ -21,7 +21,7 @@ import { type AggregateConfig, type DateTimeMetadataDTO, type FormatNumberOptions, -} from "../core/types"; +} from "./types"; import { parseISO, Locale } from "date-fns"; import { fr, enUS } from "date-fns/locale"; import { getCurrentLanguage } from "@/utils/i18nUtils"; diff --git a/webapp/src/services/api/matrix.ts b/webapp/src/services/api/matrix.ts index f50df6b926..0f026c44a6 100644 --- a/webapp/src/services/api/matrix.ts +++ b/webapp/src/services/api/matrix.ts @@ -24,7 +24,7 @@ import { } from "../../common/types"; import { FileDownloadTask } from "./downloads"; import { getConfig } from "../config"; -import { MatrixUpdateDTO } from "../../components/common/Matrix/core/types"; +import { MatrixUpdateDTO } from "../../components/common/Matrix/shared/types"; export const getMatrixList = async ( name = "", From fb6f00f93d5456ee0740e6bdc18ec9c86f2d7509 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 29 Oct 2024 10:26:16 +0100 Subject: [PATCH 41/43] refactor(ui): disable percents on daily power and reservoir levels --- .../App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts | 2 -- .../components/common/Matrix/components/MatrixGrid/index.tsx | 3 +-- webapp/src/components/common/Matrix/shared/utils.ts | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 950dc891f2..db3d8802a0 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -132,7 +132,6 @@ export const MATRICES: Matrices = { columns: generateColumns("%"), rowHeaders: ["Generating Power", "Pumping Power"], enableDateTimeColumn: false, - showPercent: true, }, [HydroMatrix.EnergyCredits]: { title: "Standard Credits", @@ -148,7 +147,6 @@ export const MATRICES: Matrices = { title: "Reservoir Levels", url: "input/hydro/common/capacity/reservoir_{areaId}", columns: ["Lev Low (%)", "Lev Avg (%)", "Lev High (%)"], - showPercent: true, }, [HydroMatrix.WaterValues]: { title: "Water Values", diff --git a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx index 71b06e0976..48cc0ba540 100644 --- a/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx +++ b/webapp/src/components/common/Matrix/components/MatrixGrid/index.tsx @@ -201,10 +201,9 @@ function MatrixGrid({ smoothScrollX smoothScrollY rowHeight={30} - verticalBorder={false} overscrollX={100} overscrollY={100} - cellActivationBehavior="single-click" + cellActivationBehavior="second-click" />
diff --git a/webapp/src/components/common/Matrix/shared/utils.ts b/webapp/src/components/common/Matrix/shared/utils.ts index c500093476..2e6d0ed541 100644 --- a/webapp/src/components/common/Matrix/shared/utils.ts +++ b/webapp/src/components/common/Matrix/shared/utils.ts @@ -184,7 +184,7 @@ export function generateCustomColumns({ export function generateDataColumns( enableTimeSeriesColumns: boolean, columnCount: number, - customColumns?: string[], + customColumns?: string[] | readonly string[], colWidth?: number, ): EnhancedGridColumn[] { // If custom columns are provided, use them From 8897455aa881a990cb8056c549af4468f6c9f8ac Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 29 Oct 2024 15:42:03 +0100 Subject: [PATCH 42/43] fix(ui): prevent undefined error in number formatting --- .../Matrix/hooks/useGridCellContent/index.ts | 6 +-- .../Matrix/shared/__tests__/utils.test.ts | 4 +- .../components/common/Matrix/shared/types.ts | 2 +- .../components/common/Matrix/shared/utils.ts | 40 +++++++++++++------ 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts b/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts index 0c36df736b..0e20b0b2b9 100644 --- a/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useGridCellContent/index.ts @@ -19,7 +19,7 @@ import { type ColumnType, MatrixAggregates, } from "../../shared/types"; -import { formatNumber } from "../../shared/utils"; +import { formatGridNumber } from "../../shared/utils"; import { Column } from "../../shared/constants"; import { type CellContentGenerator } from "./types"; @@ -56,7 +56,7 @@ const cellContentGenerators: Record = { return { kind: GridCellKind.Number, data: value, - displayData: formatNumber({ value, maxDecimals: 6 }), + displayData: formatGridNumber({ value, maxDecimals: 6 }), readonly: !column.editable, allowOverlay: true, decimalSeparator: ".", @@ -69,7 +69,7 @@ const cellContentGenerators: Record = { return { kind: GridCellKind.Number, data: value, - displayData: formatNumber({ value, maxDecimals: 3 }), + displayData: formatGridNumber({ value, maxDecimals: 3 }), readonly: !column.editable, allowOverlay: false, decimalSeparator: ".", diff --git a/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts b/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts index 847541d3b3..ace38e0f34 100644 --- a/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts +++ b/webapp/src/components/common/Matrix/shared/__tests__/utils.test.ts @@ -15,7 +15,7 @@ import { Column } from "../constants"; import { calculateMatrixAggregates, - formatNumber, + formatGridNumber, generateCustomColumns, generateDateTime, generateTimeSeriesColumns, @@ -101,7 +101,7 @@ describe("Matrix Utils", () => { describe("Number Formatting", () => { describe.each(FORMAT_TEST_CASES)("$name", ({ cases }) => { test.each(cases)("$description", ({ value, maxDecimals, expected }) => { - expect(formatNumber({ value, maxDecimals })).toBe(expected); + expect(formatGridNumber({ value, maxDecimals })).toBe(expected); }); }); }); diff --git a/webapp/src/components/common/Matrix/shared/types.ts b/webapp/src/components/common/Matrix/shared/types.ts index 8e0b1f25c2..915602d0e1 100644 --- a/webapp/src/components/common/Matrix/shared/types.ts +++ b/webapp/src/components/common/Matrix/shared/types.ts @@ -51,7 +51,7 @@ export interface CustomColumnOptions { width?: number; } -export interface FormatNumberOptions { +export interface FormatGridNumberOptions { value?: number; maxDecimals?: number; } diff --git a/webapp/src/components/common/Matrix/shared/utils.ts b/webapp/src/components/common/Matrix/shared/utils.ts index 2e6d0ed541..de9b24277c 100644 --- a/webapp/src/components/common/Matrix/shared/utils.ts +++ b/webapp/src/components/common/Matrix/shared/utils.ts @@ -20,7 +20,7 @@ import { type AggregateType, type AggregateConfig, type DateTimeMetadataDTO, - type FormatNumberOptions, + type FormatGridNumberOptions, } from "./types"; import { parseISO, Locale } from "date-fns"; import { fr, enUS } from "date-fns/locale"; @@ -28,36 +28,50 @@ import { getCurrentLanguage } from "@/utils/i18nUtils"; import { Aggregate, Column, TIME_FREQUENCY_CONFIG } from "./constants"; /** - * Formats a number by adding thousand separators. + * Formats a number for display in a grid cell by adding thousand separators and handling decimals. * * This function is particularly useful for displaying load factors, * which are numbers between 0 and 1. For load factors, a maximum of * 6 decimal places should be displayed. For statistics, a maximum of * 3 decimal places is recommended. * - * @param options - The options for formatting the number. + * @example + * ```typescript + * formatGridNumber({ value: 1234567.89, maxDecimals: 2 }) // "1 234 567.89" + * formatGridNumber({ value: 0, maxDecimals: 6 }) // "0" + * formatGridNumber({ value: undefined }) // "" + * formatGridNumber({ value: NaN }) // "" + * ``` + * @param options - The formatting options * @param options.value - The number to format. - * @param options.maxDecimals - The maximum number of decimal places to keep. - * @returns The formatted number as a string. + * @param options.maxDecimals - Maximum number of decimal places to show. + * @returns A formatted string representation of the number with proper separators. */ -export function formatNumber({ +export function formatGridNumber({ value, maxDecimals = 0, -}: FormatNumberOptions): string { +}: FormatGridNumberOptions): string { if (value === undefined) { return ""; } - // Determine if we need to apply maxDecimals + const numValue = Number(value); + + if (isNaN(numValue)) { + return ""; + } + + const stringValue = value.toString(); + const dotIndex = stringValue.indexOf("."); + const hasDecimals = dotIndex !== -1; const shouldFormatDecimals = - value % 1 !== 0 && + hasDecimals && maxDecimals > 0 && - value.toString().split(".")[1].length > maxDecimals; + stringValue.length - dotIndex - 1 > maxDecimals; - // Use toFixed only if we need to control decimals const formattedValue = shouldFormatDecimals - ? value.toFixed(maxDecimals) - : value.toString(); + ? numValue.toFixed(maxDecimals) + : stringValue; const [integerPart, decimalPart] = formattedValue.split("."); From bd53261098d18496270d6fdbf6d2df56e4891662 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Tue, 29 Oct 2024 17:54:28 +0100 Subject: [PATCH 43/43] feat(ui-hydro): add data adapter for `Allocation` and `Correlation` Temporary solution to convert API numeric arrays to string arrays for Matrix component headers. Will be removed when API model is updated to return proper string labels. --- .../Areas/Hydro/HydroMatrixDialog.tsx | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrixDialog.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrixDialog.tsx index 85223c55db..73b109f7dc 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrixDialog.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrixDialog.tsx @@ -12,13 +12,27 @@ * This file is part of the Antares project. */ -import { Button, Box } from "@mui/material"; +import { Button, Box, Skeleton } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { useState, useEffect } from "react"; import BasicDialog, { BasicDialogProps, } from "../../../../../../common/dialogs/BasicDialog"; -import HydroMatrix from "./HydroMatrix"; +import Matrix from "../../../../../../common/Matrix"; import { HydroMatrixType } from "./utils"; +import { getAllocationMatrix } from "./Allocation/utils"; +import { getCorrelationMatrix } from "./Correlation/utils"; +import { useOutletContext } from "react-router"; +import { StudyMetadata } from "../../../../../../../common/types"; +import { MatrixDataDTO } from "@/components/common/Matrix/shared/types"; +import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; +import { AxiosError } from "axios"; + +interface AdaptedMatrixData { + data: number[][]; + columns: string[]; + index: string[]; +} interface Props { open: boolean; @@ -26,8 +40,63 @@ interface Props { type: HydroMatrixType; } +const MATRIX_FETCHERS = { + Allocation: getAllocationMatrix, + Correlation: getCorrelationMatrix, +} as const; + function HydroMatrixDialog({ open, onClose, type }: Props) { + /** + * !TEMPORARY SOLUTION - Matrix Data Model Adaptation + * + * This component handles a specific case (Allocation, Correlation). + * They receive their columns and row headers from the backend as numeric arrays. + * This differs from the standard Matrix component usage where these properties expect string arrays + * representing actual header labels. + * + * Current scenario: + * - Backend returns {columns: number[], index: number[]} for these specific matrices + * - Matrix component expects {columns: string[], rowHeaders: string[]} for its headers + * + * Future API model update will: + * - Rename 'index' to 'rowHeaders' to better reflect its purpose + * - Return properly formatted string arrays for header labels + * - Allow using the standard HydroMatrix component without this adapter + * + * TODO - Once the API is updated: + * 1. The model adapter layer will be removed + * 2. These matrices will use the HydroMatrix component directly + * 3. All matrices will follow the same data structure pattern + */ + const { t } = useTranslation(); + const { study } = useOutletContext<{ study: StudyMetadata }>(); + const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); + const fetchFn = MATRIX_FETCHERS[type as keyof typeof MATRIX_FETCHERS]; + const [matrix, setMatrix] = useState( + undefined, + ); + + const matrixModelAdapter = (apiData: MatrixDataDTO): AdaptedMatrixData => ({ + data: apiData.data, + columns: apiData.columns.map(String), + index: apiData.index.map(String), // Will be renamed to rowHeaders in future API version + }); + + useEffect(() => { + const fetchData = async () => { + try { + const data = await fetchFn(study.id); + setMatrix(matrixModelAdapter(data)); + } catch (error) { + enqueueErrorSnackbar(t("data.error.matrix"), error as AxiosError); + } + }; + + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [study.id, type, t]); + const dialogProps: BasicDialogProps = { open, onClose, @@ -38,6 +107,10 @@ function HydroMatrixDialog({ open, onClose, type }: Props) { ), }; + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + return ( - + {matrix ? ( + + ) : ( + + )} );