From 5257aa7b40346fbeac5c394c88c3dba250b6e0ed Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Thu, 19 Dec 2024 15:08:32 +0100 Subject: [PATCH] refactor(ui-raw): update api methods --- .../Singlestudy/explore/Debug/Data/Json.tsx | 38 ++------ .../Singlestudy/explore/Debug/Data/Text.tsx | 26 ++---- .../explore/Debug/Data/Unsupported.tsx | 17 +--- .../common/Matrix/hooks/useMatrix/index.ts | 5 +- .../Matrix/hooks/useMatrix/useMatrix.test.tsx | 4 +- .../common/buttons/DownloadMatrixButton.tsx | 6 +- .../common/buttons/UploadFileButton.tsx | 4 +- webapp/src/services/api/studies/raw/index.ts | 91 +++++++++++++------ webapp/src/services/api/studies/raw/types.ts | 10 +- 9 files changed, 102 insertions(+), 99 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx index ed564d2d07..6f04cb26f1 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx @@ -21,7 +21,6 @@ import UsePromiseCond from "../../../../../common/utils/UsePromiseCond"; import type { DataCompProps } from "../utils"; import DownloadButton from "../../../../../common/buttons/DownloadButton"; import { downloadFile } from "../../../../../../utils/fileUtils"; -import { useEffect, useState } from "react"; import { Filename, Flex, Menubar } from "./styles"; import UploadFileButton from "../../../../../common/buttons/UploadFileButton"; import { getRawFile } from "@/services/api/studies/raw"; @@ -29,9 +28,8 @@ import { getRawFile } from "@/services/api/studies/raw"; function Json({ filePath, filename, studyId, canEdit }: DataCompProps) { const [t] = useTranslation(); const { enqueueSnackbar } = useSnackbar(); - const [currentJson, setCurrentJson] = useState(); - const fileRes = usePromiseWithSnackbarError( + const jsonRes = usePromiseWithSnackbarError( () => getStudyData(studyId, filePath, -1), { errorMessage: t("studies.error.retrieveData"), @@ -39,43 +37,27 @@ function Json({ filePath, filename, studyId, canEdit }: DataCompProps) { }, ); - const rawFileRes = usePromiseWithSnackbarError( - () => getRawFile(studyId, filePath), - { - errorMessage: t("studies.error.retrieveData"), - deps: [studyId, filePath], - }, - ); - - useEffect(() => { - setCurrentJson(fileRes.data); - }, [fileRes.data]); - - const handleDownload = () => { - if (rawFileRes.data) { - const { data, filename } = rawFileRes.data; - downloadFile(data, filename); - } - }; - //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// + const handleDownload = async () => { + const { data, filename } = await getRawFile({ studyId, path: filePath }); + downloadFile(data, filename); + }; + const handleSave: JSONEditorProps["onSave"] = (json) => { return editStudy(json, studyId, filePath); }; - const handleSaveSuccessful: JSONEditorProps["onSaveSuccessful"] = (json) => { - setCurrentJson(json); - + const handleSaveSuccessful: JSONEditorProps["onSaveSuccessful"] = () => { enqueueSnackbar(t("studies.success.saveData"), { variant: "success", }); }; const handleUploadSuccessful = () => { - fileRes.reload(); + jsonRes.reload(); }; //////////////////////////////////////////////////////////////// @@ -84,7 +66,7 @@ function Json({ filePath, filename, studyId, canEdit }: DataCompProps) { return ( ( @@ -100,7 +82,7 @@ function Json({ filePath, filename, studyId, canEdit }: DataCompProps) { getStudyData(studyId, filePath).then((text) => parseContent(text, { filePath, fileType }), @@ -87,27 +87,17 @@ function Text({ }, ); - const rawFileRes = usePromiseWithSnackbarError( - () => getRawFile(studyId, filePath), - { - errorMessage: t("studies.error.retrieveData"), - deps: [studyId, filePath], - }, - ); - - const handleDownload = () => { - if (rawFileRes.data) { - const { data, filename } = rawFileRes.data; - downloadFile(data, filename); - } - }; - //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// + const handleDownload = async () => { + const { data, filename } = await getRawFile({ studyId, path: filePath }); + downloadFile(data, filename); + }; + const handleUploadSuccessful = () => { - fileRes.reload(); + textRes.reload(); }; //////////////////////////////////////////////////////////////// @@ -116,7 +106,7 @@ function Text({ return ( ( diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx index aaf91400bf..c56c9f89d2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx @@ -19,30 +19,19 @@ import { Filename, Flex, Menubar } from "./styles"; import type { DataCompProps } from "../utils"; import DownloadButton from "@/components/common/buttons/DownloadButton"; import UploadFileButton from "@/components/common/buttons/UploadFileButton"; -import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError"; import { downloadFile } from "@/utils/fileUtils"; import { getRawFile } from "@/services/api/studies/raw"; function Unsupported({ studyId, filePath, filename, canEdit }: DataCompProps) { const { t } = useTranslation(); - const rawFileRes = usePromiseWithSnackbarError( - () => getRawFile(studyId, filePath), - { - errorMessage: t("studies.error.retrieveData"), - deps: [studyId, filePath], - }, - ); - //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// - const handleDownload = () => { - if (rawFileRes.data) { - const { data, filename } = rawFileRes.data; - downloadFile(data, filename); - } + const handleDownload = async () => { + const { data, filename } = await getRawFile({ studyId, path: filePath }); + downloadFile(data, filename); }; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts index 6558e764e8..e89f99be12 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts @@ -39,7 +39,7 @@ import { } from "../../shared/utils"; import useUndo from "use-undo"; import { GridCellKind } from "@glideapps/glide-data-grid"; -import { importFile } from "../../../../../services/api/studies/raw"; +import { uploadFile } 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 "../../shared/constants"; @@ -251,7 +251,8 @@ export function useMatrix( const handleUpload = async (file: File) => { try { - await importFile({ file, studyId, path: url }); + await uploadFile({ file, studyId, path: url }); + // TODO: update the API to return the uploaded file data and remove this await fetchMatrix(); } catch (e) { enqueueErrorSnackbar(t("matrix.error.import"), e as Error); 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 fc1087a927..e4b61d8587 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/useMatrix.test.tsx @@ -166,7 +166,7 @@ describe("useMatrix", () => { describe("File operations", () => { test("should handle file import", async () => { const mockFile = new File([""], "test.csv", { type: "text/csv" }); - vi.mocked(rawStudy.importFile).mockResolvedValue(); + vi.mocked(rawStudy.uploadFile).mockResolvedValue(); const hook = await setupHook(); @@ -174,7 +174,7 @@ describe("useMatrix", () => { await hook.result.current.handleUpload(mockFile); }); - expect(rawStudy.importFile).toHaveBeenCalledWith({ + expect(rawStudy.uploadFile).toHaveBeenCalledWith({ file: mockFile, studyId: DATA.studyId, path: DATA.url, diff --git a/webapp/src/components/common/buttons/DownloadMatrixButton.tsx b/webapp/src/components/common/buttons/DownloadMatrixButton.tsx index 55e0d029c3..0f27f75eef 100644 --- a/webapp/src/components/common/buttons/DownloadMatrixButton.tsx +++ b/webapp/src/components/common/buttons/DownloadMatrixButton.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { downloadMatrix } from "../../../services/api/studies/raw"; +import { getMatrixFile } from "../../../services/api/studies/raw"; import { downloadFile } from "../../../utils/fileUtils"; import { StudyMetadata } from "../../../common/types"; import { useTranslation } from "react-i18next"; @@ -51,7 +51,7 @@ function DownloadMatrixButton(props: DownloadMatrixButtonProps) { const isXlsx = format === "xlsx"; - const res = await downloadMatrix({ + const matrixFile = await getMatrixFile({ studyId, path, format, @@ -62,7 +62,7 @@ function DownloadMatrixButton(props: DownloadMatrixButtonProps) { const extension = format === "csv (semicolon)" ? "csv" : format; return downloadFile( - res, + matrixFile, `matrix_${studyId}_${path.replace("/", "_")}.${extension}`, ); }; diff --git a/webapp/src/components/common/buttons/UploadFileButton.tsx b/webapp/src/components/common/buttons/UploadFileButton.tsx index cc16594be0..872b05b01a 100644 --- a/webapp/src/components/common/buttons/UploadFileButton.tsx +++ b/webapp/src/components/common/buttons/UploadFileButton.tsx @@ -21,7 +21,7 @@ import { toError } from "../../../utils/fnUtils"; import { Accept, useDropzone } from "react-dropzone"; import { StudyMetadata } from "../../../common/types"; import { useSnackbar } from "notistack"; -import { importFile } from "../../../services/api/studies/raw"; +import { uploadFile } from "../../../services/api/studies/raw"; type ValidateResult = boolean | null | undefined; type Validate = (file: File) => ValidateResult | Promise; @@ -89,7 +89,7 @@ function UploadFileButton(props: UploadFileButtonProps) { const filePath = typeof path === "function" ? path(fileToUpload) : path; - await importFile({ + await uploadFile({ studyId, path: filePath, file: fileToUpload, diff --git a/webapp/src/services/api/studies/raw/index.ts b/webapp/src/services/api/studies/raw/index.ts index b334611562..4e2938c4a6 100644 --- a/webapp/src/services/api/studies/raw/index.ts +++ b/webapp/src/services/api/studies/raw/index.ts @@ -15,29 +15,56 @@ import client from "../../client"; import type { DeleteFileParams, - DownloadMatrixParams, - ImportFileParams, + GetMatrixFileParams, + GetRawFileParams, RawFile, + UploadFileParams, } from "./types"; -export async function downloadMatrix(params: DownloadMatrixParams) { +/** + * Gets a matrix file from a study's raw files. + * + * @param params - Parameters for getting the matrix + * @param params.studyId - Unique identifier of the study + * @param params.path - Path to the matrix file + * @param params.format - Optional. Export format for the matrix + * @param params.header - Optional. Whether to include headers + * @param params.index - Optional. Whether to include indices + * @returns Promise containing the matrix data as a Blob + */ +export async function getMatrixFile(params: GetMatrixFileParams) { const { studyId, ...queryParams } = params; - const url = `/v1/studies/${studyId}/raw/download`; - - const { data } = await client.get(url, { - params: queryParams, - responseType: "blob", - }); + const { data } = await client.get( + `/v1/studies/${studyId}/raw/download`, + { + params: queryParams, + responseType: "blob", + }, + ); return data; } -export async function importFile(params: ImportFileParams) { +/** + * Uploads a file to a study's raw storage, creating or updating it based on existence. + * + * !Warning: This endpoint currently uses a non-standard REST structure (/raw) which + * may lead to confusion. It handles both create and update operations through PUT, + * while directory creation is managed through a separate flag. + * + * @param params - Parameters for the file upload + * @param params.studyId - Unique identifier of the study + * @param params.path - Destination path for the file + * @param params.file - File content to upload + * @param params.createMissing - Optional. Whether to create missing parent directories + * @param params.onUploadProgress - Optional. Callback for upload progress updates + * @returns Promise that resolves when the upload is complete + */ +export async function uploadFile(params: UploadFileParams) { const { studyId, file, onUploadProgress, ...queryParams } = params; - const url = `/v1/studies/${studyId}/raw`; const body = { file }; - await client.putForm(url, body, { + await client.putForm(`/v1/studies/${studyId}/raw`, body, { params: { ...queryParams, create_missing: queryParams.createMissing, @@ -46,46 +73,54 @@ export async function importFile(params: ImportFileParams) { }); } +/** + * Deletes a raw file from a study. + * + * @param params - Parameters for deleting the file + * @param params.studyId - Unique identifier of the study + * @param params.path - Path to the file to delete + * @returns Promise that resolves when the deletion is complete + */ export async function deleteFile(params: DeleteFileParams) { const { studyId, path } = params; - const url = `/v1/studies/${studyId}/raw`; - - await client.delete(url, { params: { path } }); + await client.delete(`/v1/studies/${studyId}/raw`, { params: { path } }); } /** - * Reads an original raw file from a study with its metadata. + * Gets an original raw file from a study with its metadata. * - * @param studyId - Unique identifier of the study - * @param filePath - Path to the file within the study + * @param params - Parameters for getting the raw file and name + * @param params.studyId - Unique identifier of the study + * @param params.path - Path to the file within the study * @returns Promise containing the file data and metadata */ -export async function getRawFile( - studyId: string, - filePath: string, -): Promise { - const response = await client.get( +export async function getRawFile(params: GetRawFileParams): Promise { + const { studyId, path } = params; + + const { data, headers } = await client.get( `/v1/studies/${studyId}/raw/original-file`, { params: { - path: filePath, + path, }, responseType: "blob", }, ); - const contentDisposition = response.headers["content-disposition"]; - let filename = filePath.split("/").pop() || "file"; // fallback filename + // Get the original file name from the response Headers + const contentDisposition = headers["content-disposition"]; + let filename = path.split("/").pop() || "file"; // fallback filename if (contentDisposition) { const matches = /filename=([^;]+)/.exec(contentDisposition); + if (matches?.[1]) { filename = matches[1].replace(/"/g, "").trim(); } } return { - data: response.data, - filename: filename, + data, + filename, }; } diff --git a/webapp/src/services/api/studies/raw/types.ts b/webapp/src/services/api/studies/raw/types.ts index 43ff274183..13bece37f8 100644 --- a/webapp/src/services/api/studies/raw/types.ts +++ b/webapp/src/services/api/studies/raw/types.ts @@ -17,9 +17,10 @@ import type { StudyMetadata } from "../../../../common/types"; import { O } from "ts-toolbelt"; import { TableExportFormat } from "./constants"; +// Available export formats for matrix tables export type TTableExportFormat = O.UnionOf; -export interface DownloadMatrixParams { +export interface GetMatrixFileParams { studyId: StudyMetadata["id"]; path: string; format?: TTableExportFormat; @@ -27,10 +28,11 @@ export interface DownloadMatrixParams { index?: boolean; } -export interface ImportFileParams { +export interface UploadFileParams { studyId: StudyMetadata["id"]; path: string; file: File; + // Flag to indicate whether to create file and directories if missing createMissing?: boolean; onUploadProgress?: AxiosRequestConfig["onUploadProgress"]; } @@ -40,6 +42,10 @@ export interface DeleteFileParams { path: string; } +export interface GetRawFileParams { + studyId: string; + path: string; +} export interface RawFile { data: Blob; filename: string;