diff --git a/webapp/src/components/common/MatrixGrid/MatrixActions.tsx b/webapp/src/components/common/MatrixGrid/MatrixActions.tsx
index 6b0b6d27f5..4d135c6da2 100644
--- a/webapp/src/components/common/MatrixGrid/MatrixActions.tsx
+++ b/webapp/src/components/common/MatrixGrid/MatrixActions.tsx
@@ -85,10 +85,15 @@ function MatrixActions({
ButtonProps={{
startIcon: ,
}}
+ disabled={isSubmitting}
>
{t("global.import")}
-
+
);
}
diff --git a/webapp/src/components/common/MatrixGrid/index.test.tsx b/webapp/src/components/common/MatrixGrid/index.test.tsx
index b1858c518f..e7189ad93c 100644
--- a/webapp/src/components/common/MatrixGrid/index.test.tsx
+++ b/webapp/src/components/common/MatrixGrid/index.test.tsx
@@ -2,8 +2,7 @@ import { render } from "@testing-library/react";
import MatrixGrid, { MatrixGridProps } from ".";
import Box from "@mui/material/Box";
import { mockGetBoundingClientRect } from "../../../tests/mocks/mockGetBoundingClientRect";
-import { type EnhancedGridColumn } from "./types";
-import { ColumnDataType } from "./utils";
+import { type EnhancedGridColumn, ColumnTypes } from "./types";
import { mockHTMLCanvasElement } from "../../../tests/mocks/mockHTMLCanvasElement";
beforeEach(() => {
@@ -54,7 +53,7 @@ describe("MatrixGrid rendering", () => {
id: "col1",
title: "Column 1",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 0,
},
@@ -62,7 +61,7 @@ describe("MatrixGrid rendering", () => {
id: "col2",
title: "Column 2",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 1,
},
@@ -70,7 +69,7 @@ describe("MatrixGrid rendering", () => {
id: "col3",
title: "Column 3",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 2,
},
@@ -105,7 +104,7 @@ describe("MatrixGrid rendering", () => {
id: "col1",
title: "Column 1",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 0,
},
@@ -113,7 +112,7 @@ describe("MatrixGrid rendering", () => {
id: "col2",
title: "Column 2",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 1,
},
@@ -121,7 +120,7 @@ describe("MatrixGrid rendering", () => {
id: "col3",
title: "Column 3",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 2,
},
@@ -158,7 +157,7 @@ describe("MatrixGrid rendering", () => {
id: "col1",
title: "Column 1",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 0,
},
@@ -166,7 +165,7 @@ describe("MatrixGrid rendering", () => {
id: "col2",
title: "Column 2",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 1,
},
@@ -174,7 +173,7 @@ describe("MatrixGrid rendering", () => {
id: "col3",
title: "Column 3",
width: 100,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
editable: true,
order: 2,
},
diff --git a/webapp/src/components/common/MatrixGrid/index.tsx b/webapp/src/components/common/MatrixGrid/index.tsx
index 48c736df0b..70acbabd1e 100644
--- a/webapp/src/components/common/MatrixGrid/index.tsx
+++ b/webapp/src/components/common/MatrixGrid/index.tsx
@@ -1,14 +1,15 @@
import "@glideapps/glide-data-grid/dist/index.css";
import DataEditor, {
CompactSelection,
- EditListItem,
EditableGridCell,
+ EditListItem,
+ GridCellKind,
GridSelection,
Item,
} from "@glideapps/glide-data-grid";
import { useGridCellContent } from "./useGridCellContent";
import { useMemo, useState } from "react";
-import { type CellFillPattern, type EnhancedGridColumn } from "./types";
+import { EnhancedGridColumn, GridUpdate } from "./types";
import { darkTheme, readOnlyDarkTheme } from "./utils";
import { useColumnMapping } from "./useColumnMapping";
@@ -21,11 +22,8 @@ export interface MatrixGridProps {
rowHeaders?: string[];
width?: string;
height?: string;
- onCellEdit?: (cell: Item, newValue: number) => void;
- onMultipleCellsEdit?: (
- updates: Array<{ coordinates: Item; value: number }>,
- fillPattern?: CellFillPattern,
- ) => void;
+ onCellEdit?: (update: GridUpdate) => void;
+ onMultipleCellsEdit?: (updates: GridUpdate[]) => void;
readOnly?: boolean;
}
@@ -74,36 +72,34 @@ function MatrixGrid({
// Event Handlers
////////////////////////////////////////////////////////////////
- const handleCellEdited = (cell: Item, value: EditableGridCell) => {
- const updatedValue = value.data;
-
- if (typeof updatedValue !== "number" || isNaN(updatedValue)) {
+ const handleCellEdited = (coordinates: Item, value: EditableGridCell) => {
+ if (value.kind !== GridCellKind.Number) {
// Invalid numeric value
return;
}
- const dataCell = gridToData(cell);
+ const dataCoordinates = gridToData(coordinates);
- if (dataCell && onCellEdit) {
- onCellEdit(dataCell, updatedValue);
+ if (dataCoordinates && onCellEdit) {
+ onCellEdit({ coordinates: dataCoordinates, value });
}
};
const handleCellsEdited = (newValues: readonly EditListItem[]) => {
const updates = newValues
- .map((edit) => {
- const dataCell = gridToData(edit.location);
- return dataCell
- ? {
- coordinates: dataCell,
- value: edit.value.data as number,
- }
- : null;
+ .map((edit): GridUpdate | null => {
+ const dataCoordinates = gridToData(edit.location);
+
+ if (edit.value.kind !== GridCellKind.Number || !dataCoordinates) {
+ return null;
+ }
+
+ return {
+ coordinates: dataCoordinates,
+ value: edit.value,
+ };
})
- .filter(
- (update): update is { coordinates: Item; value: number } =>
- update !== null,
- );
+ .filter((update): update is GridUpdate => update !== null);
if (updates.length === 0) {
// No valid updates
diff --git a/webapp/src/components/common/MatrixGrid/types.ts b/webapp/src/components/common/MatrixGrid/types.ts
index 507466b495..2183801db5 100644
--- a/webapp/src/components/common/MatrixGrid/types.ts
+++ b/webapp/src/components/common/MatrixGrid/types.ts
@@ -1,16 +1,36 @@
import {
BaseGridColumn,
- FillPatternEventArgs,
+ EditableGridCell,
+ Item,
} from "@glideapps/glide-data-grid";
-import { ColumnDataType } from "./utils";
-export interface MatrixData {
- data: number[][];
- columns: number[];
- index: number[];
-}
+////////////////////////////////////////////////////////////////
+// Enums
+////////////////////////////////////////////////////////////////
-export type ColumnType = (typeof ColumnDataType)[keyof typeof ColumnDataType];
+export const ColumnTypes = {
+ DateTime: "datetime",
+ Number: "number",
+ Text: "text",
+ Aggregate: "aggregate",
+} as const;
+
+export const Operations = {
+ ADD: "+",
+ SUB: "-",
+ MUL: "*",
+ DIV: "/",
+ ABS: "ABS",
+ EQ: "=",
+} as const;
+
+////////////////////////////////////////////////////////////////
+// Types
+////////////////////////////////////////////////////////////////
+
+// Derived types
+export type ColumnType = (typeof ColumnTypes)[keyof typeof ColumnTypes];
+export type Operation = (typeof Operations)[keyof typeof Operations];
export interface EnhancedGridColumn extends BaseGridColumn {
id: string;
@@ -18,15 +38,31 @@ export interface EnhancedGridColumn extends BaseGridColumn {
type: ColumnType;
editable: boolean;
}
+// Represents data coming from the API
+export interface MatrixDataDTO {
+ data: number[][];
+ columns: number[];
+ index: number[];
+}
+
+export type Coordinates = [number, number];
-export type CellFillPattern = Omit;
+// Shape of updates provided by Glide Data Grid
+export interface GridUpdate {
+ coordinates: Item; // The cell being updated
+ value: EditableGridCell;
+}
+
+// Shape of updates to be sent to the API
+export interface MatrixUpdate {
+ operation: Operation;
+ value: number;
+}
-// TODO see MatrixIndex type, rundundant types
-export interface TimeMetadataDTO {
- start_date: string;
- steps: number;
- first_week_size: number;
- level: "hourly" | "daily" | "weekly" | "monthly" | "yearly";
+// Shape of multiple updates to be sent to the API
+export interface MatrixUpdateDTO {
+ coordinates: number[][]; // Array of [col, row] pairs
+ operation: MatrixUpdate;
}
export type DateIncrementStrategy = (
diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts b/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts
index 04003f3cb5..df476b482d 100644
--- a/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts
+++ b/webapp/src/components/common/MatrixGrid/useColumnMapping.test.ts
@@ -1,43 +1,42 @@
import { renderHook } from "@testing-library/react";
import { describe, test, expect } from "vitest";
import { useColumnMapping } from "./useColumnMapping";
-import { EnhancedGridColumn } from "./types";
-import { ColumnDataType } from "./utils";
+import { EnhancedGridColumn, ColumnTypes } from "./types";
describe("useColumnMapping", () => {
const testColumns: EnhancedGridColumn[] = [
{
id: "text",
title: "Text",
- type: ColumnDataType.Text,
+ type: ColumnTypes.Text,
width: 100,
editable: false,
},
{
id: "date",
title: "Date",
- type: ColumnDataType.DateTime,
+ type: ColumnTypes.DateTime,
width: 100,
editable: false,
},
{
id: "num1",
title: "Number 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 100,
editable: true,
},
{
id: "num2",
title: "Number 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 100,
editable: true,
},
{
id: "agg",
title: "Aggregate",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 100,
editable: false,
},
@@ -77,14 +76,14 @@ describe("useColumnMapping", () => {
{
id: "text",
title: "Text",
- type: ColumnDataType.Text,
+ type: ColumnTypes.Text,
width: 100,
editable: false,
},
{
id: "date",
title: "Date",
- type: ColumnDataType.DateTime,
+ type: ColumnTypes.DateTime,
width: 100,
editable: false,
},
@@ -100,14 +99,14 @@ describe("useColumnMapping", () => {
{
id: "num1",
title: "Number 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 100,
editable: true,
},
{
id: "num2",
title: "Number 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 100,
editable: true,
},
diff --git a/webapp/src/components/common/MatrixGrid/useColumnMapping.ts b/webapp/src/components/common/MatrixGrid/useColumnMapping.ts
index 0ec81ec29f..ab46b4db5e 100644
--- a/webapp/src/components/common/MatrixGrid/useColumnMapping.ts
+++ b/webapp/src/components/common/MatrixGrid/useColumnMapping.ts
@@ -1,7 +1,6 @@
import { useMemo } from "react";
import { Item } from "@glideapps/glide-data-grid";
-import { EnhancedGridColumn } from "./types";
-import { ColumnDataType } from "./utils";
+import { EnhancedGridColumn, ColumnTypes } from "./types";
/**
* A custom hook that provides coordinate mapping functions for a grid with mixed column types.
@@ -35,7 +34,7 @@ import { ColumnDataType } from "./utils";
export function useColumnMapping(columns: EnhancedGridColumn[]) {
return useMemo(() => {
const dataColumnIndices = columns.reduce((acc, col, index) => {
- if (col.type === ColumnDataType.Number) {
+ if (col.type === ColumnTypes.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 bfe0acf122..ccb0736fb5 100644
--- a/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts
+++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.test.ts
@@ -1,7 +1,6 @@
import { renderHook } from "@testing-library/react";
import { useGridCellContent } from "./useGridCellContent";
-import { type EnhancedGridColumn } from "./types";
-import { ColumnDataType } from "./utils";
+import { ColumnTypes, type EnhancedGridColumn } from "./types";
import { useColumnMapping } from "./useColumnMapping";
// Mocking i18next
@@ -68,14 +67,14 @@ describe("useGridCellContent", () => {
{
id: "date",
title: "Date",
- type: ColumnDataType.DateTime,
+ type: ColumnTypes.DateTime,
width: 150,
editable: false,
},
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
@@ -104,7 +103,7 @@ describe("useGridCellContent", () => {
{
id: "total",
title: "Total",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 100,
editable: false,
},
@@ -152,28 +151,28 @@ describe("useGridCellContent", () => {
{
id: "date",
title: "Date",
- type: ColumnDataType.DateTime,
+ type: ColumnTypes.DateTime,
width: 150,
editable: false,
},
{
id: "ts1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "ts2",
title: "TS 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "total",
title: "Total",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 100,
editable: false,
},
@@ -232,35 +231,35 @@ describe("useGridCellContent with mixed column types", () => {
{
id: "rowHeader",
title: "Row",
- type: ColumnDataType.Text,
+ type: ColumnTypes.Text,
width: 100,
editable: false,
},
{
id: "date",
title: "Date",
- type: ColumnDataType.DateTime,
+ type: ColumnTypes.DateTime,
width: 150,
editable: false,
},
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "data2",
title: "TS 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "total",
title: "Total",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 100,
editable: false,
},
@@ -337,21 +336,21 @@ describe("useGridCellContent with mixed column types", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "data2",
title: "TS 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "data3",
title: "TS 3",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
@@ -382,7 +381,7 @@ describe("useGridCellContent additional tests", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
@@ -404,7 +403,7 @@ describe("useGridCellContent additional tests", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
@@ -427,7 +426,7 @@ describe("useGridCellContent additional tests", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
@@ -449,7 +448,7 @@ describe("useGridCellContent additional tests", () => {
{
id: "total",
title: "Total",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 100,
editable: false,
},
@@ -474,14 +473,14 @@ describe("useGridCellContent additional tests", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
{
id: "data2",
title: "TS 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: false,
},
@@ -506,7 +505,7 @@ describe("useGridCellContent additional tests", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
width: 50,
editable: true,
},
diff --git a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts
index 725cae2291..09f6d06451 100644
--- a/webapp/src/components/common/MatrixGrid/useGridCellContent.ts
+++ b/webapp/src/components/common/MatrixGrid/useGridCellContent.ts
@@ -1,7 +1,7 @@
import { useCallback, useMemo } from "react";
import { GridCell, GridCellKind, Item } from "@glideapps/glide-data-grid";
-import { type EnhancedGridColumn, type ColumnType } from "./types";
-import { ColumnDataType, formatDateTime } from "./utils";
+import { type EnhancedGridColumn, type ColumnType, ColumnTypes } from "./types";
+import { formatDateTime } from "./utils";
type CellContentGenerator = (
row: number,
@@ -18,7 +18,7 @@ type CellContentGenerator = (
* Each generator function creates the appropriate GridCell based on the column type and data.
*/
const cellContentGenerators: Record = {
- [ColumnDataType.Text]: (
+ [ColumnTypes.Text]: (
row,
col,
column,
@@ -33,14 +33,14 @@ const cellContentGenerators: Record = {
readonly: !column.editable,
allowOverlay: false,
}),
- [ColumnDataType.DateTime]: (row, col, column, data, dateTime) => ({
+ [ColumnTypes.DateTime]: (row, col, column, data, dateTime) => ({
kind: GridCellKind.Text,
data: "", // Date/time columns are not editable
displayData: formatDateTime(dateTime?.[row] ?? ""),
readonly: !column.editable,
allowOverlay: false,
}),
- [ColumnDataType.Number]: (row, col, column, data) => {
+ [ColumnTypes.Number]: (row, col, column, data) => {
const value = data?.[row]?.[col];
return {
@@ -51,14 +51,7 @@ const cellContentGenerators: Record = {
allowOverlay: true,
};
},
- [ColumnDataType.Aggregate]: (
- row,
- col,
- column,
- data,
- dateTime,
- aggregates,
- ) => {
+ [ColumnTypes.Aggregate]: (row, col, column, data, dateTime, aggregates) => {
const value = aggregates?.[column.id]?.[row];
return {
@@ -142,7 +135,7 @@ export function useGridCellContent(
// accounting for any non-data columns in the grid
let adjustedCol = col;
- if (column.type === ColumnDataType.Number && gridToData) {
+ if (column.type === ColumnTypes.Number && gridToData) {
// Map grid cell to data array index
const dataCell = gridToData(cell);
diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx
index b15ca61642..2fe6599aca 100644
--- a/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx
+++ b/webapp/src/components/common/MatrixGrid/useMatrix.test.tsx
@@ -9,7 +9,8 @@ import {
Operator,
StudyOutputDownloadLevelDTO,
} from "../../../common/types";
-import { MatrixData } from "./types";
+import { GridUpdate, MatrixDataDTO } from "./types";
+import { GridCellKind } from "@glideapps/glide-data-grid";
vi.mock("../../../services/api/matrix");
vi.mock("../../../services/api/study");
@@ -18,7 +19,7 @@ describe("useMatrix", () => {
const mockStudyId = "study123";
const mockUrl = "https://studies/study123/matrix";
- const mockMatrixData: MatrixData = {
+ const mockMatrixData: MatrixDataDTO = {
data: [
[1, 2],
[3, 4],
@@ -50,6 +51,21 @@ describe("useMatrix", () => {
return result;
};
+ // Helper function to create a grid update object
+ const createGridUpdate = (
+ row: number,
+ col: number,
+ value: number,
+ ): GridUpdate => ({
+ coordinates: [row, col],
+ value: {
+ kind: GridCellKind.Number,
+ data: value,
+ displayData: value.toString(),
+ allowOverlay: true,
+ },
+ });
+
beforeEach(() => {
vi.clearAllMocks();
});
@@ -80,7 +96,7 @@ describe("useMatrix", () => {
});
act(() => {
- result.current.handleCellEdit([0, 1], 5);
+ result.current.handleCellEdit(createGridUpdate(0, 1, 5));
});
expect(result.current.data[1][0]).toBe(5);
@@ -99,8 +115,8 @@ describe("useMatrix", () => {
act(() => {
result.current.handleMultipleCellsEdit([
- { coordinates: [0, 1], value: 5 },
- { coordinates: [1, 0], value: 6 },
+ createGridUpdate(0, 1, 5),
+ createGridUpdate(1, 0, 6),
]);
});
@@ -112,7 +128,7 @@ describe("useMatrix", () => {
test("should handle save updates", async () => {
vi.mocked(apiStudy.getStudyData).mockResolvedValue(mockMatrixData);
vi.mocked(apiMatrix.getStudyMatrixIndex).mockResolvedValue(mockMatrixIndex);
- vi.mocked(apiMatrix.editMatrix).mockResolvedValue(undefined);
+ vi.mocked(apiMatrix.updateMatrix).mockResolvedValue(undefined);
const result = await setupHook();
@@ -121,7 +137,7 @@ describe("useMatrix", () => {
});
act(() => {
- result.current.handleCellEdit([0, 1], 5);
+ result.current.handleCellEdit(createGridUpdate(0, 1, 5));
});
await act(async () => {
@@ -136,7 +152,7 @@ describe("useMatrix", () => {
},
};
- expect(apiMatrix.editMatrix).toHaveBeenCalledWith(mockStudyId, mockUrl, [
+ expect(apiMatrix.updateMatrix).toHaveBeenCalledWith(mockStudyId, mockUrl, [
expectedEdit,
]);
expect(result.current.pendingUpdatesCount).toBe(0);
@@ -191,7 +207,7 @@ describe("useMatrix", () => {
});
act(() => {
- result.current.handleCellEdit([0, 1], 5);
+ result.current.handleCellEdit(createGridUpdate(0, 1, 5));
});
expect(result.current.canUndo).toBe(true);
@@ -225,7 +241,7 @@ describe("useMatrix", () => {
});
act(() => {
- result.current.handleCellEdit([0, 1], 5);
+ result.current.handleCellEdit(createGridUpdate(0, 1, 5));
});
act(() => {
@@ -235,7 +251,7 @@ describe("useMatrix", () => {
expect(result.current.canRedo).toBe(true);
act(() => {
- result.current.handleCellEdit([1, 0], 6);
+ result.current.handleCellEdit(createGridUpdate(1, 0, 6));
});
expect(result.current.canUndo).toBe(true);
@@ -255,7 +271,7 @@ describe("useMatrix", () => {
});
act(() => {
- result.current.handleCellEdit([0, 1], 5);
+ result.current.handleCellEdit(createGridUpdate(0, 1, 5));
});
act(() => {
diff --git a/webapp/src/components/common/MatrixGrid/useMatrix.ts b/webapp/src/components/common/MatrixGrid/useMatrix.ts
index 4df22528c8..ff16f05b09 100644
--- a/webapp/src/components/common/MatrixGrid/useMatrix.ts
+++ b/webapp/src/components/common/MatrixGrid/useMatrix.ts
@@ -1,28 +1,28 @@
import { useCallback, useEffect, useMemo, useState } from "react";
-import { Item } from "@glideapps/glide-data-grid";
import { AxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import { t } from "i18next";
-import { MatrixEditDTO, MatrixIndex, Operator } from "../../../common/types";
+import { MatrixIndex, Operator } from "../../../common/types";
import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar";
-import { getStudyMatrixIndex, editMatrix } from "../../../services/api/matrix";
+import {
+ getStudyMatrixIndex,
+ updateMatrix,
+} from "../../../services/api/matrix";
import { getStudyData, importFile } from "../../../services/api/study";
import {
EnhancedGridColumn,
- CellFillPattern,
- TimeMetadataDTO,
- MatrixData,
+ MatrixDataDTO,
+ ColumnTypes,
+ GridUpdate,
+ MatrixUpdateDTO,
} from "./types";
-import {
- generateDateTime,
- ColumnDataType,
- generateTimeSeriesColumns,
-} from "./utils";
+import { generateDateTime, generateTimeSeriesColumns } from "./utils";
import useUndo from "use-undo";
+import { GridCellKind } from "@glideapps/glide-data-grid";
interface DataState {
data: number[][];
- pendingUpdates: MatrixEditDTO[];
+ pendingUpdates: MatrixUpdateDTO[];
}
export function useMatrix(
@@ -44,7 +44,7 @@ export function useMatrix(
setIsLoading(true);
try {
const [matrix, index] = await Promise.all([
- getStudyData(studyId, url),
+ getStudyData(studyId, url),
getStudyMatrixIndex(studyId, url),
]);
@@ -65,7 +65,7 @@ export function useMatrix(
}, [fetchMatrix]);
const dateTime = useMemo(() => {
- return index ? generateDateTime(index as TimeMetadataDTO) : [];
+ return index ? generateDateTime(index) : [];
}, [index]);
const columns: EnhancedGridColumn[] = useMemo(() => {
@@ -77,7 +77,7 @@ export function useMatrix(
{
id: "date",
title: "Date",
- type: ColumnDataType.DateTime,
+ type: ColumnTypes.DateTime,
editable: false,
},
];
@@ -91,21 +91,21 @@ export function useMatrix(
{
id: "min",
title: "Min",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 50,
editable: false,
},
{
id: "max",
title: "Max",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 50,
editable: false,
},
{
id: "avg",
title: "Avg",
- type: ColumnDataType.Aggregate,
+ type: ColumnTypes.Aggregate,
width: 50,
editable: false,
},
@@ -122,24 +122,28 @@ export function useMatrix(
// Apply updates to the matrix data and store them in the pending updates list
const applyUpdates = useCallback(
- (updates: Array<{ coordinates: Item; value: number }>) => {
- if (!currentState.data) {
- return;
- }
-
+ (updates: GridUpdate[]) => {
const updatedData = currentState.data.map((col) => [...col]);
- const newUpdates = updates.map(({ coordinates: [row, col], value }) => {
- updatedData[col][row] = value;
-
- return {
- coordinates: [[col, row]],
- operation: {
- operation: Operator.EQ,
- value,
- },
- };
- });
+ const newUpdates: MatrixUpdateDTO[] = updates
+ .map(({ coordinates: [row, col], value }) => {
+ if (value.kind === GridCellKind.Number && value.data) {
+ updatedData[col][row] = value.data;
+
+ return {
+ coordinates: [[col, row]],
+ operation: {
+ operation: Operator.EQ,
+ value: value.data,
+ },
+ };
+ }
+
+ return null;
+ })
+ .filter(
+ (update): update is NonNullable => update !== null,
+ );
setState({
data: updatedData,
@@ -149,22 +153,18 @@ export function useMatrix(
[currentState, setState],
);
- const handleCellEdit = function (coordinates: Item, newValue: number) {
- applyUpdates([{ coordinates, value: newValue }]);
+ const handleCellEdit = function (update: GridUpdate) {
+ applyUpdates([update]);
};
- const handleMultipleCellsEdit = function (
- newValues: Array<{ coordinates: Item; value: number }>,
- fillPattern?: CellFillPattern,
- ) {
- applyUpdates(newValues);
+ const handleMultipleCellsEdit = function (updates: GridUpdate[]) {
+ applyUpdates(updates);
};
const handleImport = async (file: File) => {
try {
await importFile(file, studyId, url);
-
- enqueueSnackbar(t("matrix.success.import"), { variant: "success" });
+ await fetchMatrix();
} catch (e) {
enqueueErrorSnackbar(t("matrix.error.import"), e as Error);
}
@@ -177,7 +177,7 @@ export function useMatrix(
setIsSubmitting(true);
try {
- await editMatrix(studyId, url, currentState.pendingUpdates);
+ await updateMatrix(studyId, url, currentState.pendingUpdates);
setState({ data: currentState.data, pendingUpdates: [] });
enqueueSnackbar(t("matrix.success.matrixUpdate"), {
variant: "success",
diff --git a/webapp/src/components/common/MatrixGrid/utils.test.ts b/webapp/src/components/common/MatrixGrid/utils.test.ts
index 636918fa0e..26e42ff6eb 100644
--- a/webapp/src/components/common/MatrixGrid/utils.test.ts
+++ b/webapp/src/components/common/MatrixGrid/utils.test.ts
@@ -1,17 +1,17 @@
-import { TimeMetadataDTO } from "./types";
import {
- ColumnDataType,
- generateDateTime,
- generateTimeSeriesColumns,
-} from "./utils";
+ MatrixIndex,
+ StudyOutputDownloadLevelDTO,
+} from "../../../common/types";
+import { ColumnTypes } from "./types";
+import { generateDateTime, generateTimeSeriesColumns } from "./utils";
describe("generateDateTime", () => {
test("generates correct number of dates", () => {
- const metadata: TimeMetadataDTO = {
+ const metadata: MatrixIndex = {
start_date: "2023-01-01T00:00:00Z",
steps: 5,
first_week_size: 7,
- level: "daily",
+ level: StudyOutputDownloadLevelDTO.DAILY,
};
const result = generateDateTime(metadata);
expect(result).toHaveLength(5);
@@ -55,18 +55,18 @@ describe("generateDateTime", () => {
],
},
{
- level: "yearly",
+ 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: TimeMetadataDTO = {
+ const metadata: MatrixIndex = {
start_date: start,
steps: expected.length,
first_week_size: 7,
- level: level as TimeMetadataDTO["level"],
+ level: level as MatrixIndex["level"],
};
const result = generateDateTime(metadata);
@@ -76,11 +76,11 @@ describe("generateDateTime", () => {
);
test("handles edge cases", () => {
- const metadata: TimeMetadataDTO = {
+ const metadata: MatrixIndex = {
start_date: "2023-12-31T23:59:59Z",
steps: 2,
first_week_size: 7,
- level: "hourly",
+ level: StudyOutputDownloadLevelDTO.HOURLY,
};
const result = generateDateTime(metadata);
expect(result).toEqual([
@@ -102,7 +102,7 @@ describe("generateTimeSeriesColumns", () => {
{
id: "data1",
title: "TS 1",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
style: "normal",
width: 50,
editable: true,
@@ -110,7 +110,7 @@ describe("generateTimeSeriesColumns", () => {
{
id: "data2",
title: "TS 2",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
style: "normal",
width: 50,
editable: true,
@@ -118,7 +118,7 @@ describe("generateTimeSeriesColumns", () => {
{
id: "data3",
title: "TS 3",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
style: "normal",
width: 50,
editable: true,
@@ -138,7 +138,7 @@ describe("generateTimeSeriesColumns", () => {
{
id: "data10",
title: "Data 10",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
style: "normal",
width: 80,
editable: false,
@@ -146,7 +146,7 @@ describe("generateTimeSeriesColumns", () => {
{
id: "data11",
title: "Data 11",
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
style: "normal",
width: 80,
editable: false,
@@ -169,7 +169,7 @@ describe("generateTimeSeriesColumns", () => {
test("maintains consistent type and style", () => {
const result = generateTimeSeriesColumns({ count: 1000 });
result.forEach((column) => {
- expect(column.type).toBe(ColumnDataType.Number);
+ expect(column.type).toBe(ColumnTypes.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 66278fe838..8dfefd802f 100644
--- a/webapp/src/components/common/MatrixGrid/utils.ts
+++ b/webapp/src/components/common/MatrixGrid/utils.ts
@@ -1,26 +1,12 @@
import moment from "moment";
import {
- TimeMetadataDTO,
DateIncrementStrategy,
EnhancedGridColumn,
+ ColumnTypes,
} from "./types";
import { getCurrentLanguage } from "../../../utils/i18nUtils";
import { Theme } from "@glideapps/glide-data-grid";
-
-////////////////////////////////////////////////////////////////
-// Enums
-////////////////////////////////////////////////////////////////
-
-export const ColumnDataType = {
- DateTime: "datetime",
- Number: "number",
- Text: "text",
- Aggregate: "aggregate",
-} as const;
-
-////////////////////////////////////////////////////////////////
-// Utils
-////////////////////////////////////////////////////////////////
+import { MatrixIndex } from "../../../common/types";
export const darkTheme: Theme = {
accentColor: "rgba(255, 184, 0, 0.9)",
@@ -45,7 +31,7 @@ export const darkTheme: Theme = {
borderColor: "rgba(255, 255, 255, 0.12)",
drilldownBorder: "rgba(255, 255, 255, 0.35)",
linkColor: "#818CF8",
- headerFontStyle: "bold 13px",
+ headerFontStyle: "bold 11px",
baseFontStyle: "13px",
fontFamily: "Inter, sans-serif",
editorFontSize: "13px",
@@ -70,14 +56,14 @@ export const readOnlyDarkTheme: Partial = {
};
const dateIncrementStrategies: Record<
- TimeMetadataDTO["level"],
+ 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"),
- yearly: (date, step) => date.clone().add(step, "years"),
+ annual: (date, step) => date.clone().add(step, "years"),
};
const dateTimeFormatOptions: Intl.DateTimeFormatOptions = {
@@ -117,7 +103,6 @@ const dateTimeFormatOptions: Intl.DateTimeFormatOptions = {
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);
@@ -150,7 +135,7 @@ export function formatDateTime(dateTime: string): string {
* "2023-01-03T00:00:00.000Z"
* ]
*
- * @see {@link TimeMetadataDTO} for the structure of the timeMetadata object.
+ * @see {@link MatrixIndex} for the structure of the timeMetadata object.
* @see {@link DateIncrementStrategy} for the date increment strategy type.
*/
export function generateDateTime({
@@ -158,7 +143,7 @@ export function generateDateTime({
start_date,
steps,
level,
-}: TimeMetadataDTO): string[] {
+}: MatrixIndex): string[] {
const startDate = moment.utc(start_date, "YYYY-MM-DD HH:mm:ss");
const incrementStrategy = dateIncrementStrategies[level];
@@ -181,12 +166,12 @@ export function generateDateTime({
*
* @example Usage within a column definition array
* const columns = [
- * { id: "rowHeaders", title: "", type: ColumnDataType.Text, ... },
- * { id: "date", title: "Date", type: ColumnDataType.DateTime, ... },
+ * { id: "rowHeaders", title: "", type: ColumnTypes.Text, ... },
+ * { id: "date", title: "Date", type: ColumnTypes.DateTime, ... },
* ...generateTimeSeriesColumns({ count: 60 }),
- * { id: "min", title: "Min", type: ColumnDataType.Aggregate, ... },
- * { id: "max", title: "Max", type: ColumnDataType.Aggregate, ... },
- * { id: "avg", title: "Avg", type: ColumnDataType.Aggregate, ... }
+ * { id: "min", title: "Min", type: ColumnTypes.Aggregate, ... },
+ * { id: "max", title: "Max", type: ColumnTypes.Aggregate, ... },
+ * { id: "avg", title: "Avg", type: ColumnTypes.Aggregate, ... }
* ];
*/
export function generateTimeSeriesColumns({
@@ -207,7 +192,7 @@ export function generateTimeSeriesColumns({
return Array.from({ length: count }, (_, index) => ({
id: `data${startIndex + index}`,
title: `${prefix} ${startIndex + index}`,
- type: ColumnDataType.Number,
+ type: ColumnTypes.Number,
style: style,
width: width,
editable: editable,
diff --git a/webapp/src/services/api/matrix.ts b/webapp/src/services/api/matrix.ts
index ad80cfa92c..54e028b2ef 100644
--- a/webapp/src/services/api/matrix.ts
+++ b/webapp/src/services/api/matrix.ts
@@ -10,6 +10,7 @@ import {
} from "../../common/types";
import { FileDownloadTask } from "./downloads";
import { getConfig } from "../config";
+import { MatrixUpdateDTO } from "../../components/common/MatrixGrid/types";
export const getMatrixList = async (
name = "",
@@ -91,6 +92,13 @@ export const deleteDataSet = async (id: string): Promise => {
return res.data;
};
+/**
+ * @deprecated Use `updateMatrix` instead.
+ *
+ * @param sid - The study ID.
+ * @param path - The path of the matrix.
+ * @param matrixEdit - The matrix edit data.
+ */
export const editMatrix = async (
sid: string,
path: string,
@@ -104,6 +112,19 @@ export const editMatrix = async (
);
};
+export const updateMatrix = async (
+ studyId: string,
+ path: string,
+ updates: MatrixUpdateDTO[],
+): Promise => {
+ const sanitizedPath = path.startsWith("/") ? path.substring(1) : path;
+
+ await client.put(
+ `/v1/studies/${studyId}/matrix?path=${encodeURIComponent(sanitizedPath)}`,
+ updates,
+ );
+};
+
export const getStudyMatrixIndex = async (
sid: string,
path?: string,