Skip to content

Commit

Permalink
feat(ui): implement manual save and batch updates for matrix editing
Browse files Browse the repository at this point in the history
  • Loading branch information
hdinia committed Sep 6, 2024
1 parent c4dcf74 commit 9653c7c
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ function Load() {
const currentArea = useAppSelector(getCurrentAreaId);
const url = `input/load/series/load_${currentArea}`;

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return <Matrix url={url} />;
}

Expand Down
35 changes: 24 additions & 11 deletions webapp/src/components/common/MatrixGrid/Matrix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useOutletContext } from "react-router";
import { StudyMetadata } from "../../../common/types";
import { MatrixContainer, MatrixHeader, MatrixTitle } from "./style";
import MatrixActions from "./MatrixActions";
import EmptyView from "../page/SimpleContent";

interface MatrixProps {
url: string;
Expand All @@ -27,46 +28,58 @@ function Matrix({
const [openImportDialog, setOpenImportDialog] = useState(false);

const {
matrixData,
data,
error,
isLoading,
isSubmitting,
columns,
dateTime,
handleCellEdit,
handleMultipleCellsEdit,
handleImport,
handleSaveUpdates,
pendingUpdatesCount,
} = useMatrix(study.id, url, enableTimeSeriesColumns, enableAggregateColumns);

if (isLoading || !matrixData) {
return <Skeleton sx={{ height: 1, transform: "none" }} />;
}

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

if (isLoading) {
return <Skeleton sx={{ height: 1, transform: "none" }} />;
}

if (error) {
return <EmptyView title={error.toString()} />;
}

if (!data || data.length === 0) {
return <EmptyView title={t("matrix.error.noData")} />;
}

return (
<MatrixContainer>
<MatrixHeader>
<MatrixTitle>{t(title)}</MatrixTitle>
<MatrixActions
onImport={() => setOpenImportDialog(true)}
onSave={handleSaveUpdates}
studyId={study.id}
path={url}
disabled={matrixData.data.length === 0}
disabled={data.length === 0}
pendingUpdatesCount={pendingUpdatesCount}
isSubmitting={isSubmitting}
/>
</MatrixHeader>

<Divider sx={{ width: 1, mt: 1, mb: 2 }} />

<MatrixGrid
data={matrixData.data}
data={data}
columns={columns}
rows={matrixData.data.length}
rows={data.length}
dateTime={dateTime}
onCellEdit={handleCellEdit}
onMultipleCellsEdit={handleMultipleCellsEdit}
/>

{openImportDialog && (
<ImportDialog
open={openImportDialog}
Expand Down
24 changes: 22 additions & 2 deletions webapp/src/components/common/MatrixGrid/MatrixActions.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { Box } from "@mui/material";
import { Box, Divider } from "@mui/material";
import SplitButton from "../buttons/SplitButton";
import DownloadMatrixButton from "../DownloadMatrixButton";
import FileDownload from "@mui/icons-material/FileDownload";
import { useTranslation } from "react-i18next";
import { LoadingButton } from "@mui/lab";
import Save from "@mui/icons-material/Save";

interface MatrixActionsProps {
onImport: () => void;
onImport: VoidFunction;
onSave: VoidFunction;
studyId: string;
path: string;
disabled: boolean;
pendingUpdatesCount: number;
isSubmitting: boolean;
}

function MatrixActions({
onImport,
onSave,
studyId,
path,
disabled,
pendingUpdatesCount,
isSubmitting,
}: MatrixActionsProps) {
const { t } = useTranslation();

Expand All @@ -25,6 +33,18 @@ function MatrixActions({

return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<LoadingButton
onClick={onSave}
loading={isSubmitting}
loadingPosition="start"
startIcon={<Save />}
variant="contained"
size="small"
disabled={pendingUpdatesCount === 0}
>
{t("global.save")} ({pendingUpdatesCount})
</LoadingButton>
<Divider sx={{ mx: 2 }} orientation="vertical" flexItem />
<SplitButton
options={[t("global.import.fromFile"), t("global.import.fromDatabase")]}
onClick={onImport}
Expand Down
154 changes: 89 additions & 65 deletions webapp/src/components/common/MatrixGrid/useMatrix.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useMemo } from "react";
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, Operator } from "../../../common/types";
import { MatrixEditDTO, MatrixIndex, Operator } from "../../../common/types";
import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar";
import usePromiseWithSnackbarError from "../../../hooks/usePromiseWithSnackbarError";
import { getStudyMatrixIndex, editMatrix } from "../../../services/api/matrix";
import { getStudyData, importFile } from "../../../services/api/study";
import {
Expand All @@ -27,31 +26,44 @@ export function useMatrix(
enableAggregateColumns: boolean,
) {
const enqueueErrorSnackbar = useEnqueueErrorSnackbar();
const [data, setData] = useState<MatrixData["data"]>([]);
const [columnCount, setColumnCount] = useState(0);
const [index, setIndex] = useState<MatrixIndex | undefined>(undefined);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [pendingUpdates, setPendingUpdates] = useState<MatrixEditDTO[]>([]);

const fetchMatrix = useCallback(async () => {
setIsLoading(true);
setError(undefined);
try {
const [matrix, index] = await Promise.all([
getStudyData<MatrixData>(studyId, url),
getStudyMatrixIndex(studyId, url),
]);

setData(matrix.data);
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);
setIsLoading(false);
}
}, [enqueueErrorSnackbar, studyId, url]);

const {
data: matrixData,
isLoading: isLoadingMatrix,
reload: reloadMatrix,
} = usePromiseWithSnackbarError(
() => getStudyData<MatrixData>(studyId, url),
{
errorMessage: t("data.error.matrix"),
deps: [studyId, url],
},
);

const { data: matrixIndex, isLoading: isLoadingIndex } =
usePromiseWithSnackbarError(() => getStudyMatrixIndex(studyId, url), {
errorMessage: t("matrix.error.failedToretrieveIndex"),
deps: [studyId, url, matrixData],
});
useEffect(() => {
fetchMatrix();
}, [fetchMatrix]);

const dateTime = useMemo(() => {
return matrixIndex ? generateDateTime(matrixIndex as TimeMetadataDTO) : [];
}, [matrixIndex]);
return index ? generateDateTime(index as TimeMetadataDTO) : [];
}, [index]);

const columns: EnhancedGridColumn[] = useMemo(() => {
if (!matrixData) {
if (!data) {
return [];
}

Expand All @@ -60,13 +72,12 @@ export function useMatrix(
id: "date",
title: "Date",
type: ColumnDataType.DateTime,
width: 150,
editable: false,
},
];

const dataColumns = enableTimeSeriesColumns
? generateTimeSeriesColumns({ count: matrixData.columns.length })
? generateTimeSeriesColumns({ count: columnCount })
: [];

const aggregateColumns = enableAggregateColumns
Expand Down Expand Up @@ -96,73 +107,86 @@ export function useMatrix(
: [];

return [...baseColumns, ...dataColumns, ...aggregateColumns];
}, [matrixData, enableTimeSeriesColumns, enableAggregateColumns]);

const handleCellEdit = async function (coordinates: Item, newValue: number) {
const [row, col] = coordinates;
}, [data, enableTimeSeriesColumns, columnCount, enableAggregateColumns]);

const updateDataAndPendingUpdates = (
updates: Array<{ coordinates: Item; value: number }>,
) => {
setData((prevData) => {
const newData = prevData.map((col) => [...col]);
updates.forEach(({ coordinates: [row, col], value }) => {
newData[col][row] = value;
});
return newData;
});

const update: MatrixEditDTO[] = [
{
setPendingUpdates((prevUpdates) => [
...prevUpdates,
...updates.map(({ coordinates: [row, col], value }) => ({
coordinates: [[col, row]],
operation: {
operation: Operator.EQ,
value: newValue,
value,
},
},
];
})),
]);
};

try {
await editMatrix(studyId, url, update);
reloadMatrix();
enqueueSnackbar(t("matrix.success.matrixUpdate"), {
variant: "success",
});
} catch (e) {
enqueueErrorSnackbar(t("matrix.error.matrixUpdate"), e as AxiosError);
}
const handleCellEdit = function (coordinates: Item, newValue: number) {
updateDataAndPendingUpdates([{ coordinates, value: newValue }]);
};

const handleMultipleCellsEdit = async function (
const handleMultipleCellsEdit = function (
newValues: Array<{ coordinates: Item; value: number }>,
fillPattern?: CellFillPattern,
) {
const updates = newValues.map(({ coordinates, value }) => ({
coordinates: [[coordinates[1], coordinates[0]]],
operation: {
operation: Operator.EQ,
value,
},
}));

try {
await editMatrix(studyId, url, updates);
reloadMatrix();
enqueueSnackbar(t("matrix.success.matrixUpdate"), {
variant: "success",
});
} catch (e) {
enqueueErrorSnackbar(t("matrix.error.matrixUpdate"), e as AxiosError);
}
updateDataAndPendingUpdates(newValues);
};

const handleImport = async (file: File) => {
try {
await importFile(file, studyId, url);
reloadMatrix();

enqueueSnackbar(t("matrix.success.import"), { variant: "success" });
} catch (e) {
enqueueErrorSnackbar(t("matrix.error.import"), e as Error);
}
};

const handleSaveUpdates = async () => {
if (pendingUpdates.length > 0) {
setIsSubmitting(true);
setError(undefined);
try {
await editMatrix(studyId, url, pendingUpdates);
setPendingUpdates([]);
enqueueSnackbar(t("matrix.success.matrixUpdate"), {
variant: "success",
});
} catch (error) {
setError(new Error(t("matrix.error.matrixUpdate")));
enqueueErrorSnackbar(
t("matrix.error.matrixUpdate"),
error as AxiosError,
);
} finally {
await fetchMatrix();
setIsSubmitting(false);
}
}
};

return {
matrixData,
isLoading: isLoadingMatrix || isLoadingIndex,
data,
error,
isLoading,
isSubmitting,
columns,
dateTime,
handleCellEdit,
handleMultipleCellsEdit,
handleImport,
reloadMatrix,
handleSaveUpdates,
pendingUpdatesCount: pendingUpdates.length,
};
}
3 changes: 1 addition & 2 deletions webapp/src/components/common/MatrixGrid/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ export function generateDateTime({
* @param [options.style="normal"] - The style of the columns (default is "normal").
* @returns An array of EnhancedGridColumn objects representing time series data columns.
*
* @example
* // Usage within a column definition array
* @example <caption>Usage within a column definition array</caption>
* const columns = [
* { id: "rowHeaders", title: "", type: ColumnDataType.Text, ... },
* { id: "date", title: "Date", type: ColumnDataType.DateTime, ... },
Expand Down

0 comments on commit 9653c7c

Please sign in to comment.