From 077feebc6dcc20f75043079faa6d53aaf865393f Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 14 Nov 2024 10:10:39 +0100 Subject: [PATCH 01/42] feat(ui): add on click fetch and display list of non studies folder --- .../components/App/Studies/StudyTree.test.tsx | 185 ++++++++++++++++++ .../src/components/App/Studies/StudyTree.tsx | 95 ++++++++- webapp/src/components/App/Studies/utils.ts | 7 + webapp/src/services/api/study.ts | 24 +++ 4 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 webapp/src/components/App/Studies/StudyTree.test.tsx diff --git a/webapp/src/components/App/Studies/StudyTree.test.tsx b/webapp/src/components/App/Studies/StudyTree.test.tsx new file mode 100644 index 0000000000..1d4320b878 --- /dev/null +++ b/webapp/src/components/App/Studies/StudyTree.test.tsx @@ -0,0 +1,185 @@ +/** + * 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 { mergeStudyTreeAndFolders } from "./StudyTree"; +import { StudyTreeNode, NonStudyFolder } from "./utils"; + +describe("mergeStudyTreeAndFolder", () => { + test("should merge study tree and folder correctly", () => { + const studyTree: StudyTreeNode = { + name: "Root", + path: "/", + children: [ + { name: "a", path: "/a", children: [] }, + { name: "b", path: "/b", children: [] }, + ], + }; + const folder: NonStudyFolder = { + name: "folder1", + path: "folder1", + workspace: "a", + parentPath: "/a", + }; + + const result = mergeStudyTreeAndFolders(studyTree, [folder]); + + expect(result).toEqual({ + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "folder1", path: "/a/folder1", children: [] }], + }, + { name: "b", path: "/b", children: [] }, + ], + }); + }); + + // test("should handle empty study tree", () => { + // const studyTree: StudyTreeNode = { name: "Root", path: "/", children: [] }; + // const folder: NonStudyFolder = { + // name: "folder1", + // path: "folder1", + // workspace: "a", + // parentPath: "/", + // }; + + // const result = mergeStudyTreeAndFolders(studyTree, [folder]); + + // expect(result).toEqual({ + // name: "Root", + // path: "/", + // children: [{ name: "folder1", path: "/folder1", children: [] }], + // }); + // }); + + test("should handle nested study tree and folder correctly", () => { + const studyTree: StudyTreeNode = { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "suba", path: "/a/suba", children: [] }], + }, + ], + }; + const folder: NonStudyFolder = { + name: "folder1", + path: "suba/folder1", + workspace: "a", + parentPath: "/a/suba", + }; + + const result = mergeStudyTreeAndFolders(studyTree, [folder]); + + expect(result).toEqual({ + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [ + { + name: "suba", + path: "/a/suba", + children: [ + { name: "folder1", path: "/a/suba/folder1", children: [] }, + ], + }, + ], + }, + ], + }); + }); + + test("should not add duplicate folders", () => { + const studyTree: StudyTreeNode = { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "folder1", path: "/a/folder1", children: [] }], + }, + ], + }; + const folder: NonStudyFolder = { + name: "folder1", + path: "/folder1", + workspace: "a", + parentPath: "/a", + }; + + const result = mergeStudyTreeAndFolders(studyTree, [folder]); + + expect(result).toEqual({ + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "folder1", path: "/a/folder1", children: [] }], + }, + ], + }); + }); +}); + +describe("mergeStudyTreeAndFolders", () => { + test("should merge multiple folders correctly", () => { + const studyTree: StudyTreeNode = { + name: "Root", + path: "/", + children: [{ name: "a", path: "/a", children: [] }], + }; + const folders: NonStudyFolder[] = [ + { + name: "folder1", + path: "/folder1", + workspace: "a", + parentPath: "/a", + }, + { + name: "folder2", + path: "/folder2", + workspace: "a", + parentPath: "/a", + }, + ]; + + const result = mergeStudyTreeAndFolders(studyTree, folders); + + expect(result).toEqual({ + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [ + { name: "folder1", path: "/a/folder1", children: [] }, + { name: "folder2", path: "/a/folder2", children: [] }, + ], + }, + ], + }); + }); +}); diff --git a/webapp/src/components/App/Studies/StudyTree.tsx b/webapp/src/components/App/Studies/StudyTree.tsx index 638d475f50..e07ad9c8c6 100644 --- a/webapp/src/components/App/Studies/StudyTree.tsx +++ b/webapp/src/components/App/Studies/StudyTree.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { StudyTreeNode } from "./utils"; +import { NonStudyFolder, StudyTreeNode } from "./utils"; import useAppSelector from "../../../redux/hooks/useAppSelector"; import { getStudiesTree, getStudyFilters } from "../../../redux/selectors"; import useAppDispatch from "../../../redux/hooks/useAppDispatch"; @@ -21,18 +21,109 @@ import TreeItemEnhanced from "../../common/TreeItemEnhanced"; import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; import { getParentPaths } from "../../../utils/pathUtils"; import * as R from "ramda"; +import { useEffect, useState } from "react"; +import * as api from "../../../services/api/study"; + +/** + * Add a folder that was returned by the explorer into the study tree view. + * + * This folder isn't a study, otherwise it woudl'nt be + * returned by the explorer API, but this folder can have study in it + * and still not be in the initial study tree that's parsed from the study + * list, this happen when the studies in the folder aren't scanned yet. + * + * However we want to allow the user to see these folder. When the user explore + * they shoudln't wait for a long running scan to complete before they're able to + * a folder in the hierarchy. + * + * @param studiesTree + * @param folder + * @returns + */ +function mergeStudyTreeAndFolder( + studiesTree: StudyTreeNode, + folder: NonStudyFolder, +): StudyTreeNode { + if (folder.parentPath == studiesTree.path) { + for (let child of studiesTree.children) { + if (child.name == folder.name) { + // parent path is the same, folder name is the same + // we don't override the existing folder + // so we return here and don't update this node + return studiesTree; + } + } + // parent path is the same, but no folder with the same name at this level + return { + ...studiesTree, + children: [ + ...studiesTree.children, + { + path: `${folder.parentPath}/${folder.name}`, + name: folder.name, + children: [], + }, + ], + }; + } + // try to recursively merge in each childen + return { + ...studiesTree, + children: studiesTree.children.map((child) => + mergeStudyTreeAndFolder(child, folder), + ), + }; +} + +/** + * Merge several folders in the study tree. + * + * @param studiesTree + * @param folders + * @returns + */ +export function mergeStudyTreeAndFolders( + studiesTree: StudyTreeNode, + folders: NonStudyFolder[], +): StudyTreeNode { + for (let folder of folders) { + studiesTree = mergeStudyTreeAndFolder(studiesTree, folder); + } + return studiesTree; +} function StudyTree() { + const emptyNode: StudyTreeNode = { name: "", path: "", children: [] }; + const [studiesTree, setStudiesTree] = useState(emptyNode); + const [initializedFlag, setInitializedFlag] = useState(false); const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); - const studiesTree = useAppSelector(getStudiesTree); + const initialStudiesTree = useAppSelector(getStudiesTree); const dispatch = useAppDispatch(); + useEffect(() => { + if (!initializedFlag) { + setStudiesTree(initialStudiesTree); + setInitializedFlag(true); + } + }, [studiesTree]); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// const handleTreeItemClick = (itemId: string) => { dispatch(updateStudyFilters({ folder: itemId })); + // dispatch the action to fetch folders + if (itemId === "root") { + // Under root there're workspaces not subfolders + return; + } + const [_, workspace, ...other] = itemId.split("/"); + const subPath = other.join("/"); + api.getFolders(workspace, subPath).then((res) => { + const nextStudiesTree = mergeStudyTreeAndFolders(studiesTree, res); + setStudiesTree(nextStudiesTree); + }); }; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Studies/utils.ts b/webapp/src/components/App/Studies/utils.ts index dc2d1c5396..03eb4214bb 100644 --- a/webapp/src/components/App/Studies/utils.ts +++ b/webapp/src/components/App/Studies/utils.ts @@ -20,6 +20,13 @@ export interface StudyTreeNode { children: StudyTreeNode[]; } +export interface NonStudyFolder { + name: string; + path: string; + workspace: string; + parentPath: string; +} + const nodeProcess = ( tree: StudyTreeNode, path: string[], diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 8750b177f4..f66b933e68 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -34,6 +34,7 @@ import { getConfig } from "../config"; import { convertStudyDtoToMetadata } from "../utils"; import { FileDownloadTask } from "./downloads"; import { StudyMapDistrict } from "../../redux/ducks/studyMaps"; +import { NonStudyFolder } from "@/components/App/Studies/utils"; const getStudiesRaw = async (): Promise> => { const res = await client.get(`/v1/studies`); @@ -48,6 +49,29 @@ export const getStudies = async (): Promise => { }); }; +export const getFolders = async ( + workspace: string, + folder_name: string, +): Promise => { + const res = await client.get( + `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folder_name)}`, + ); + return res.data.map((folder: any) => { + let parentPath = [ + "", + folder.workspace, + ...folder.path + .split("/") + .filter((elm: string) => elm !== "") + .slice(0, -1), + ].join("/"); + return { + ...folder, + parentPath, + }; + }); +}; + export const getStudyVersions = async (): Promise => { const res = await client.get("/v1/studies/_versions"); return res.data; From 3860608f37691eb7fe7699b68a3c4b2ee65c66f4 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 14 Nov 2024 12:11:21 +0100 Subject: [PATCH 02/42] refactor(ui): fix build to pass npm lint add docs --- .../src/components/App/Studies/StudyTree.tsx | 57 ++++++++++++------- webapp/src/services/api/study.ts | 4 +- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree.tsx b/webapp/src/components/App/Studies/StudyTree.tsx index e07ad9c8c6..3841bde864 100644 --- a/webapp/src/components/App/Studies/StudyTree.tsx +++ b/webapp/src/components/App/Studies/StudyTree.tsx @@ -45,7 +45,7 @@ function mergeStudyTreeAndFolder( folder: NonStudyFolder, ): StudyTreeNode { if (folder.parentPath == studiesTree.path) { - for (let child of studiesTree.children) { + for (const child of studiesTree.children) { if (child.name == folder.name) { // parent path is the same, folder name is the same // we don't override the existing folder @@ -65,14 +65,19 @@ function mergeStudyTreeAndFolder( }, ], }; + } else if (folder.parentPath.startsWith(studiesTree.path)) { + // recursively walk though the tree + // recursively merge in each child + return { + ...studiesTree, + children: studiesTree.children.map((child) => + mergeStudyTreeAndFolder(child, folder), + ), + }; + } else { + // folder isn't part of this hierarchy + return studiesTree; } - // try to recursively merge in each childen - return { - ...studiesTree, - children: studiesTree.children.map((child) => - mergeStudyTreeAndFolder(child, folder), - ), - }; } /** @@ -86,12 +91,32 @@ export function mergeStudyTreeAndFolders( studiesTree: StudyTreeNode, folders: NonStudyFolder[], ): StudyTreeNode { - for (let folder of folders) { + for (const folder of folders) { studiesTree = mergeStudyTreeAndFolder(studiesTree, folder); } return studiesTree; } +async function fetchAndMergeSubfolders( + path: string, + studiesTree: StudyTreeNode, +): Promise { + if (path === "root") { + // Under root there're workspaces not subfolders + return studiesTree; + } + const pathParts = path.split("/"); + if (pathParts.length < 2) { + return studiesTree; + } + // path parts should be ["root", workspace, "foler1", ...] + const workspace = pathParts[1]; + const subPath = pathParts.slice(2).join("/"); + const subFolders = await api.getFolders(workspace, subPath); + const nextStudiesTree = mergeStudyTreeAndFolders(studiesTree, subFolders); + return nextStudiesTree; +} + function StudyTree() { const emptyNode: StudyTreeNode = { name: "", path: "", children: [] }; const [studiesTree, setStudiesTree] = useState(emptyNode); @@ -113,17 +138,9 @@ function StudyTree() { const handleTreeItemClick = (itemId: string) => { dispatch(updateStudyFilters({ folder: itemId })); - // dispatch the action to fetch folders - if (itemId === "root") { - // Under root there're workspaces not subfolders - return; - } - const [_, workspace, ...other] = itemId.split("/"); - const subPath = other.join("/"); - api.getFolders(workspace, subPath).then((res) => { - const nextStudiesTree = mergeStudyTreeAndFolders(studiesTree, res); - setStudiesTree(nextStudiesTree); - }); + fetchAndMergeSubfolders(itemId, studiesTree).then((nextStudiesTree) => + setStudiesTree(nextStudiesTree), + ); }; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index f66b933e68..1cd8b4b289 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -51,10 +51,10 @@ export const getStudies = async (): Promise => { export const getFolders = async ( workspace: string, - folder_name: string, + folderName: string, ): Promise => { const res = await client.get( - `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folder_name)}`, + `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folderName)}`, ); return res.data.map((folder: any) => { let parentPath = [ From fe91d5a0c764908c452d8f4165e904bcfd15c02a Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 14 Nov 2024 12:17:29 +0100 Subject: [PATCH 03/42] refactor(ui): make npm lint pass --- webapp/src/components/App/Studies/StudyTree.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree.tsx b/webapp/src/components/App/Studies/StudyTree.tsx index 3841bde864..20e8f13f6f 100644 --- a/webapp/src/components/App/Studies/StudyTree.tsx +++ b/webapp/src/components/App/Studies/StudyTree.tsx @@ -91,10 +91,11 @@ export function mergeStudyTreeAndFolders( studiesTree: StudyTreeNode, folders: NonStudyFolder[], ): StudyTreeNode { + let mergedStudyTree = studiesTree; for (const folder of folders) { - studiesTree = mergeStudyTreeAndFolder(studiesTree, folder); + mergedStudyTree = mergeStudyTreeAndFolder(mergedStudyTree, folder); } - return studiesTree; + return mergedStudyTree; } async function fetchAndMergeSubfolders( From 37bb38e1f109d109bc91a6dce3466f474a91f2d0 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 14 Nov 2024 12:27:25 +0100 Subject: [PATCH 04/42] refactor(ui): make npm lint pass --- webapp/src/services/api/study.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 1cd8b4b289..36a0801cfa 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -56,8 +56,8 @@ export const getFolders = async ( const res = await client.get( `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folderName)}`, ); - return res.data.map((folder: any) => { - let parentPath = [ + return res.data.map((folder: NonStudyFolder) => { + const parentPath = [ "", folder.workspace, ...folder.path From 4d8db33d49388ac784123cabe9aa577e29beacd5 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Mon, 18 Nov 2024 17:29:49 +0100 Subject: [PATCH 05/42] refactor(ui): move files, better naming --- .../{ => StudyTree}/StudyTree.test.tsx | 4 +- .../{StudyTree.tsx => StudyTree/index.tsx} | 122 +++++++++++------- 2 files changed, 77 insertions(+), 49 deletions(-) rename webapp/src/components/App/Studies/{ => StudyTree}/StudyTree.test.tsx (97%) rename webapp/src/components/App/Studies/{StudyTree.tsx => StudyTree/index.tsx} (57%) diff --git a/webapp/src/components/App/Studies/StudyTree.test.tsx b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx similarity index 97% rename from webapp/src/components/App/Studies/StudyTree.test.tsx rename to webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx index 1d4320b878..3b6f783688 100644 --- a/webapp/src/components/App/Studies/StudyTree.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx @@ -12,8 +12,8 @@ * This file is part of the Antares project. */ -import { mergeStudyTreeAndFolders } from "./StudyTree"; -import { StudyTreeNode, NonStudyFolder } from "./utils"; +import { mergeStudyTreeAndFolders } from "."; +import { StudyTreeNode, NonStudyFolder } from "../utils"; describe("mergeStudyTreeAndFolder", () => { test("should merge study tree and folder correctly", () => { diff --git a/webapp/src/components/App/Studies/StudyTree.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx similarity index 57% rename from webapp/src/components/App/Studies/StudyTree.tsx rename to webapp/src/components/App/Studies/StudyTree/index.tsx index 20e8f13f6f..468aed3991 100644 --- a/webapp/src/components/App/Studies/StudyTree.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -12,22 +12,22 @@ * This file is part of the Antares project. */ -import { NonStudyFolder, StudyTreeNode } from "./utils"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; -import { getStudiesTree, getStudyFilters } from "../../../redux/selectors"; -import useAppDispatch from "../../../redux/hooks/useAppDispatch"; -import { updateStudyFilters } from "../../../redux/ducks/studies"; -import TreeItemEnhanced from "../../common/TreeItemEnhanced"; +import { NonStudyFolder, StudyTreeNode } from ".././utils"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import { getStudiesTree, getStudyFilters } from "../../../../redux/selectors"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; +import { updateStudyFilters } from "../../../../redux/ducks/studies"; +import TreeItemEnhanced from "../../../common/TreeItemEnhanced"; import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; -import { getParentPaths } from "../../../utils/pathUtils"; +import { getParentPaths } from "../../../../utils/pathUtils"; import * as R from "ramda"; import { useEffect, useState } from "react"; -import * as api from "../../../services/api/study"; +import * as api from "../../../../services/api/study"; /** * Add a folder that was returned by the explorer into the study tree view. * - * This folder isn't a study, otherwise it woudl'nt be + * This folder isn't a study, otherwise it would'nt be * returned by the explorer API, but this folder can have study in it * and still not be in the initial study tree that's parsed from the study * list, this happen when the studies in the folder aren't scanned yet. @@ -36,23 +36,26 @@ import * as api from "../../../services/api/study"; * they shoudln't wait for a long running scan to complete before they're able to * a folder in the hierarchy. * - * @param studiesTree - * @param folder + * @param studiesTree study tree to merge the folder into + * @param folder folder to merge into the tree * @returns */ function mergeStudyTreeAndFolder( studiesTree: StudyTreeNode, folder: NonStudyFolder, ): StudyTreeNode { + // Early return if folder doesn't belong in this branch + if (!folder.parentPath.startsWith(studiesTree.path)) { + return studiesTree; + } + + // direct child case if (folder.parentPath == studiesTree.path) { - for (const child of studiesTree.children) { - if (child.name == folder.name) { - // parent path is the same, folder name is the same - // we don't override the existing folder - // so we return here and don't update this node - return studiesTree; - } - } + const folderExists = studiesTree.children.some( + (child) => child.name === folder.name, + ); + if (folderExists) return studiesTree; + // parent path is the same, but no folder with the same name at this level return { ...studiesTree, @@ -65,19 +68,15 @@ function mergeStudyTreeAndFolder( }, ], }; - } else if (folder.parentPath.startsWith(studiesTree.path)) { - // recursively walk though the tree - // recursively merge in each child - return { - ...studiesTree, - children: studiesTree.children.map((child) => - mergeStudyTreeAndFolder(child, folder), - ), - }; - } else { - // folder isn't part of this hierarchy - return studiesTree; } + + // not a direct child, but does belong to this branch so recursively walk though the tree + return { + ...studiesTree, + children: studiesTree.children.map((child) => + mergeStudyTreeAndFolder(child, folder), + ), + }; } /** @@ -91,11 +90,10 @@ export function mergeStudyTreeAndFolders( studiesTree: StudyTreeNode, folders: NonStudyFolder[], ): StudyTreeNode { - let mergedStudyTree = studiesTree; - for (const folder of folders) { - mergedStudyTree = mergeStudyTreeAndFolder(mergedStudyTree, folder); - } - return mergedStudyTree; + return folders.reduce( + (tree, folder) => mergeStudyTreeAndFolder(tree, folder), + studiesTree, + ); } async function fetchAndMergeSubfolders( @@ -119,29 +117,59 @@ async function fetchAndMergeSubfolders( } function StudyTree() { - const emptyNode: StudyTreeNode = { name: "", path: "", children: [] }; - const [studiesTree, setStudiesTree] = useState(emptyNode); - const [initializedFlag, setInitializedFlag] = useState(false); - const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); const initialStudiesTree = useAppSelector(getStudiesTree); + const [studiesTree, setStudiesTree] = useState(initialStudiesTree); + // const [initializedFlag, setInitializedFlag] = useState(false); + const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); const dispatch = useAppDispatch(); + // useEffect(() => { + // if (!initializedFlag) { + // setStudiesTree(initialStudiesTree); + // setInitializedFlag(true); + // } + // }, [studiesTree]); + + // Initialize folders once we have the tree useEffect(() => { - if (!initializedFlag) { - setStudiesTree(initialStudiesTree); - setInitializedFlag(true); + if (!folder || !initialStudiesTree.children.length) { + return; } - }, [studiesTree]); + // extract folder paths excluding root + const getFolderPaths = (path: string): string[] => + path.split("/").reduce((acc, _, i, parts) => { + const currentPath = parts.slice(0, i + 1).join("/"); + return currentPath !== "root" ? [...acc, currentPath] : acc; + }, []); + + // sequential folder fetching with error handling + const updateFolderTree = async () => { + try { + const finalTree = await getFolderPaths(folder).reduce( + async (treePromise, path) => { + const tree = await treePromise; + return fetchAndMergeSubfolders(path, tree); + }, + Promise.resolve(initialStudiesTree), + ); + + setStudiesTree(finalTree); + } catch (error) { + // here you can use a snackback error component if needed + console.error("Failed to initialize folders:", error); + } + }; + + updateFolderTree(); + }, [folder, initialStudiesTree]); //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// const handleTreeItemClick = (itemId: string) => { dispatch(updateStudyFilters({ folder: itemId })); - fetchAndMergeSubfolders(itemId, studiesTree).then((nextStudiesTree) => - setStudiesTree(nextStudiesTree), - ); + fetchAndMergeSubfolders(itemId, studiesTree).then(setStudiesTree); }; //////////////////////////////////////////////////////////////// From 65b8d8e224a64c2b8a4287c98afb34be3fbc9bf8 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Tue, 19 Nov 2024 19:48:04 +0100 Subject: [PATCH 06/42] refactor(ui): wip, add snack bar error alert for failed path load, refactor code into a functional style --- .../App/Studies/StudyTree/index.tsx | 100 +++++++++++------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 468aed3991..472152a6e1 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -23,6 +23,7 @@ import { getParentPaths } from "../../../../utils/pathUtils"; import * as R from "ramda"; import { useEffect, useState } from "react"; import * as api from "../../../../services/api/study"; +import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; /** * Add a folder that was returned by the explorer into the study tree view. @@ -108,7 +109,7 @@ async function fetchAndMergeSubfolders( if (pathParts.length < 2) { return studiesTree; } - // path parts should be ["root", workspace, "foler1", ...] + // path parts should be ["root", workspace, "folder1", ...] const workspace = pathParts[1]; const subPath = pathParts.slice(2).join("/"); const subFolders = await api.getFolders(workspace, subPath); @@ -116,52 +117,61 @@ async function fetchAndMergeSubfolders( return nextStudiesTree; } +async function fetchAndMergeSubfoldersForPathsV4( // reduce no try catch ( + paths: string[], + studiesTree: StudyTreeNode, +): Promise<[StudyTreeNode, string[]]> { + const emptyTree: StudyTreeNode = { name: "Root", path: "/", children: [] }; + const results: [StudyTreeNode, string][] = await Promise.all( + paths.map(async (path): Promise<[StudyTreeNode, string]> => { + try { + return [await fetchAndMergeSubfolders(path, studiesTree), ""]; + } catch (error) { + console.error("failed to load path ", path, error); + return [emptyTree, path]; + } + }), + ); + const finalTree = results + .filter((r) => r[0] !== emptyTree) + .map((r) => r[0]) + .reduce((acc, tree) => R.mergeDeepRight(acc, tree), studiesTree); + const failedPaths = results.map((r) => r[1]).filter((p) => p); + return [finalTree, failedPaths]; +} + +function getStudyTreeNode( + path: string, + stydyTree: StudyTreeNode, +): StudyTreeNode | null { + // path always start with root + const studyTreePath = `root${stydyTree.path}`; + if (studyTreePath === path) { + return stydyTree; + } + let result: StudyTreeNode | null = null; + for (const child of stydyTree.children) { + result = getStudyTreeNode(path, child); + if (result) { + return result; + } + } + return null; +} + function StudyTree() { const initialStudiesTree = useAppSelector(getStudiesTree); const [studiesTree, setStudiesTree] = useState(initialStudiesTree); - // const [initializedFlag, setInitializedFlag] = useState(false); const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); + const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const dispatch = useAppDispatch(); - // useEffect(() => { - // if (!initializedFlag) { - // setStudiesTree(initialStudiesTree); - // setInitializedFlag(true); - // } - // }, [studiesTree]); - // Initialize folders once we have the tree useEffect(() => { if (!folder || !initialStudiesTree.children.length) { return; } - - // extract folder paths excluding root - const getFolderPaths = (path: string): string[] => - path.split("/").reduce((acc, _, i, parts) => { - const currentPath = parts.slice(0, i + 1).join("/"); - return currentPath !== "root" ? [...acc, currentPath] : acc; - }, []); - - // sequential folder fetching with error handling - const updateFolderTree = async () => { - try { - const finalTree = await getFolderPaths(folder).reduce( - async (treePromise, path) => { - const tree = await treePromise; - return fetchAndMergeSubfolders(path, tree); - }, - Promise.resolve(initialStudiesTree), - ); - - setStudiesTree(finalTree); - } catch (error) { - // here you can use a snackback error component if needed - console.error("Failed to initialize folders:", error); - } - }; - - updateFolderTree(); + setStudiesTree(initialStudiesTree); }, [folder, initialStudiesTree]); //////////////////////////////////////////////////////////////// // Event Handlers @@ -169,7 +179,23 @@ function StudyTree() { const handleTreeItemClick = (itemId: string) => { dispatch(updateStudyFilters({ folder: itemId })); - fetchAndMergeSubfolders(itemId, studiesTree).then(setStudiesTree); + const currentNode = getStudyTreeNode(itemId, studiesTree); + if (!currentNode) { + console.error("Clicked on a non existing node", itemId); + return; + } + const chidrenPaths = currentNode.children.map( + (child) => `root${child.path}`, + ); + fetchAndMergeSubfoldersForPathsV4(chidrenPaths, studiesTree).then((r) => { + setStudiesTree(r[0]); + for (const path of r[1]) { + enqueueErrorSnackbar( + `Failed to initialize folders for : ${path}`, + "details in console.error", + ); + } + }); }; //////////////////////////////////////////////////////////////// From ea5950fe207f7ef2961d2d94a4a7a5f734b8852c Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 10:56:07 +0100 Subject: [PATCH 07/42] feat(ui): add workspace merge + optimize folder merge --- .../App/Studies/StudyTree/index.tsx | 56 ++++++++++++++----- webapp/src/services/api/study.ts | 5 ++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 472152a6e1..9fbe1b0464 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -24,6 +24,7 @@ import * as R from "ramda"; import { useEffect, useState } from "react"; import * as api from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; +import { set } from "lodash"; /** * Add a folder that was returned by the explorer into the study tree view. @@ -117,27 +118,44 @@ async function fetchAndMergeSubfolders( return nextStudiesTree; } -async function fetchAndMergeSubfoldersForPathsV4( // reduce no try catch ( +async function fetchAndMergeSubfoldersForPaths( // reduce try catch ( paths: string[], studiesTree: StudyTreeNode, ): Promise<[StudyTreeNode, string[]]> { - const emptyTree: StudyTreeNode = { name: "Root", path: "/", children: [] }; - const results: [StudyTreeNode, string][] = await Promise.all( - paths.map(async (path): Promise<[StudyTreeNode, string]> => { + return paths.reduce>( + async (acc, path) => { + const accRes = await acc; + const accTree: StudyTreeNode = accRes[0]; + const accFailedPaths: string[] = accRes[1]; try { - return [await fetchAndMergeSubfolders(path, studiesTree), ""]; + return [await fetchAndMergeSubfolders(path, accTree), accFailedPaths]; } catch (error) { console.error("failed to load path ", path, error); - return [emptyTree, path]; + return [accTree, [...accFailedPaths, path]]; } - }), + }, + Promise.resolve([studiesTree, []]), ); - const finalTree = results - .filter((r) => r[0] !== emptyTree) - .map((r) => r[0]) - .reduce((acc, tree) => R.mergeDeepRight(acc, tree), studiesTree); - const failedPaths = results.map((r) => r[1]).filter((p) => p); - return [finalTree, failedPaths]; +} + +function mergeNewWorkspace(workspace: string, stydyTree: StudyTreeNode) { + const emptyNode = { name: workspace, path: `/${workspace}`, children: [] }; + if (stydyTree.children.some((child) => child.name === workspace)) { + return stydyTree; + } + return { + ...stydyTree, + children: [...stydyTree.children, emptyNode], + }; +} + +function mergeNewWorkspaces( + workspaces: string[], + stydyTree: StudyTreeNode, +): StudyTreeNode { + return workspaces.reduce((acc, workspace) => { + return mergeNewWorkspace(workspace, acc); + }, stydyTree); } function getStudyTreeNode( @@ -171,7 +189,15 @@ function StudyTree() { if (!folder || !initialStudiesTree.children.length) { return; } - setStudiesTree(initialStudiesTree); + api + .getWorkspaces() + .then((workspaces) => { + setStudiesTree(mergeNewWorkspaces(workspaces, initialStudiesTree)); + }) + .catch((error) => { + enqueueErrorSnackbar("Failed to load list workspaces", error); + setStudiesTree(initialStudiesTree); + }); }, [folder, initialStudiesTree]); //////////////////////////////////////////////////////////////// // Event Handlers @@ -187,7 +213,7 @@ function StudyTree() { const chidrenPaths = currentNode.children.map( (child) => `root${child.path}`, ); - fetchAndMergeSubfoldersForPathsV4(chidrenPaths, studiesTree).then((r) => { + fetchAndMergeSubfoldersForPaths(chidrenPaths, studiesTree).then((r) => { setStudiesTree(r[0]); for (const path of r[1]) { enqueueErrorSnackbar( diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 36a0801cfa..6f1094296f 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -49,6 +49,11 @@ export const getStudies = async (): Promise => { }); }; +export const getWorkspaces = async (): Promise => { + const res = await client.get(`/v1/private/explorer/_list_workspaces`); + return res.data.map((folder: { name: string }) => folder.name); +}; + export const getFolders = async ( workspace: string, folderName: string, From c70db000837382c57b6ced047ee4a632def474c9 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 11:05:48 +0100 Subject: [PATCH 08/42] fix(ui): fix build --- webapp/src/components/App/Studies/StudyTree/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 9fbe1b0464..3661188f20 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -24,7 +24,6 @@ import * as R from "ramda"; import { useEffect, useState } from "react"; import * as api from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; -import { set } from "lodash"; /** * Add a folder that was returned by the explorer into the study tree view. From 360cd2d8b013383d1634302ac9112f4b8c192851 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 14:10:29 +0100 Subject: [PATCH 09/42] fix(ui): fix build for real --- webapp/src/components/App/Studies/StudyTree/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 3661188f20..03200d9a6f 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -55,8 +55,9 @@ function mergeStudyTreeAndFolder( const folderExists = studiesTree.children.some( (child) => child.name === folder.name, ); - if (folderExists) return studiesTree; - + if (folderExists) { + return studiesTree; + } // parent path is the same, but no folder with the same name at this level return { ...studiesTree, From 10a722310138fa4dcd8a0284fff19e72ba280ad9 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 16:25:38 +0100 Subject: [PATCH 10/42] refactor(ui): refactor some test + ui --- .../App/Studies/StudyTree/StudyTree.test.tsx | 207 ++++-------------- .../App/Studies/StudyTree/fixtures.ts | 156 +++++++++++++ .../App/Studies/StudyTree/index.tsx | 34 +-- webapp/src/components/App/Studies/utils.ts | 5 +- webapp/src/services/api/study.ts | 5 +- 5 files changed, 221 insertions(+), 186 deletions(-) create mode 100644 webapp/src/components/App/Studies/StudyTree/fixtures.ts diff --git a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx index 3b6f783688..fd71564fd4 100644 --- a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx @@ -11,175 +11,54 @@ * * This file is part of the Antares project. */ - import { mergeStudyTreeAndFolders } from "."; -import { StudyTreeNode, NonStudyFolder } from "../utils"; - -describe("mergeStudyTreeAndFolder", () => { - test("should merge study tree and folder correctly", () => { - const studyTree: StudyTreeNode = { - name: "Root", - path: "/", - children: [ - { name: "a", path: "/a", children: [] }, - { name: "b", path: "/b", children: [] }, - ], - }; - const folder: NonStudyFolder = { - name: "folder1", - path: "folder1", - workspace: "a", - parentPath: "/a", - }; - - const result = mergeStudyTreeAndFolders(studyTree, [folder]); - - expect(result).toEqual({ - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "folder1", path: "/a/folder1", children: [] }], - }, - { name: "b", path: "/b", children: [] }, - ], - }); - }); - - // test("should handle empty study tree", () => { - // const studyTree: StudyTreeNode = { name: "Root", path: "/", children: [] }; - // const folder: NonStudyFolder = { - // name: "folder1", - // path: "folder1", - // workspace: "a", - // parentPath: "/", - // }; - - // const result = mergeStudyTreeAndFolders(studyTree, [folder]); - - // expect(result).toEqual({ - // name: "Root", - // path: "/", - // children: [{ name: "folder1", path: "/folder1", children: [] }], - // }); - // }); - - test("should handle nested study tree and folder correctly", () => { - const studyTree: StudyTreeNode = { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "suba", path: "/a/suba", children: [] }], - }, - ], - }; - const folder: NonStudyFolder = { - name: "folder1", - path: "suba/folder1", - workspace: "a", - parentPath: "/a/suba", - }; - - const result = mergeStudyTreeAndFolders(studyTree, [folder]); - - expect(result).toEqual({ - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [ - { - name: "suba", - path: "/a/suba", - children: [ - { name: "folder1", path: "/a/suba/folder1", children: [] }, - ], - }, - ], - }, - ], +import { NonStudyFolder, StudyTreeNode } from "../utils"; +import { FIXTURES } from "./fixtures"; + +describe("StudyTree Utils", () => { + describe("mergeStudyTreeAndFolders", () => { + test.each(Object.values(FIXTURES))( + "$name", + ({ studyTree, folders, expected }) => { + const result = mergeStudyTreeAndFolders(studyTree, folders); + expect(result).toEqual(expected); + }, + ); + + test("should handle empty study tree", () => { + const emptyTree: StudyTreeNode = { + name: "Root", + path: "/", + children: [], + }; + const result = mergeStudyTreeAndFolders(emptyTree, []); + expect(result).toEqual(emptyTree); }); - }); - test("should not add duplicate folders", () => { - const studyTree: StudyTreeNode = { - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "folder1", path: "/a/folder1", children: [] }], - }, - ], - }; - const folder: NonStudyFolder = { - name: "folder1", - path: "/folder1", - workspace: "a", - parentPath: "/a", - }; - - const result = mergeStudyTreeAndFolders(studyTree, [folder]); - - expect(result).toEqual({ - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [{ name: "folder1", path: "/a/folder1", children: [] }], - }, - ], + test("should handle empty folders array", () => { + const tree: StudyTreeNode = { + name: "Root", + path: "/", + children: [{ name: "a", path: "/a", children: [] }], + }; + const result = mergeStudyTreeAndFolders(tree, []); + expect(result).toEqual(tree); }); - }); -}); - -describe("mergeStudyTreeAndFolders", () => { - test("should merge multiple folders correctly", () => { - const studyTree: StudyTreeNode = { - name: "Root", - path: "/", - children: [{ name: "a", path: "/a", children: [] }], - }; - const folders: NonStudyFolder[] = [ - { - name: "folder1", - path: "/folder1", - workspace: "a", - parentPath: "/a", - }, - { - name: "folder2", - path: "/folder2", - workspace: "a", - parentPath: "/a", - }, - ]; - - const result = mergeStudyTreeAndFolders(studyTree, folders); - expect(result).toEqual({ - name: "Root", - path: "/", - children: [ - { - name: "a", - path: "/a", - children: [ - { name: "folder1", path: "/a/folder1", children: [] }, - { name: "folder2", path: "/a/folder2", children: [] }, - ], - }, - ], + test("should handle invalid parent paths", () => { + const tree: StudyTreeNode = { + name: "Root", + path: "/", + children: [{ name: "a", path: "/a", children: [] }], + }; + const invalidFolder: NonStudyFolder = { + name: "invalid", + path: "/invalid", + workspace: "nonexistent", + parentPath: "/nonexistent", + }; + const result = mergeStudyTreeAndFolders(tree, [invalidFolder]); + expect(result).toEqual(tree); }); }); }); diff --git a/webapp/src/components/App/Studies/StudyTree/fixtures.ts b/webapp/src/components/App/Studies/StudyTree/fixtures.ts new file mode 100644 index 0000000000..21245749fb --- /dev/null +++ b/webapp/src/components/App/Studies/StudyTree/fixtures.ts @@ -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. + */ +export const FIXTURES = { + basicTree: { + name: "Basic tree with single level", + studyTree: { + name: "Root", + path: "/", + children: [ + { name: "a", path: "/a", children: [] }, + { name: "b", path: "/b", children: [] }, + ], + }, + folders: [ + { + name: "folder1", + path: "folder1", + workspace: "a", + parentPath: "/a", + }, + ], + expected: { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "folder1", path: "/a/folder1", children: [] }], + }, + { name: "b", path: "/b", children: [] }, + ], + }, + }, + nestedTree: { + name: "Nested tree structure", + studyTree: { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "suba", path: "/a/suba", children: [] }], + }, + ], + }, + folders: [ + { + name: "folder1", + path: "suba/folder1", + workspace: "a", + parentPath: "/a/suba", + }, + ], + expected: { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [ + { + name: "suba", + path: "/a/suba", + children: [ + { name: "folder1", path: "/a/suba/folder1", children: [] }, + ], + }, + ], + }, + ], + }, + }, + duplicateCase: { + name: "Tree with potential duplicates", + studyTree: { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "folder1", path: "/a/folder1", children: [] }], + }, + ], + }, + folders: [ + { + name: "folder1", + path: "/folder1", + workspace: "a", + parentPath: "/a", + }, + ], + expected: { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "folder1", path: "/a/folder1", children: [] }], + }, + ], + }, + }, + multipleFolders: { + name: "Multiple folders merge", + studyTree: { + name: "Root", + path: "/", + children: [{ name: "a", path: "/a", children: [] }], + }, + folders: [ + { + name: "folder1", + path: "/folder1", + workspace: "a", + parentPath: "/a", + }, + { + name: "folder2", + path: "/folder2", + workspace: "a", + parentPath: "/a", + }, + ], + expected: { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [ + { name: "folder1", path: "/a/folder1", children: [] }, + { name: "folder2", path: "/a/folder2", children: [] }, + ], + }, + ], + }, + }, +}; diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 03200d9a6f..54543ea335 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -24,11 +24,12 @@ import * as R from "ramda"; import { useEffect, useState } from "react"; import * as api from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; +import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; /** * Add a folder that was returned by the explorer into the study tree view. * - * This folder isn't a study, otherwise it would'nt be + * This folder isn't a study, otherwise it wouldn't be * returned by the explorer API, but this folder can have study in it * and still not be in the initial study tree that's parsed from the study * list, this happen when the studies in the folder aren't scanned yet. @@ -118,15 +119,13 @@ async function fetchAndMergeSubfolders( return nextStudiesTree; } -async function fetchAndMergeSubfoldersForPaths( // reduce try catch ( +async function fetchAndMergeSubfoldersForPaths( paths: string[], studiesTree: StudyTreeNode, ): Promise<[StudyTreeNode, string[]]> { - return paths.reduce>( + return paths.reduce( async (acc, path) => { - const accRes = await acc; - const accTree: StudyTreeNode = accRes[0]; - const accFailedPaths: string[] = accRes[1]; + const [accTree, accFailedPaths] = await acc; try { return [await fetchAndMergeSubfolders(path, accTree), accFailedPaths]; } catch (error) { @@ -134,7 +133,7 @@ async function fetchAndMergeSubfoldersForPaths( // reduce try catch ( return [accTree, [...accFailedPaths, path]]; } }, - Promise.resolve([studiesTree, []]), + Promise.resolve<[StudyTreeNode, string[]]>([studiesTree, []]), ); } @@ -158,6 +157,13 @@ function mergeNewWorkspaces( }, stydyTree); } +async function fetchAndMergeWorkspace( + studyTree: StudyTreeNode, +): Promise { + const workspaces = await api.getWorkspaces(); + return mergeNewWorkspaces(workspaces, studyTree); +} + function getStudyTreeNode( path: string, stydyTree: StudyTreeNode, @@ -185,20 +191,14 @@ function StudyTree() { const dispatch = useAppDispatch(); // Initialize folders once we have the tree - useEffect(() => { - if (!folder || !initialStudiesTree.children.length) { - return; - } - api - .getWorkspaces() - .then((workspaces) => { - setStudiesTree(mergeNewWorkspaces(workspaces, initialStudiesTree)); - }) + useUpdateEffectOnce(() => { + fetchAndMergeWorkspace(initialStudiesTree) + .then(setStudiesTree) .catch((error) => { enqueueErrorSnackbar("Failed to load list workspaces", error); setStudiesTree(initialStudiesTree); }); - }, [folder, initialStudiesTree]); + }, [initialStudiesTree]); //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Studies/utils.ts b/webapp/src/components/App/Studies/utils.ts index 03eb4214bb..ab245d16a8 100644 --- a/webapp/src/components/App/Studies/utils.ts +++ b/webapp/src/components/App/Studies/utils.ts @@ -20,10 +20,13 @@ export interface StudyTreeNode { children: StudyTreeNode[]; } -export interface NonStudyFolder { +export interface NonStudyFolderDTO { name: string; path: string; workspace: string; +} + +export interface NonStudyFolder extends NonStudyFolderDTO { parentPath: string; } diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 6f1094296f..d014ed457b 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -65,10 +65,7 @@ export const getFolders = async ( const parentPath = [ "", folder.workspace, - ...folder.path - .split("/") - .filter((elm: string) => elm !== "") - .slice(0, -1), + ...folder.path.split("/").filter(Boolean).slice(0, -1), ].join("/"); return { ...folder, From 8bc68822c9b7243a6a6d92e28c2d4b8e2dbe1bbd Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 16:26:57 +0100 Subject: [PATCH 11/42] refactor(ui): refactor some test + ui --- webapp/src/components/App/Studies/StudyTree/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 54543ea335..6b8fb19635 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -21,7 +21,7 @@ import TreeItemEnhanced from "../../../common/TreeItemEnhanced"; import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; import { getParentPaths } from "../../../../utils/pathUtils"; import * as R from "ramda"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import * as api from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; From 3c3e76b82e444c31dec31f2c7a49cec60552e9aa Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 16:28:52 +0100 Subject: [PATCH 12/42] fix(ui): fix build --- webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx | 1 + webapp/src/components/App/Studies/StudyTree/fixtures.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx index fd71564fd4..3c05c92a42 100644 --- a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx @@ -11,6 +11,7 @@ * * This file is part of the Antares project. */ + import { mergeStudyTreeAndFolders } from "."; import { NonStudyFolder, StudyTreeNode } from "../utils"; import { FIXTURES } from "./fixtures"; diff --git a/webapp/src/components/App/Studies/StudyTree/fixtures.ts b/webapp/src/components/App/Studies/StudyTree/fixtures.ts index 21245749fb..a6a4dbc3ad 100644 --- a/webapp/src/components/App/Studies/StudyTree/fixtures.ts +++ b/webapp/src/components/App/Studies/StudyTree/fixtures.ts @@ -11,6 +11,7 @@ * * This file is part of the Antares project. */ + export const FIXTURES = { basicTree: { name: "Basic tree with single level", From ed08ddc0d9b2e6e22476703812e03b218b6cb805 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 18:13:06 +0100 Subject: [PATCH 13/42] refactor(ui): remove useless code, add doc, better naming --- .../App/Studies/StudyTree/index.tsx | 133 ++++++++++-------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 6b8fb19635..52d59b4145 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -38,11 +38,12 @@ import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; * they shoudln't wait for a long running scan to complete before they're able to * a folder in the hierarchy. * - * @param studiesTree study tree to merge the folder into - * @param folder folder to merge into the tree - * @returns + * @param studiesTree study tree to insert the folder into + * @param folder folder to inert into the tree + * @returns study tree with the folder inserted if it wasn't already there. + * New branch is created if it contain the folder otherwise the branch is left unchanged. */ -function mergeStudyTreeAndFolder( +function insertFolderIfNotExist( studiesTree: StudyTreeNode, folder: NonStudyFolder, ): StudyTreeNode { @@ -77,49 +78,65 @@ function mergeStudyTreeAndFolder( return { ...studiesTree, children: studiesTree.children.map((child) => - mergeStudyTreeAndFolder(child, folder), + insertFolderIfNotExist(child, folder), ), }; } /** - * Merge several folders in the study tree. + * Insert several folders in the study tree if they don't exist already in the tree. * - * @param studiesTree - * @param folders - * @returns + * @param studiesTree study tree to insert the folder into + * @param folders folders to inert into the tree + * @param studiesTree study tree to insert the folder into + * @param folder folder to inert into the tree + * @returns study tree with the folder inserted if it wasn't already there. + * New branch is created if it contain the folder otherwise the branch is left unchanged. */ -export function mergeStudyTreeAndFolders( +export function insertFoldersIfNotExist( studiesTree: StudyTreeNode, folders: NonStudyFolder[], ): StudyTreeNode { return folders.reduce( - (tree, folder) => mergeStudyTreeAndFolder(tree, folder), + (tree, folder) => insertFolderIfNotExist(tree, folder), studiesTree, ); } -async function fetchAndMergeSubfolders( - path: string, - studiesTree: StudyTreeNode, -): Promise { +/** + * Call the explorer api to fetch the subfolders under the given path. + * + * @param path path of the subfolder to fetch, should sart with root, e.g. root/workspace/folder1 + * @returns list of subfolders under the given path + */ +async function fetchSubfolders(path: string): Promise { if (path === "root") { // Under root there're workspaces not subfolders - return studiesTree; + return []; } + // less than 2 parts means we're at the root level const pathParts = path.split("/"); if (pathParts.length < 2) { - return studiesTree; + return []; } // path parts should be ["root", workspace, "folder1", ...] const workspace = pathParts[1]; const subPath = pathParts.slice(2).join("/"); - const subFolders = await api.getFolders(workspace, subPath); - const nextStudiesTree = mergeStudyTreeAndFolders(studiesTree, subFolders); - return nextStudiesTree; + return await api.getFolders(workspace, subPath); } -async function fetchAndMergeSubfoldersForPaths( +/** + * Fetch and insert the subfolders under the given paths into the study tree. + * + * This function is used to fill the study tree when the user clicks on a folder. + * + * Subfolders are inserted only if they don't exist already in the tree. + * + * @param paths list of paths to fetch the subfolders for + * @param studiesTree study tree to insert the subfolders into + * @returns study tree with the subfolders inserted if they weren't already there. + */ +async function fetchAndInsertSubfolders( paths: string[], studiesTree: StudyTreeNode, ): Promise<[StudyTreeNode, string[]]> { @@ -127,7 +144,8 @@ async function fetchAndMergeSubfoldersForPaths( async (acc, path) => { const [accTree, accFailedPaths] = await acc; try { - return [await fetchAndMergeSubfolders(path, accTree), accFailedPaths]; + const subfolders = await fetchSubfolders(path); + return [insertFoldersIfNotExist(accTree, subfolders), accFailedPaths]; } catch (error) { console.error("failed to load path ", path, error); return [accTree, [...accFailedPaths, path]]; @@ -137,7 +155,17 @@ async function fetchAndMergeSubfoldersForPaths( ); } -function mergeNewWorkspace(workspace: string, stydyTree: StudyTreeNode) { +/** + * Insert a workspace into the study tree if it doesn't exist already. + * + * @param workspace key of the workspace + * @param stydyTree study tree to insert the workspace into + * @returns study tree with the empty workspace inserted if it wasn't already there. + */ +function insertWorkspaceIfNotExist( + stydyTree: StudyTreeNode, + workspace: string, +) { const emptyNode = { name: workspace, path: `/${workspace}`, children: [] }; if (stydyTree.children.some((child) => child.name === workspace)) { return stydyTree; @@ -148,39 +176,32 @@ function mergeNewWorkspace(workspace: string, stydyTree: StudyTreeNode) { }; } -function mergeNewWorkspaces( - workspaces: string[], +/** + * Insert several workspaces into the study tree if they don't exist already in the tree. + * + * @param workspaces workspaces to insert into the tree + * @param stydyTree study tree to insert the workspaces into + * @returns study tree with the empty workspaces inserted if they weren't already there. + */ +export function insertWorkspacesIfNotExist( stydyTree: StudyTreeNode, + workspaces: string[], ): StudyTreeNode { return workspaces.reduce((acc, workspace) => { - return mergeNewWorkspace(workspace, acc); + return insertWorkspaceIfNotExist(acc, workspace); }, stydyTree); } -async function fetchAndMergeWorkspace( +/** + * fetch and insert the workspaces into the study tree. + * @param studyTree study tree to insert the workspaces into + * @returns study tree with the workspaces inserted if they weren't already there. + */ +async function fetchAndInsertWorkspaces( studyTree: StudyTreeNode, ): Promise { const workspaces = await api.getWorkspaces(); - return mergeNewWorkspaces(workspaces, studyTree); -} - -function getStudyTreeNode( - path: string, - stydyTree: StudyTreeNode, -): StudyTreeNode | null { - // path always start with root - const studyTreePath = `root${stydyTree.path}`; - if (studyTreePath === path) { - return stydyTree; - } - let result: StudyTreeNode | null = null; - for (const child of stydyTree.children) { - result = getStudyTreeNode(path, child); - if (result) { - return result; - } - } - return null; + return insertWorkspacesIfNotExist(studyTree, workspaces); } function StudyTree() { @@ -192,7 +213,7 @@ function StudyTree() { // Initialize folders once we have the tree useUpdateEffectOnce(() => { - fetchAndMergeWorkspace(initialStudiesTree) + fetchAndInsertWorkspaces(initialStudiesTree) .then(setStudiesTree) .catch((error) => { enqueueErrorSnackbar("Failed to load list workspaces", error); @@ -203,17 +224,15 @@ function StudyTree() { // Event Handlers //////////////////////////////////////////////////////////////// - const handleTreeItemClick = (itemId: string) => { + const handleTreeItemClick = ( + itemId: string, + studyTreeNode: StudyTreeNode, + ) => { dispatch(updateStudyFilters({ folder: itemId })); - const currentNode = getStudyTreeNode(itemId, studiesTree); - if (!currentNode) { - console.error("Clicked on a non existing node", itemId); - return; - } - const chidrenPaths = currentNode.children.map( + const chidrenPaths = studyTreeNode.children.map( (child) => `root${child.path}`, ); - fetchAndMergeSubfoldersForPaths(chidrenPaths, studiesTree).then((r) => { + fetchAndInsertSubfolders(chidrenPaths, studiesTree).then((r) => { setStudiesTree(r[0]); for (const path of r[1]) { enqueueErrorSnackbar( @@ -237,7 +256,7 @@ function StudyTree() { key={id} itemId={id} label={elm.name} - onClick={() => handleTreeItemClick(id)} + onClick={() => handleTreeItemClick(id, elm)} > {buildTree(elm.children, id)} From 968060de099848d158276a6c248486b24fa4011c Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 18:13:27 +0100 Subject: [PATCH 14/42] fix(ui): fix tests --- .../App/Studies/StudyTree/StudyTree.test.tsx | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx index 3c05c92a42..729c8022aa 100644 --- a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { mergeStudyTreeAndFolders } from "."; +import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "."; import { NonStudyFolder, StudyTreeNode } from "../utils"; import { FIXTURES } from "./fixtures"; @@ -21,7 +21,7 @@ describe("StudyTree Utils", () => { test.each(Object.values(FIXTURES))( "$name", ({ studyTree, folders, expected }) => { - const result = mergeStudyTreeAndFolders(studyTree, folders); + const result = insertFoldersIfNotExist(studyTree, folders); expect(result).toEqual(expected); }, ); @@ -32,7 +32,7 @@ describe("StudyTree Utils", () => { path: "/", children: [], }; - const result = mergeStudyTreeAndFolders(emptyTree, []); + const result = insertFoldersIfNotExist(emptyTree, []); expect(result).toEqual(emptyTree); }); @@ -42,7 +42,7 @@ describe("StudyTree Utils", () => { path: "/", children: [{ name: "a", path: "/a", children: [] }], }; - const result = mergeStudyTreeAndFolders(tree, []); + const result = insertFoldersIfNotExist(tree, []); expect(result).toEqual(tree); }); @@ -58,8 +58,56 @@ describe("StudyTree Utils", () => { workspace: "nonexistent", parentPath: "/nonexistent", }; - const result = mergeStudyTreeAndFolders(tree, [invalidFolder]); + const result = insertFoldersIfNotExist(tree, [invalidFolder]); expect(result).toEqual(tree); }); + + test("should handle empty workspaces", () => { + const tree: StudyTreeNode = { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "suba", path: "/a/suba", children: [] }], + }, + ], + }; + const workspaces: string[] = []; + const result = insertWorkspacesIfNotExist(tree, workspaces); + expect(result).toEqual(tree); + }); + + test("should merge workspaces", () => { + const tree: StudyTreeNode = { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "suba", path: "/a/suba", children: [] }], + }, + ], + }; + const expected: StudyTreeNode = { + name: "Root", + path: "/", + children: [ + { + name: "a", + path: "/a", + children: [{ name: "suba", path: "/a/suba", children: [] }], + }, + { name: "workspace1", path: "/workspace1", children: [] }, + { name: "workspace2", path: "/workspace2", children: [] }, + ], + }; + + const workspaces: string[] = ["a", "workspace1", "workspace2"]; + const result = insertWorkspacesIfNotExist(tree, workspaces); + expect(result).toEqual(expected); + }); }); }); From ee8bc0e73f620b0a77a6b0d231e93db3d7904fce Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 20 Nov 2024 18:16:43 +0100 Subject: [PATCH 15/42] feat(ui): handle click on root --- webapp/src/components/App/Studies/StudyTree/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 52d59b4145..7a82a44340 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -229,6 +229,14 @@ function StudyTree() { studyTreeNode: StudyTreeNode, ) => { dispatch(updateStudyFilters({ folder: itemId })); + if (itemId === "root") { + fetchAndInsertWorkspaces(studiesTree) + .then(setStudiesTree) + .catch((error) => { + enqueueErrorSnackbar("Failed to load list workspaces", error); + }); + return; + } const chidrenPaths = studyTreeNode.children.map( (child) => `root${child.path}`, ); From f946e45e136719571dfa213dec097a9c05eeef83 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 21 Nov 2024 10:14:56 +0100 Subject: [PATCH 16/42] refactor(ui): add docs, renaming --- webapp/src/services/api/study.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index d014ed457b..003cf0125a 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -54,14 +54,20 @@ export const getWorkspaces = async (): Promise => { return res.data.map((folder: { name: string }) => folder.name); }; +/** + * Call the explorer API to get the list of folders in a workspace + * @param workspace workspace name + * @param folderPath path starting from the workspace root (not including the workspace name) + * @returns + */ export const getFolders = async ( workspace: string, - folderName: string, + folderPath: string, ): Promise => { - const res = await client.get( - `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folderName)}`, + const res = await client.get( + `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folderPath)}`, ); - return res.data.map((folder: NonStudyFolder) => { + return res.data.map((folder) => { const parentPath = [ "", folder.workspace, From fb0da14d3bc49bdfb3f4eb0b429620ab1c7f7fe6 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 21 Nov 2024 10:57:31 +0100 Subject: [PATCH 17/42] fix(ui): fis bug first level of folder wasn't initialized --- .../components/App/Studies/StudyTree/index.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 7a82a44340..71b8f6ffe1 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -214,7 +214,19 @@ function StudyTree() { // Initialize folders once we have the tree useUpdateEffectOnce(() => { fetchAndInsertWorkspaces(initialStudiesTree) - .then(setStudiesTree) + .then((tree) => { + // once we get the workspaces we intialize the first level of folders + const chidrenPaths = tree.children.map((child) => `root${child.path}`); + fetchAndInsertSubfolders(chidrenPaths, tree).then((r) => { + setStudiesTree(r[0]); + for (const path of r[1]) { + enqueueErrorSnackbar( + `Failed to initialize folders for : ${path}`, + "details in console.error", + ); + } + }); + }) .catch((error) => { enqueueErrorSnackbar("Failed to load list workspaces", error); setStudiesTree(initialStudiesTree); @@ -235,11 +247,11 @@ function StudyTree() { .catch((error) => { enqueueErrorSnackbar("Failed to load list workspaces", error); }); - return; } const chidrenPaths = studyTreeNode.children.map( (child) => `root${child.path}`, ); + // children paths and current element path fetchAndInsertSubfolders(chidrenPaths, studiesTree).then((r) => { setStudiesTree(r[0]); for (const path of r[1]) { From b68b992cde90bb7a778b91fdf06ad9dd6bfdd9d5 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 09:58:51 +0100 Subject: [PATCH 18/42] refactor(ui): make more modular --- .../App/Studies/StudyTree/index.tsx | 232 +++--------------- .../{StudyTree.test.tsx => utils.test.tsx} | 2 +- 2 files changed, 35 insertions(+), 199 deletions(-) rename webapp/src/components/App/Studies/StudyTree/{StudyTree.test.tsx => utils.test.tsx} (99%) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 71b8f6ffe1..0c4002a3fc 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { NonStudyFolder, StudyTreeNode } from ".././utils"; +import { StudyTreeNode } from ".././utils"; import useAppSelector from "../../../../redux/hooks/useAppSelector"; import { getStudiesTree, getStudyFilters } from "../../../../redux/selectors"; import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; @@ -22,187 +22,10 @@ import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView"; import { getParentPaths } from "../../../../utils/pathUtils"; import * as R from "ramda"; import { useState } from "react"; -import * as api from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; - -/** - * Add a folder that was returned by the explorer into the study tree view. - * - * This folder isn't a study, otherwise it wouldn't be - * returned by the explorer API, but this folder can have study in it - * and still not be in the initial study tree that's parsed from the study - * list, this happen when the studies in the folder aren't scanned yet. - * - * However we want to allow the user to see these folder. When the user explore - * they shoudln't wait for a long running scan to complete before they're able to - * a folder in the hierarchy. - * - * @param studiesTree study tree to insert the folder into - * @param folder folder to inert into the tree - * @returns study tree with the folder inserted if it wasn't already there. - * New branch is created if it contain the folder otherwise the branch is left unchanged. - */ -function insertFolderIfNotExist( - studiesTree: StudyTreeNode, - folder: NonStudyFolder, -): StudyTreeNode { - // Early return if folder doesn't belong in this branch - if (!folder.parentPath.startsWith(studiesTree.path)) { - return studiesTree; - } - - // direct child case - if (folder.parentPath == studiesTree.path) { - const folderExists = studiesTree.children.some( - (child) => child.name === folder.name, - ); - if (folderExists) { - return studiesTree; - } - // parent path is the same, but no folder with the same name at this level - return { - ...studiesTree, - children: [ - ...studiesTree.children, - { - path: `${folder.parentPath}/${folder.name}`, - name: folder.name, - children: [], - }, - ], - }; - } - - // not a direct child, but does belong to this branch so recursively walk though the tree - return { - ...studiesTree, - children: studiesTree.children.map((child) => - insertFolderIfNotExist(child, folder), - ), - }; -} - -/** - * Insert several folders in the study tree if they don't exist already in the tree. - * - * @param studiesTree study tree to insert the folder into - * @param folders folders to inert into the tree - * @param studiesTree study tree to insert the folder into - * @param folder folder to inert into the tree - * @returns study tree with the folder inserted if it wasn't already there. - * New branch is created if it contain the folder otherwise the branch is left unchanged. - */ -export function insertFoldersIfNotExist( - studiesTree: StudyTreeNode, - folders: NonStudyFolder[], -): StudyTreeNode { - return folders.reduce( - (tree, folder) => insertFolderIfNotExist(tree, folder), - studiesTree, - ); -} - -/** - * Call the explorer api to fetch the subfolders under the given path. - * - * @param path path of the subfolder to fetch, should sart with root, e.g. root/workspace/folder1 - * @returns list of subfolders under the given path - */ -async function fetchSubfolders(path: string): Promise { - if (path === "root") { - // Under root there're workspaces not subfolders - return []; - } - // less than 2 parts means we're at the root level - const pathParts = path.split("/"); - if (pathParts.length < 2) { - return []; - } - // path parts should be ["root", workspace, "folder1", ...] - const workspace = pathParts[1]; - const subPath = pathParts.slice(2).join("/"); - return await api.getFolders(workspace, subPath); -} - -/** - * Fetch and insert the subfolders under the given paths into the study tree. - * - * This function is used to fill the study tree when the user clicks on a folder. - * - * Subfolders are inserted only if they don't exist already in the tree. - * - * @param paths list of paths to fetch the subfolders for - * @param studiesTree study tree to insert the subfolders into - * @returns study tree with the subfolders inserted if they weren't already there. - */ -async function fetchAndInsertSubfolders( - paths: string[], - studiesTree: StudyTreeNode, -): Promise<[StudyTreeNode, string[]]> { - return paths.reduce( - async (acc, path) => { - const [accTree, accFailedPaths] = await acc; - try { - const subfolders = await fetchSubfolders(path); - return [insertFoldersIfNotExist(accTree, subfolders), accFailedPaths]; - } catch (error) { - console.error("failed to load path ", path, error); - return [accTree, [...accFailedPaths, path]]; - } - }, - Promise.resolve<[StudyTreeNode, string[]]>([studiesTree, []]), - ); -} - -/** - * Insert a workspace into the study tree if it doesn't exist already. - * - * @param workspace key of the workspace - * @param stydyTree study tree to insert the workspace into - * @returns study tree with the empty workspace inserted if it wasn't already there. - */ -function insertWorkspaceIfNotExist( - stydyTree: StudyTreeNode, - workspace: string, -) { - const emptyNode = { name: workspace, path: `/${workspace}`, children: [] }; - if (stydyTree.children.some((child) => child.name === workspace)) { - return stydyTree; - } - return { - ...stydyTree, - children: [...stydyTree.children, emptyNode], - }; -} - -/** - * Insert several workspaces into the study tree if they don't exist already in the tree. - * - * @param workspaces workspaces to insert into the tree - * @param stydyTree study tree to insert the workspaces into - * @returns study tree with the empty workspaces inserted if they weren't already there. - */ -export function insertWorkspacesIfNotExist( - stydyTree: StudyTreeNode, - workspaces: string[], -): StudyTreeNode { - return workspaces.reduce((acc, workspace) => { - return insertWorkspaceIfNotExist(acc, workspace); - }, stydyTree); -} - -/** - * fetch and insert the workspaces into the study tree. - * @param studyTree study tree to insert the workspaces into - * @returns study tree with the workspaces inserted if they weren't already there. - */ -async function fetchAndInsertWorkspaces( - studyTree: StudyTreeNode, -): Promise { - const workspaces = await api.getWorkspaces(); - return insertWorkspacesIfNotExist(studyTree, workspaces); -} +import { fetchAndInsertSubfolders, fetchAndInsertWorkspaces } from "./utils"; +import { useTranslation } from "react-i18next"; function StudyTree() { const initialStudiesTree = useAppSelector(getStudiesTree); @@ -210,28 +33,41 @@ function StudyTree() { const folder = useAppSelector((state) => getStudyFilters(state).folder, R.T); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const dispatch = useAppDispatch(); + const [t] = useTranslation(); // Initialize folders once we have the tree + // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized useUpdateEffectOnce(() => { - fetchAndInsertWorkspaces(initialStudiesTree) - .then((tree) => { - // once we get the workspaces we intialize the first level of folders - const chidrenPaths = tree.children.map((child) => `root${child.path}`); - fetchAndInsertSubfolders(chidrenPaths, tree).then((r) => { - setStudiesTree(r[0]); - for (const path of r[1]) { - enqueueErrorSnackbar( - `Failed to initialize folders for : ${path}`, - "details in console.error", - ); - } + const initializeFolders = async () => { + try { + const treeWithWorkspaces = + await fetchAndInsertWorkspaces(initialStudiesTree); + const childrenPaths = treeWithWorkspaces.children.map( + (child) => `root${child.path}`, + ); + const [updatedTree, failedPaths] = await fetchAndInsertSubfolders( + childrenPaths, + treeWithWorkspaces, + ); + setStudiesTree(updatedTree); + failedPaths.forEach((path) => { + enqueueErrorSnackbar( + t("studies.tree.error.failToFetchFolder", { path }), + t("studies.tree.error.detailsInConsole"), + ); }); - }) - .catch((error) => { - enqueueErrorSnackbar("Failed to load list workspaces", error); + } catch (error) { setStudiesTree(initialStudiesTree); - }); + enqueueErrorSnackbar( + t("studies.tree.error.failToFetchWorkspace"), + t("studies.tree.error.detailsInConsole"), + ); + } + }; + + initializeFolders(); }, [initialStudiesTree]); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// @@ -256,8 +92,8 @@ function StudyTree() { setStudiesTree(r[0]); for (const path of r[1]) { enqueueErrorSnackbar( - `Failed to initialize folders for : ${path}`, - "details in console.error", + t("studies.tree.error.failToFetchFolder", { path }), + t("studies.tree.error.detailsInConsole"), ); } }); diff --git a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx b/webapp/src/components/App/Studies/StudyTree/utils.test.tsx similarity index 99% rename from webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx rename to webapp/src/components/App/Studies/StudyTree/utils.test.tsx index 729c8022aa..7a66e9cffb 100644 --- a/webapp/src/components/App/Studies/StudyTree/StudyTree.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/utils.test.tsx @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "."; +import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "./utils"; import { NonStudyFolder, StudyTreeNode } from "../utils"; import { FIXTURES } from "./fixtures"; From 9c995c63ef51188d2a59976a75ea0076d45302dc Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 10:00:06 +0100 Subject: [PATCH 19/42] refactor(ui): use i18n, make code more functionnal style --- webapp/public/locales/en/main.json | 3 + webapp/public/locales/fr/main.json | 4 +- .../components/App/Studies/StudyTree/utils.ts | 196 ++++++++++++++++++ webapp/src/services/api/study.ts | 10 +- 4 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 webapp/src/components/App/Studies/StudyTree/utils.ts diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 952091b0d4..b6798f58fa 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -669,6 +669,9 @@ "studies.exportOutputFilter": "Export filtered output", "studies.selectOutput": "Select an output", "studies.variant": "Variant", + "studies.tree.error.failToFetchWorkspace": "Failed to load workspaces", + "studies.tree.error.failToFetchFolder": "Failed to load subfolders for {{path}}", + "studies.tree.error.detailsInConsole": "Details logged in the console", "variants.createNewVariant": "Create new variant", "variants.newVariant": "New variant", "variants.newCommand": "Add new command", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 81ffca6679..2e3e8de147 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -668,7 +668,9 @@ "studies.exportOutput": "Exporter une sortie", "studies.exportOutputFilter": "Exporter une sortie filtrée", "studies.selectOutput": "Selectionnez une sortie", - "studies.variant": "Variante", + "studies.tree.error.failToFetchWorkspace": "Échec lors de la récupération de l'espace de travail", + "studies.tree.error.failToFetchFolder": "Échec lors de la récupération des sous dossiers de {{path}}", + "studies.tree.error.detailsInConsole": "Détails de l'érreur dans la console", "variants.createNewVariant": "Créer une nouvelle variante", "variants.newVariant": "Nouvelle variante", "variants.newCommand": "Ajouter une nouvelle commande", diff --git a/webapp/src/components/App/Studies/StudyTree/utils.ts b/webapp/src/components/App/Studies/StudyTree/utils.ts new file mode 100644 index 0000000000..2313930a99 --- /dev/null +++ b/webapp/src/components/App/Studies/StudyTree/utils.ts @@ -0,0 +1,196 @@ +/** + * 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 { NonStudyFolder, StudyTreeNode } from "../utils"; +import * as api from "../../../../services/api/study"; + +/** + * Add a folder that was returned by the explorer into the study tree view. + * + * This folder isn't a study, otherwise it wouldn't be + * returned by the explorer API, but this folder can have study in it + * and still not be in the initial study tree that's parsed from the study + * list, this happen when the studies in the folder aren't scanned yet. + * + * However we want to allow the user to see these folder. When the user explore + * they shoudln't wait for a long running scan to complete before they're able to + * a folder in the hierarchy. + * + * @param studiesTree study tree to insert the folder into + * @param folder folder to inert into the tree + * @returns study tree with the folder inserted if it wasn't already there. + * New branch is created if it contain the folder otherwise the branch is left unchanged. + */ +function insertFolderIfNotExist( + studiesTree: StudyTreeNode, + folder: NonStudyFolder, +): StudyTreeNode { + // Early return if folder doesn't belong in this branch + if (!folder.parentPath.startsWith(studiesTree.path)) { + return studiesTree; + } + + // direct child case + if (folder.parentPath == studiesTree.path) { + const folderExists = studiesTree.children.some( + (child) => child.name === folder.name, + ); + if (folderExists) { + return studiesTree; + } + // parent path is the same, but no folder with the same name at this level + return { + ...studiesTree, + children: [ + ...studiesTree.children, + { + path: `${folder.parentPath}/${folder.name}`, + name: folder.name, + children: [], + }, + ], + }; + } + + // not a direct child, but does belong to this branch so recursively walk though the tree + return { + ...studiesTree, + children: studiesTree.children.map((child) => + insertFolderIfNotExist(child, folder), + ), + }; +} + +/** + * Insert several folders in the study tree if they don't exist already in the tree. + * + * @param studiesTree study tree to insert the folder into + * @param folders folders to inert into the tree + * @param studiesTree study tree to insert the folder into + * @param folder folder to inert into the tree + * @returns study tree with the folder inserted if it wasn't already there. + * New branch is created if it contain the folder otherwise the branch is left unchanged. + */ +export function insertFoldersIfNotExist( + studiesTree: StudyTreeNode, + folders: NonStudyFolder[], +): StudyTreeNode { + return folders.reduce( + (tree, folder) => insertFolderIfNotExist(tree, folder), + studiesTree, + ); +} + +/** + * Call the explorer api to fetch the subfolders under the given path. + * + * @param path path of the subfolder to fetch, should sart with root, e.g. root/workspace/folder1 + * @returns list of subfolders under the given path + */ +async function fetchSubfolders(path: string): Promise { + if (path === "root") { + // Under root there're workspaces not subfolders + return []; + } + // less than 2 parts means we're at the root level + const pathParts = path.split("/"); + if (pathParts.length < 2) { + return []; + } + // path parts should be ["root", workspace, "folder1", ...] + const workspace = pathParts[1]; + const subPath = pathParts.slice(2).join("/"); + return await api.getFolders(workspace, subPath); +} + +/** + * Fetch and insert the subfolders under the given paths into the study tree. + * + * This function is used to fill the study tree when the user clicks on a folder. + * + * Subfolders are inserted only if they don't exist already in the tree. + * + * @param paths list of paths to fetch the subfolders for + * @param studiesTree study tree to insert the subfolders into + * @returns a tuple with study tree with the subfolders inserted if they weren't already there and path for which + * the fetch failed. + */ +export async function fetchAndInsertSubfolders( + paths: string[], + studiesTree: StudyTreeNode, +): Promise<[StudyTreeNode, string[]]> { + const results = await Promise.allSettled( + paths.map((path) => fetchSubfolders(path)), + ); + + return results.reduce<[StudyTreeNode, string[]]>( + ([tree, failed], result, index) => { + if (result.status === "fulfilled") { + return [insertFoldersIfNotExist(tree, result.value), failed]; + } + console.error("Failed to load path:", paths[index], result.reason); + return [tree, [...failed, paths[index]]]; + }, + [studiesTree, []], + ); +} + +/** + * Insert a workspace into the study tree if it doesn't exist already. + * + * @param workspace key of the workspace + * @param stydyTree study tree to insert the workspace into + * @returns study tree with the empty workspace inserted if it wasn't already there. + */ +function insertWorkspaceIfNotExist( + stydyTree: StudyTreeNode, + workspace: string, +) { + const emptyNode = { name: workspace, path: `/${workspace}`, children: [] }; + if (stydyTree.children.some((child) => child.name === workspace)) { + return stydyTree; + } + return { + ...stydyTree, + children: [...stydyTree.children, emptyNode], + }; +} + +/** + * Insert several workspaces into the study tree if they don't exist already in the tree. + * + * @param workspaces workspaces to insert into the tree + * @param stydyTree study tree to insert the workspaces into + * @returns study tree with the empty workspaces inserted if they weren't already there. + */ +export function insertWorkspacesIfNotExist( + stydyTree: StudyTreeNode, + workspaces: string[], +): StudyTreeNode { + return workspaces.reduce((acc, workspace) => { + return insertWorkspaceIfNotExist(acc, workspace); + }, stydyTree); +} + +/** + * fetch and insert the workspaces into the study tree. + * @param studyTree study tree to insert the workspaces into + * @returns study tree with the workspaces inserted if they weren't already there. + */ +export async function fetchAndInsertWorkspaces( + studyTree: StudyTreeNode, +): Promise { + const workspaces = await api.getWorkspaces(); + return insertWorkspacesIfNotExist(studyTree, workspaces); +} diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 003cf0125a..06f41a2e7a 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -36,6 +36,10 @@ import { FileDownloadTask } from "./downloads"; import { StudyMapDistrict } from "../../redux/ducks/studyMaps"; import { NonStudyFolder } from "@/components/App/Studies/utils"; +interface Workspace { + name: string; +} + const getStudiesRaw = async (): Promise> => { const res = await client.get(`/v1/studies`); return res.data; @@ -50,8 +54,10 @@ export const getStudies = async (): Promise => { }; export const getWorkspaces = async (): Promise => { - const res = await client.get(`/v1/private/explorer/_list_workspaces`); - return res.data.map((folder: { name: string }) => folder.name); + const res = await client.get( + `/v1/private/explorer/_list_workspaces`, + ); + return res.data.map((folder) => folder.name); }; /** From 10926f6dce74fe5b1b2273f12a7d6e087a86a89e Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 16:16:33 +0100 Subject: [PATCH 20/42] feat(explorer): add parent path field, rename Dto object --- antarest/study/model.py | 17 +++++++++++++++-- antarest/study/storage/explorer_service.py | 6 +++--- antarest/study/web/explorer_blueprint.py | 6 +++--- .../explorer_blueprint/test_explorer.py | 6 +++--- tests/storage/business/test_explorer_service.py | 12 +++++------- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/antarest/study/model.py b/antarest/study/model.py index b8378aa356..9b3fe0c77c 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -19,7 +19,7 @@ from pathlib import Path from antares.study.version import StudyVersion -from pydantic import BeforeValidator, PlainSerializer, field_validator +from pydantic import BeforeValidator, PlainSerializer, computed_field, field_validator from sqlalchemy import ( # type: ignore Boolean, Column, @@ -323,7 +323,7 @@ class StudyFolder: groups: t.List[Group] -class NonStudyFolder(AntaresBaseModel): +class NonStudyFolderDTO(AntaresBaseModel): """ DTO used by the explorer to list directories that aren't studies directory, this will be usefull for the front so the user can navigate in the hierarchy @@ -333,6 +333,19 @@ class NonStudyFolder(AntaresBaseModel): workspace: str name: str + @computed_field(alias="parentPath") + def parent_path(self) -> str: + """ + This computed field is convenient for the front. + + This field is also aliased as parentPath to match the front-end naming convention. + + Returns: the parent path of the current directory. Starting with the workspace as a root directory. + """ + workspace_path = Path(f"/{self.workspace}") + full_path = workspace_path.joinpath(self.path) + return full_path.parent + class WorkspaceMetadata(AntaresBaseModel): """ diff --git a/antarest/study/storage/explorer_service.py b/antarest/study/storage/explorer_service.py index 5610f3e5f8..fa9ef7fa30 100644 --- a/antarest/study/storage/explorer_service.py +++ b/antarest/study/storage/explorer_service.py @@ -14,7 +14,7 @@ from typing import List from antarest.core.config import Config -from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolder, WorkspaceMetadata +from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolderDTO, WorkspaceMetadata from antarest.study.storage.utils import ( get_folder_from_workspace, get_workspace_from_config, @@ -33,7 +33,7 @@ def list_dir( self, workspace_name: str, workspace_directory_path: str, - ) -> List[NonStudyFolder]: + ) -> List[NonStudyFolderDTO]: """ return a list of all directories under workspace_directory_path, that aren't studies. """ @@ -44,7 +44,7 @@ def list_dir( if child.is_dir() and not is_study_folder(child) and not should_ignore_folder_for_scan(child): # we don't want to expose the full absolute path on the server child_rel_path = child.relative_to(workspace.path) - directories.append(NonStudyFolder(path=child_rel_path, workspace=workspace_name, name=child.name)) + directories.append(NonStudyFolderDTO(path=child_rel_path, workspace=workspace_name, name=child.name)) return directories def list_workspaces( diff --git a/antarest/study/web/explorer_blueprint.py b/antarest/study/web/explorer_blueprint.py index 0981ba5214..b453cab787 100644 --- a/antarest/study/web/explorer_blueprint.py +++ b/antarest/study/web/explorer_blueprint.py @@ -18,7 +18,7 @@ from antarest.core.config import Config from antarest.core.jwt import JWTUser from antarest.login.auth import Auth -from antarest.study.model import NonStudyFolder, WorkspaceMetadata +from antarest.study.model import NonStudyFolderDTO, WorkspaceMetadata from antarest.study.storage.explorer_service import Explorer logger = logging.getLogger(__name__) @@ -40,13 +40,13 @@ def create_explorer_routes(config: Config, explorer: Explorer) -> APIRouter: @bp.get( "/explorer/{workspace}/_list_dir", summary="For a given directory, list sub directories that aren't studies", - response_model=List[NonStudyFolder], + response_model=List[NonStudyFolderDTO], ) def list_dir( workspace: str, path: str, current_user: JWTUser = Depends(auth.get_current_user), - ) -> List[NonStudyFolder]: + ) -> List[NonStudyFolderDTO]: """ Endpoint to list sub directories of a given directory Args: diff --git a/tests/integration/explorer_blueprint/test_explorer.py b/tests/integration/explorer_blueprint/test_explorer.py index dbb6f83ebc..31990b1781 100644 --- a/tests/integration/explorer_blueprint/test_explorer.py +++ b/tests/integration/explorer_blueprint/test_explorer.py @@ -14,7 +14,7 @@ import pytest from starlette.testclient import TestClient -from antarest.study.model import NonStudyFolder, WorkspaceMetadata +from antarest.study.model import NonStudyFolderDTO, WorkspaceMetadata BAD_REQUEST_STATUS_CODE = 400 # Status code for directory listing with invalid parameters @@ -65,9 +65,9 @@ def test_explorer(client: TestClient, admin_access_token: str, study_tree: Path) ) res.raise_for_status() directories_res = res.json() - directories_res = [NonStudyFolder(**d) for d in directories_res] + directories_res = [NonStudyFolderDTO(**d) for d in directories_res] directorires_expected = [ - NonStudyFolder( + NonStudyFolderDTO( path=Path("folder/trash"), workspace="ext", name="trash", diff --git a/tests/storage/business/test_explorer_service.py b/tests/storage/business/test_explorer_service.py index 883e79cfca..37a7c0c033 100644 --- a/tests/storage/business/test_explorer_service.py +++ b/tests/storage/business/test_explorer_service.py @@ -15,7 +15,7 @@ import pytest from antarest.core.config import Config, StorageConfig, WorkspaceConfig -from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolder, WorkspaceMetadata +from antarest.study.model import DEFAULT_WORKSPACE_NAME, NonStudyFolderDTO, WorkspaceMetadata from antarest.study.storage.explorer_service import Explorer @@ -85,8 +85,7 @@ def test_list_dir_empty_string(config_scenario_a: Config): result = explorer.list_dir("diese", "") assert len(result) == 1 - workspace_path = config_scenario_a.get_workspace_path(workspace="diese") - assert result[0] == NonStudyFolder(path=Path("folder"), workspace="diese", name="folder") + assert result[0] == NonStudyFolderDTO(path=Path("folder"), workspace="diese", name="folder") @pytest.mark.unit_test @@ -95,11 +94,10 @@ def test_list_dir_several_subfolders(config_scenario_a: Config): result = explorer.list_dir("diese", "folder") assert len(result) == 3 - workspace_path = config_scenario_a.get_workspace_path(workspace="diese") folder_path = Path("folder") - assert NonStudyFolder(path=(folder_path / "subfolder1"), workspace="diese", name="subfolder1") in result - assert NonStudyFolder(path=(folder_path / "subfolder2"), workspace="diese", name="subfolder2") in result - assert NonStudyFolder(path=(folder_path / "subfolder3"), workspace="diese", name="subfolder3") in result + assert NonStudyFolderDTO(path=(folder_path / "subfolder1"), workspace="diese", name="subfolder1") in result + assert NonStudyFolderDTO(path=(folder_path / "subfolder2"), workspace="diese", name="subfolder2") in result + assert NonStudyFolderDTO(path=(folder_path / "subfolder3"), workspace="diese", name="subfolder3") in result @pytest.mark.unit_test From 5469e62b2e91989779b6f479285471f4627cf6a9 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 16:32:16 +0100 Subject: [PATCH 21/42] refactor(explorer): better doc string --- antarest/study/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/antarest/study/model.py b/antarest/study/model.py index 9b3fe0c77c..30a8100ae9 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -340,7 +340,7 @@ def parent_path(self) -> str: This field is also aliased as parentPath to match the front-end naming convention. - Returns: the parent path of the current directory. Starting with the workspace as a root directory. + Returns: the parent path of the current directory. Starting with the workspace as a root directory (we want /workspafe/folder1/sub... and not workspace/folder1/fsub... ). """ workspace_path = Path(f"/{self.workspace}") full_path = workspace_path.joinpath(self.path) From 38d606cd36e4f2104fdd29bb80c38fbf3ad08aab Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 16:44:05 +0100 Subject: [PATCH 22/42] refactor(ui): adapt to new backend format --- .../components/App/Studies/StudyTree/utils.ts | 8 ++++---- webapp/src/components/App/Studies/utils.ts | 3 --- webapp/src/services/api/study.ts | 18 ++++-------------- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/utils.ts b/webapp/src/components/App/Studies/StudyTree/utils.ts index 2313930a99..5606791e6c 100644 --- a/webapp/src/components/App/Studies/StudyTree/utils.ts +++ b/webapp/src/components/App/Studies/StudyTree/utils.ts @@ -12,7 +12,7 @@ * This file is part of the Antares project. */ -import { NonStudyFolder, StudyTreeNode } from "../utils"; +import { NonStudyFolderDTO, StudyTreeNode } from "../utils"; import * as api from "../../../../services/api/study"; /** @@ -34,7 +34,7 @@ import * as api from "../../../../services/api/study"; */ function insertFolderIfNotExist( studiesTree: StudyTreeNode, - folder: NonStudyFolder, + folder: NonStudyFolderDTO, ): StudyTreeNode { // Early return if folder doesn't belong in this branch if (!folder.parentPath.startsWith(studiesTree.path)) { @@ -84,7 +84,7 @@ function insertFolderIfNotExist( */ export function insertFoldersIfNotExist( studiesTree: StudyTreeNode, - folders: NonStudyFolder[], + folders: NonStudyFolderDTO[], ): StudyTreeNode { return folders.reduce( (tree, folder) => insertFolderIfNotExist(tree, folder), @@ -98,7 +98,7 @@ export function insertFoldersIfNotExist( * @param path path of the subfolder to fetch, should sart with root, e.g. root/workspace/folder1 * @returns list of subfolders under the given path */ -async function fetchSubfolders(path: string): Promise { +async function fetchSubfolders(path: string): Promise { if (path === "root") { // Under root there're workspaces not subfolders return []; diff --git a/webapp/src/components/App/Studies/utils.ts b/webapp/src/components/App/Studies/utils.ts index ab245d16a8..760daa9316 100644 --- a/webapp/src/components/App/Studies/utils.ts +++ b/webapp/src/components/App/Studies/utils.ts @@ -24,9 +24,6 @@ export interface NonStudyFolderDTO { name: string; path: string; workspace: string; -} - -export interface NonStudyFolder extends NonStudyFolderDTO { parentPath: string; } diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 06f41a2e7a..965df8909f 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -34,7 +34,7 @@ import { getConfig } from "../config"; import { convertStudyDtoToMetadata } from "../utils"; import { FileDownloadTask } from "./downloads"; import { StudyMapDistrict } from "../../redux/ducks/studyMaps"; -import { NonStudyFolder } from "@/components/App/Studies/utils"; +import { NonStudyFolderDTO } from "@/components/App/Studies/utils"; interface Workspace { name: string; @@ -69,21 +69,11 @@ export const getWorkspaces = async (): Promise => { export const getFolders = async ( workspace: string, folderPath: string, -): Promise => { - const res = await client.get( +): Promise => { + const res = await client.get( `/v1/private/explorer/${workspace}/_list_dir?path=${encodeURIComponent(folderPath)}`, ); - return res.data.map((folder) => { - const parentPath = [ - "", - folder.workspace, - ...folder.path.split("/").filter(Boolean).slice(0, -1), - ].join("/"); - return { - ...folder, - parentPath, - }; - }); + return res.data; }; export const getStudyVersions = async (): Promise => { From f8521dc14af29504a2f869dfee9b4389e967eb3c Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 16:44:37 +0100 Subject: [PATCH 23/42] fix(ui): over escaped special charachters --- webapp/src/components/App/Studies/StudyTree/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 0c4002a3fc..841ed1ce5d 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -52,7 +52,10 @@ function StudyTree() { setStudiesTree(updatedTree); failedPaths.forEach((path) => { enqueueErrorSnackbar( - t("studies.tree.error.failToFetchFolder", { path }), + t("studies.tree.error.failToFetchFolder", { + path, + interpolation: { escapeValue: false }, + }), t("studies.tree.error.detailsInConsole"), ); }); @@ -92,7 +95,10 @@ function StudyTree() { setStudiesTree(r[0]); for (const path of r[1]) { enqueueErrorSnackbar( - t("studies.tree.error.failToFetchFolder", { path }), + t("studies.tree.error.failToFetchFolder", { + path, + interpolation: { escapeValue: false }, + }), t("studies.tree.error.detailsInConsole"), ); } From a7b1b979b2072d5c38646e46c5d97c365ce6fa1d Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 16:50:27 +0100 Subject: [PATCH 24/42] fix(explorer): fix build --- antarest/study/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/antarest/study/model.py b/antarest/study/model.py index 30a8100ae9..207662aeea 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -334,7 +334,7 @@ class NonStudyFolderDTO(AntaresBaseModel): name: str @computed_field(alias="parentPath") - def parent_path(self) -> str: + def parent_path(self) -> Path: """ This computed field is convenient for the front. From 74a47c45385e528f7e312f933be2004a5a164d4d Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 16:55:54 +0100 Subject: [PATCH 25/42] fix(ui): fix build --- webapp/src/components/App/Studies/SideNav.tsx | 2 +- webapp/src/components/App/Studies/StudyTree/utils.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/App/Studies/SideNav.tsx b/webapp/src/components/App/Studies/SideNav.tsx index c0009ce7d2..b966f27fd6 100644 --- a/webapp/src/components/App/Studies/SideNav.tsx +++ b/webapp/src/components/App/Studies/SideNav.tsx @@ -16,7 +16,7 @@ import { useNavigate } from "react-router"; import { Box, Typography, List, ListItem, ListItemText } from "@mui/material"; import { useTranslation } from "react-i18next"; import { STUDIES_SIDE_NAV_WIDTH } from "../../../theme"; -import StudyTree from "./StudyTree"; +import StudyTree from "@/components/App/Studies/StudyTree"; import useAppSelector from "../../../redux/hooks/useAppSelector"; import { getFavoriteStudies } from "../../../redux/selectors"; diff --git a/webapp/src/components/App/Studies/StudyTree/utils.test.tsx b/webapp/src/components/App/Studies/StudyTree/utils.test.tsx index 7a66e9cffb..471d6f2885 100644 --- a/webapp/src/components/App/Studies/StudyTree/utils.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/utils.test.tsx @@ -13,7 +13,7 @@ */ import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "./utils"; -import { NonStudyFolder, StudyTreeNode } from "../utils"; +import { NonStudyFolderDTO, StudyTreeNode } from "../utils"; import { FIXTURES } from "./fixtures"; describe("StudyTree Utils", () => { @@ -52,7 +52,7 @@ describe("StudyTree Utils", () => { path: "/", children: [{ name: "a", path: "/a", children: [] }], }; - const invalidFolder: NonStudyFolder = { + const invalidFolder: NonStudyFolderDTO = { name: "invalid", path: "/invalid", workspace: "nonexistent", From 16283c92e65fb940fab2d59694a157442572d306 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 22 Nov 2024 17:07:50 +0100 Subject: [PATCH 26/42] fix(ui): fix build --- webapp/src/services/api/study.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 965df8909f..dcf5453de4 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -62,9 +62,10 @@ export const getWorkspaces = async (): Promise => { /** * Call the explorer API to get the list of folders in a workspace - * @param workspace workspace name - * @param folderPath path starting from the workspace root (not including the workspace name) - * @returns + * + * @param workspace - workspace name + * @param folderPath - path starting from the workspace root (not including the workspace name) + * @returns list of folders that are not studies, under the given path */ export const getFolders = async ( workspace: string, From 54177b8f4a135ba85324eeef76960f73aa06a105 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 28 Nov 2024 10:39:24 +0100 Subject: [PATCH 27/42] feat(ui): make the scan non recursive by defaul, add an option for recursive scan, show only studies in the current folder by default use a tree icon when we show studies for all descendants --- webapp/public/locales/en/main.json | 1 + webapp/public/locales/fr/main.json | 1 + .../App/Studies/StudiesList/index.tsx | 37 +++++++++++++++---- webapp/src/redux/ducks/studies.ts | 2 +- webapp/src/services/api/study.ts | 9 ++++- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index b6798f58fa..ef640365b2 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -638,6 +638,7 @@ "studies.folder": "Folder", "studies.filters.strictfolder": "Show only direct folder children", "studies.scanFolder": "Scan folder", + "studies.requestDeepScan": "Scan the folder recursively (may be long) ?", "studies.moveStudy": "Move", "studies.movefolderplaceholder": "Path separated by '/'", "studies.importcopy": "Copy to database", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 2e3e8de147..e9cbe8465d 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -638,6 +638,7 @@ "studies.folder": "Dossier", "studies.filters.strictfolder": "Afficher uniquement les descendants directs", "studies.scanFolder": "Scanner le dossier", + "studies.requestDeepScan": "Scanner le dossier récursivement (ça peut être long) ?", "studies.moveStudy": "Déplacer", "studies.movefolderplaceholder": "Chemin séparé par des '/'", "studies.importcopy": "Copier en base", diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index a6c412e625..7b5e565881 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -26,6 +26,8 @@ import { FormControl, InputLabel, IconButton, + Checkbox, + FormControlLabel, } from "@mui/material"; import { useTranslation } from "react-i18next"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; @@ -33,7 +35,8 @@ import AutoSizer from "react-virtualized-auto-sizer"; import HomeIcon from "@mui/icons-material/Home"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; -import FolderOffIcon from "@mui/icons-material/FolderOff"; +import FolderIcon from "@mui/icons-material/Folder"; +import AccountTreeIcon from "@mui/icons-material/AccountTree"; import RadarIcon from "@mui/icons-material/Radar"; import { FixedSizeGrid, GridOnScrollProps } from "react-window"; import { v4 as uuidv4 } from "uuid"; @@ -91,6 +94,7 @@ function StudiesList(props: StudiesListProps) { const [selectedStudies, setSelectedStudies] = useState([]); const [selectionMode, setSelectionMode] = useState(false); const [confirmFolderScan, setConfirmFolderScan] = useState(false); + const [requestDeepscan, setRequestDeepScan] = useState(false); useEffect(() => { setFolderList(folder.split("/")); @@ -159,13 +163,18 @@ function StudiesList(props: StudiesListProps) { try { // Remove "/root" from the path const folder = folderList.slice(1).join("/"); - await scanFolder(folder); + await scanFolder(folder, requestDeepscan); setConfirmFolderScan(false); + setRequestDeepScan(false); } catch (e) { enqueueErrorSnackbar(t("studies.error.scanFolder"), e as AxiosError); } }; + const handleDeepScanCheckboxChange = () => { + setRequestDeepScan(!requestDeepscan); + }; + //////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////// @@ -253,11 +262,15 @@ function StudiesList(props: StudiesListProps) { ({`${studyIds.length} ${t("global.studies").toLowerCase()}`}) - - - + {strictFolderFilter ? ( + + + + ) : ( + + + + )} {folder !== "root" && ( @@ -269,12 +282,20 @@ function StudiesList(props: StudiesListProps) { {folder !== "root" && confirmFolderScan && ( setConfirmFolderScan(false)} + onCancel={() => { + setConfirmFolderScan(false); + setRequestDeepScan(false); + }} onConfirm={handleFolderScan} alert="warning" open > {`${t("studies.scanFolder")} ${folder}?`} + } + label={t("studies.requestDeepScan")} + onChange={handleDeepScanCheckboxChange} + />{" "} )} diff --git a/webapp/src/redux/ducks/studies.ts b/webapp/src/redux/ducks/studies.ts index 3b3e8b04d2..9b4603a23f 100644 --- a/webapp/src/redux/ducks/studies.ts +++ b/webapp/src/redux/ducks/studies.ts @@ -94,7 +94,7 @@ const initialState = studiesAdapter.getInitialState({ filters: { inputValue: "", folder: "root", - strictFolder: false, + strictFolder: true, managed: false, archived: false, variant: false, diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index dcf5453de4..daf32f2291 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -464,8 +464,13 @@ export const updateStudyMetadata = async ( return res.data; }; -export const scanFolder = async (folderPath: string): Promise => { - await client.post(`/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}`); +export const scanFolder = async ( + folderPath: string, + recursive: Boolean = false, +): Promise => { + await client.post( + `/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}&recursive=${recursive}`, + ); }; export const getStudyLayers = async (uuid: string): Promise => { From 72e4e8f29d546322359705fdce0186c859ac6dbd Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 28 Nov 2024 11:02:14 +0100 Subject: [PATCH 28/42] fix(ui): fix build npm lint --- webapp/src/services/api/study.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index daf32f2291..83c006b34a 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -466,7 +466,7 @@ export const updateStudyMetadata = async ( export const scanFolder = async ( folderPath: string, - recursive: Boolean = false, + recursive: boolean = false, ): Promise => { await client.post( `/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}&recursive=${recursive}`, From 4442a5f9aad989fbb11a70ebff1b3cd18d7aae75 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 28 Nov 2024 11:07:49 +0100 Subject: [PATCH 29/42] fix(ui): fix build --- webapp/src/services/api/study.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index 83c006b34a..4b841428f3 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -466,7 +466,7 @@ export const updateStudyMetadata = async ( export const scanFolder = async ( folderPath: string, - recursive: boolean = false, + recursive = false, ): Promise => { await client.post( `/v1/watcher/_scan?path=${encodeURIComponent(folderPath)}&recursive=${recursive}`, From 6612892bd2db0219f6792ee263d61a66cb1ac683 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 28 Nov 2024 11:20:07 +0100 Subject: [PATCH 30/42] fix(ui): fix nonense tooltip messsage --- webapp/public/locales/en/main.json | 1 + webapp/public/locales/fr/main.json | 1 + .../components/App/Studies/StudiesList/index.tsx | 14 +++++++++----- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index ef640365b2..e839856832 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -637,6 +637,7 @@ "studies.copySuffix": "Copy", "studies.folder": "Folder", "studies.filters.strictfolder": "Show only direct folder children", + "studies.filters.showAllDescendants": "Show all children", "studies.scanFolder": "Scan folder", "studies.requestDeepScan": "Scan the folder recursively (may be long) ?", "studies.moveStudy": "Move", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index e9cbe8465d..321bf2b57b 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -637,6 +637,7 @@ "studies.copySuffix": "Copie", "studies.folder": "Dossier", "studies.filters.strictfolder": "Afficher uniquement les descendants directs", + "studies.filters.showAllDescendants": "Afficher tous les descendants", "studies.scanFolder": "Scanner le dossier", "studies.requestDeepScan": "Scanner le dossier récursivement (ça peut être long) ?", "studies.moveStudy": "Déplacer", diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index 7b5e565881..3a88597f38 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -261,17 +261,21 @@ function StudiesList(props: StudiesListProps) { ({`${studyIds.length} ${t("global.studies").toLowerCase()}`}) - - {strictFolderFilter ? ( + + {strictFolderFilter ? ( + - ) : ( + + ) : ( + - )} - + + )} + {folder !== "root" && ( setConfirmFolderScan(true)}> From fc19796ed578a7fba472cb330fdb188fe9c04a01 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 28 Nov 2024 11:33:19 +0100 Subject: [PATCH 31/42] fix(ui): fix build --- webapp/src/components/App/Studies/StudiesList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index 3a88597f38..f05f83ef65 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -94,7 +94,7 @@ function StudiesList(props: StudiesListProps) { const [selectedStudies, setSelectedStudies] = useState([]); const [selectionMode, setSelectionMode] = useState(false); const [confirmFolderScan, setConfirmFolderScan] = useState(false); - const [requestDeepscan, setRequestDeepScan] = useState(false); + const [requestDeepScan, setRequestDeepScan] = useState(false); useEffect(() => { setFolderList(folder.split("/")); From e54429fdb3ac14055c4f1892a174e9b0dd2006c8 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 28 Nov 2024 11:35:55 +0100 Subject: [PATCH 32/42] fix(ui): fix build --- webapp/src/components/App/Studies/StudiesList/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index f05f83ef65..b111be325f 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -163,7 +163,7 @@ function StudiesList(props: StudiesListProps) { try { // Remove "/root" from the path const folder = folderList.slice(1).join("/"); - await scanFolder(folder, requestDeepscan); + await scanFolder(folder, requestDeepScan); setConfirmFolderScan(false); setRequestDeepScan(false); } catch (e) { @@ -172,7 +172,7 @@ function StudiesList(props: StudiesListProps) { }; const handleDeepScanCheckboxChange = () => { - setRequestDeepScan(!requestDeepscan); + setRequestDeepScan(!requestDeepScan); }; //////////////////////////////////////////////////////////////// @@ -296,7 +296,7 @@ function StudiesList(props: StudiesListProps) { > {`${t("studies.scanFolder")} ${folder}?`} } + control={} label={t("studies.requestDeepScan")} onChange={handleDeepScanCheckboxChange} />{" "} From eb1a1704cebb8e90fc554c84dd1915bb4995aec9 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Tue, 3 Dec 2024 10:39:31 +0100 Subject: [PATCH 33/42] refactor(ui): better naming and messages --- webapp/public/locales/en/main.json | 2 +- webapp/public/locales/fr/main.json | 4 +- .../App/Studies/StudiesList/index.tsx | 16 +++---- .../App/Studies/StudyTree/index.tsx | 46 +++++++++++-------- 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index e839856832..e6ad22ca4c 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -639,7 +639,7 @@ "studies.filters.strictfolder": "Show only direct folder children", "studies.filters.showAllDescendants": "Show all children", "studies.scanFolder": "Scan folder", - "studies.requestDeepScan": "Scan the folder recursively (may be long) ?", + "studies.requestDeepScan": "Recursive scan", "studies.moveStudy": "Move", "studies.movefolderplaceholder": "Path separated by '/'", "studies.importcopy": "Copy to database", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 321bf2b57b..65e6ff80dd 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -637,9 +637,9 @@ "studies.copySuffix": "Copie", "studies.folder": "Dossier", "studies.filters.strictfolder": "Afficher uniquement les descendants directs", - "studies.filters.showAllDescendants": "Afficher tous les descendants", + "studies.filters.showAllDescendants": "Voir les sous-dossiers", "studies.scanFolder": "Scanner le dossier", - "studies.requestDeepScan": "Scanner le dossier récursivement (ça peut être long) ?", + "studies.requestDeepScan": "Scan récursif", "studies.moveStudy": "Déplacer", "studies.movefolderplaceholder": "Chemin séparé par des '/'", "studies.importcopy": "Copier en base", diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index b111be325f..2b4c2d7cb0 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -94,7 +94,7 @@ function StudiesList(props: StudiesListProps) { const [selectedStudies, setSelectedStudies] = useState([]); const [selectionMode, setSelectionMode] = useState(false); const [confirmFolderScan, setConfirmFolderScan] = useState(false); - const [requestDeepScan, setRequestDeepScan] = useState(false); + const [isRecursiveScan, setIsRecursiveScan] = useState(false); useEffect(() => { setFolderList(folder.split("/")); @@ -163,16 +163,16 @@ function StudiesList(props: StudiesListProps) { try { // Remove "/root" from the path const folder = folderList.slice(1).join("/"); - await scanFolder(folder, requestDeepScan); + await scanFolder(folder, isRecursiveScan); setConfirmFolderScan(false); - setRequestDeepScan(false); + setIsRecursiveScan(false); } catch (e) { enqueueErrorSnackbar(t("studies.error.scanFolder"), e as AxiosError); } }; - const handleDeepScanCheckboxChange = () => { - setRequestDeepScan(!requestDeepScan); + const handleRecursiveScan = () => { + setIsRecursiveScan(!isRecursiveScan); }; //////////////////////////////////////////////////////////////// @@ -288,7 +288,7 @@ function StudiesList(props: StudiesListProps) { titleIcon={RadarIcon} onCancel={() => { setConfirmFolderScan(false); - setRequestDeepScan(false); + setIsRecursiveScan(false); }} onConfirm={handleFolderScan} alert="warning" @@ -296,9 +296,9 @@ function StudiesList(props: StudiesListProps) { > {`${t("studies.scanFolder")} ${folder}?`} } + control={} label={t("studies.requestDeepScan")} - onChange={handleDeepScanCheckboxChange} + onChange={handleRecursiveScan} />{" "} )} diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 841ed1ce5d..f7bd95aec5 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -26,6 +26,7 @@ import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; import { fetchAndInsertSubfolders, fetchAndInsertWorkspaces } from "./utils"; import { useTranslation } from "react-i18next"; +import { AxiosError } from "axios"; function StudyTree() { const initialStudiesTree = useAppSelector(getStudiesTree); @@ -63,7 +64,7 @@ function StudyTree() { setStudiesTree(initialStudiesTree); enqueueErrorSnackbar( t("studies.tree.error.failToFetchWorkspace"), - t("studies.tree.error.detailsInConsole"), + error as AxiosError, ); } }; @@ -75,36 +76,41 @@ function StudyTree() { // Event Handlers //////////////////////////////////////////////////////////////// - const handleTreeItemClick = ( + const handleTreeItemClick = async ( itemId: string, studyTreeNode: StudyTreeNode, ) => { dispatch(updateStudyFilters({ folder: itemId })); if (itemId === "root") { - fetchAndInsertWorkspaces(studiesTree) - .then(setStudiesTree) - .catch((error) => { - enqueueErrorSnackbar("Failed to load list workspaces", error); - }); + try { + const nextTree = await fetchAndInsertWorkspaces(studiesTree); + setStudiesTree(nextTree); + } catch (error) { + enqueueErrorSnackbar( + "studies.tree.error.failToFetchWorkspace", + error as AxiosError, + ); + } } const chidrenPaths = studyTreeNode.children.map( (child) => `root${child.path}`, ); // children paths and current element path - fetchAndInsertSubfolders(chidrenPaths, studiesTree).then((r) => { - setStudiesTree(r[0]); - for (const path of r[1]) { - enqueueErrorSnackbar( - t("studies.tree.error.failToFetchFolder", { - path, - interpolation: { escapeValue: false }, - }), - t("studies.tree.error.detailsInConsole"), - ); - } - }); + const [nextTree, failedPath] = await fetchAndInsertSubfolders( + chidrenPaths, + studiesTree, + ); + setStudiesTree(nextTree); + for (const path of failedPath) { + enqueueErrorSnackbar( + t("studies.tree.error.failToFetchFolder", { + path, + interpolation: { escapeValue: false }, + }), + t("studies.tree.error.detailsInConsole"), + ); + } }; - //////////////////////////////////////////////////////////////// // JSX //////////////////////////////////////////////////////////////// From e24f1da828a0c1d57a5ddc8c2fa3b77de7abbe69 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 5 Dec 2024 18:02:50 +0100 Subject: [PATCH 34/42] refactor(ui): refactor duplicate code and fix style --- .../App/Studies/StudiesList/index.tsx | 13 ++- .../App/Studies/StudyTree/index.tsx | 91 +++++++------------ 2 files changed, 41 insertions(+), 63 deletions(-) diff --git a/webapp/src/components/App/Studies/StudiesList/index.tsx b/webapp/src/components/App/Studies/StudiesList/index.tsx index 2b4c2d7cb0..b88f1a5490 100644 --- a/webapp/src/components/App/Studies/StudiesList/index.tsx +++ b/webapp/src/components/App/Studies/StudiesList/index.tsx @@ -26,8 +26,6 @@ import { FormControl, InputLabel, IconButton, - Checkbox, - FormControlLabel, } from "@mui/material"; import { useTranslation } from "react-i18next"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; @@ -67,6 +65,7 @@ import RefreshButton from "../RefreshButton"; import { scanFolder } from "../../../../services/api/study"; import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; +import CheckBoxFE from "@/components/common/fieldEditors/CheckBoxFE"; const CARD_TARGET_WIDTH = 500; const CARD_HEIGHT = 250; @@ -263,13 +262,13 @@ function StudiesList(props: StudiesListProps) { {strictFolderFilter ? ( - + ) : ( - + @@ -295,11 +294,11 @@ function StudiesList(props: StudiesListProps) { open > {`${t("studies.scanFolder")} ${folder}?`} - } + {" "} + /> )} diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index f7bd95aec5..354082b4de 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -26,7 +26,7 @@ import useEnqueueErrorSnackbar from "@/hooks/useEnqueueErrorSnackbar"; import useUpdateEffectOnce from "@/hooks/useUpdateEffectOnce"; import { fetchAndInsertSubfolders, fetchAndInsertWorkspaces } from "./utils"; import { useTranslation } from "react-i18next"; -import { AxiosError } from "axios"; +import { toError } from "@/utils/fnUtils"; function StudyTree() { const initialStudiesTree = useAppSelector(getStudiesTree); @@ -36,81 +36,60 @@ function StudyTree() { const dispatch = useAppDispatch(); const [t] = useTranslation(); - // Initialize folders once we have the tree - // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized - useUpdateEffectOnce(() => { - const initializeFolders = async () => { - try { - const treeWithWorkspaces = - await fetchAndInsertWorkspaces(initialStudiesTree); - const childrenPaths = treeWithWorkspaces.children.map( - (child) => `root${child.path}`, - ); - const [updatedTree, failedPaths] = await fetchAndInsertSubfolders( - childrenPaths, - treeWithWorkspaces, - ); - setStudiesTree(updatedTree); - failedPaths.forEach((path) => { - enqueueErrorSnackbar( - t("studies.tree.error.failToFetchFolder", { - path, - interpolation: { escapeValue: false }, - }), - t("studies.tree.error.detailsInConsole"), - ); - }); - } catch (error) { - setStudiesTree(initialStudiesTree); - enqueueErrorSnackbar( - t("studies.tree.error.failToFetchWorkspace"), - error as AxiosError, - ); - } - }; - - initializeFolders(); - }, [initialStudiesTree]); - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// + const updateTree = async (itemId: string, studyTreeNode: StudyTreeNode) => { + let treeAfterWorkspacesUpdate = studiesTree; + let chidrenPaths = studyTreeNode.children.map( + (child) => `root${child.path}`, + ); - const handleTreeItemClick = async ( - itemId: string, - studyTreeNode: StudyTreeNode, - ) => { - dispatch(updateStudyFilters({ folder: itemId })); if (itemId === "root") { try { - const nextTree = await fetchAndInsertWorkspaces(studiesTree); - setStudiesTree(nextTree); + treeAfterWorkspacesUpdate = await fetchAndInsertWorkspaces(studiesTree); + chidrenPaths = treeAfterWorkspacesUpdate.children.map( + (child) => `root${child.path}`, + ); } catch (error) { enqueueErrorSnackbar( "studies.tree.error.failToFetchWorkspace", - error as AxiosError, + toError(error), ); } } - const chidrenPaths = studyTreeNode.children.map( - (child) => `root${child.path}`, - ); // children paths and current element path - const [nextTree, failedPath] = await fetchAndInsertSubfolders( + let [treeAfterChildrenUpdate, failedPath] = await fetchAndInsertSubfolders( chidrenPaths, - studiesTree, + treeAfterWorkspacesUpdate, ); - setStudiesTree(nextTree); - for (const path of failedPath) { + if (failedPath.length > 0) { enqueueErrorSnackbar( t("studies.tree.error.failToFetchFolder", { - path, + path: failedPath.join(" "), interpolation: { escapeValue: false }, }), t("studies.tree.error.detailsInConsole"), ); } + setStudiesTree(treeAfterChildrenUpdate); + }; + + // Initialize folders once we have the tree + // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized + useUpdateEffectOnce(() => { + updateTree("root", initialStudiesTree); + }, [initialStudiesTree]); + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleTreeItemClick = async ( + itemId: string, + studyTreeNode: StudyTreeNode, + ) => { + dispatch(updateStudyFilters({ folder: itemId })); + updateTree(itemId, studyTreeNode); }; + //////////////////////////////////////////////////////////////// // JSX //////////////////////////////////////////////////////////////// From b59eb6f4df3e3cd122783b0f3e7aa0e345bb5aeb Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Thu, 5 Dec 2024 18:05:45 +0100 Subject: [PATCH 35/42] fix(ui): fix build --- webapp/src/components/App/Studies/StudyTree/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 354082b4de..9a66076740 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -56,10 +56,8 @@ function StudyTree() { } } // children paths and current element path - let [treeAfterChildrenUpdate, failedPath] = await fetchAndInsertSubfolders( - chidrenPaths, - treeAfterWorkspacesUpdate, - ); + const [treeAfterChildrenUpdate, failedPath] = + await fetchAndInsertSubfolders(chidrenPaths, treeAfterWorkspacesUpdate); if (failedPath.length > 0) { enqueueErrorSnackbar( t("studies.tree.error.failToFetchFolder", { From 474169b06fde40927ed4f6dc1a2244faa3e6e305 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Fri, 6 Dec 2024 02:24:29 +0100 Subject: [PATCH 36/42] refactor(ui): move code into clean sections --- .../App/Studies/StudyTree/index.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 9a66076740..0be6ccb9a8 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -36,7 +36,21 @@ function StudyTree() { const dispatch = useAppDispatch(); const [t] = useTranslation(); - const updateTree = async (itemId: string, studyTreeNode: StudyTreeNode) => { + //////////////////////////////////////////////////////////////// + // initialize + //////////////////////////////////////////////////////////////// + + // Initialize folders once we have the tree + // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized + useUpdateEffectOnce(() => { + updateTree("root", initialStudiesTree); + }, [initialStudiesTree]); + + //////////////////////////////////////////////////////////////// + // utils + //////////////////////////////////////////////////////////////// + + async function updateTree(itemId: string, studyTreeNode: StudyTreeNode) { let treeAfterWorkspacesUpdate = studiesTree; let chidrenPaths = studyTreeNode.children.map( (child) => `root${child.path}`, @@ -68,13 +82,7 @@ function StudyTree() { ); } setStudiesTree(treeAfterChildrenUpdate); - }; - - // Initialize folders once we have the tree - // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized - useUpdateEffectOnce(() => { - updateTree("root", initialStudiesTree); - }, [initialStudiesTree]); + } //////////////////////////////////////////////////////////////// // Event Handlers From d1fb1348a91085b3fb276f4ba3cb79b6b2703738 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 11 Dec 2024 11:00:46 +0100 Subject: [PATCH 37/42] refactor(ui): move test for study tree in their own folder --- .../App/Studies/StudyTree/{ => __test__}/fixtures.ts | 0 .../App/Studies/StudyTree/{ => __test__}/utils.test.tsx | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename webapp/src/components/App/Studies/StudyTree/{ => __test__}/fixtures.ts (100%) rename webapp/src/components/App/Studies/StudyTree/{ => __test__}/utils.test.tsx (97%) diff --git a/webapp/src/components/App/Studies/StudyTree/fixtures.ts b/webapp/src/components/App/Studies/StudyTree/__test__/fixtures.ts similarity index 100% rename from webapp/src/components/App/Studies/StudyTree/fixtures.ts rename to webapp/src/components/App/Studies/StudyTree/__test__/fixtures.ts diff --git a/webapp/src/components/App/Studies/StudyTree/utils.test.tsx b/webapp/src/components/App/Studies/StudyTree/__test__/utils.test.tsx similarity index 97% rename from webapp/src/components/App/Studies/StudyTree/utils.test.tsx rename to webapp/src/components/App/Studies/StudyTree/__test__/utils.test.tsx index 471d6f2885..87a5419c54 100644 --- a/webapp/src/components/App/Studies/StudyTree/utils.test.tsx +++ b/webapp/src/components/App/Studies/StudyTree/__test__/utils.test.tsx @@ -12,8 +12,8 @@ * This file is part of the Antares project. */ -import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "./utils"; -import { NonStudyFolderDTO, StudyTreeNode } from "../utils"; +import { insertFoldersIfNotExist, insertWorkspacesIfNotExist } from "../utils"; +import { NonStudyFolderDTO, StudyTreeNode } from "../../utils"; import { FIXTURES } from "./fixtures"; describe("StudyTree Utils", () => { From dfe103ccd577d93f274c6e5a96871e03c133ad3e Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Tue, 17 Dec 2024 17:19:45 +0100 Subject: [PATCH 38/42] refactor(ui): remove useless await --- webapp/src/components/App/Studies/StudyTree/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/App/Studies/StudyTree/utils.ts b/webapp/src/components/App/Studies/StudyTree/utils.ts index 5606791e6c..3380334e8c 100644 --- a/webapp/src/components/App/Studies/StudyTree/utils.ts +++ b/webapp/src/components/App/Studies/StudyTree/utils.ts @@ -111,7 +111,7 @@ async function fetchSubfolders(path: string): Promise { // path parts should be ["root", workspace, "folder1", ...] const workspace = pathParts[1]; const subPath = pathParts.slice(2).join("/"); - return await api.getFolders(workspace, subPath); + return api.getFolders(workspace, subPath); } /** From 8361ec549a9da1322a920e68319701ac17e73d5c Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Tue, 17 Dec 2024 17:30:48 +0100 Subject: [PATCH 39/42] fix(build): add forgotten file --- .../App/Studies/StudyTree/index.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 0be6ccb9a8..a5a852a652 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -101,17 +101,17 @@ function StudyTree() { //////////////////////////////////////////////////////////////// const buildTree = (children: StudyTreeNode[], parentId?: string) => { - return children.map((elm) => { - const id = parentId ? `${parentId}/${elm.name}` : elm.name; + return children.map((child) => { + const id = parentId ? `${parentId}/${child.name}` : child.name; return ( handleTreeItemClick(id, elm)} + label={child.name} + onClick={() => handleTreeItemClick(id, child)} > - {buildTree(elm.children, id)} + {buildTree(child.children, id)} ); }); @@ -121,7 +121,14 @@ function StudyTree() { {buildTree([studiesTree])} From b8ab1f7f827d43189e82853e9b9c2c5c63cc96c9 Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 18 Dec 2024 15:07:45 +0100 Subject: [PATCH 40/42] feat(ui): fetch current folder and not only children --- .../App/Studies/StudyTree/index.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index a5a852a652..47ed72d0c5 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -50,12 +50,24 @@ function StudyTree() { // utils //////////////////////////////////////////////////////////////// + /** + * This function is called at the initialization of the component and when the user clicks on a folder. + * + * The strategy to update the tree is to fetch the subfolders for a given folder when a user click on it. + * + * Doing the fetch only when the user clicks on a folder allows us to only fetch the data when the user needs it, + * and not the whole tree, which can be very large. + * + * @param itemId - The id of the item clicked + * @param studyTreeNode - The node of the item clicked + */ async function updateTree(itemId: string, studyTreeNode: StudyTreeNode) { let treeAfterWorkspacesUpdate = studiesTree; let chidrenPaths = studyTreeNode.children.map( (child) => `root${child.path}`, ); - + // If the user clicks on the root folder, we fetch the workspaces and insert them. + // Then we fetch the direct subfolders of the workspaces. if (itemId === "root") { try { treeAfterWorkspacesUpdate = await fetchAndInsertWorkspaces(studiesTree); @@ -68,8 +80,17 @@ function StudyTree() { toError(error), ); } + } else { + /* If the user clicks on a folder, we add the path of the clicked folder to the list of paths to fetch. + * as well as the path of the children of the clicked folder. + * If we don't fetch the subfolders of the children then we won't know if they're themselves folders, which we need + * to know to display the little arrow next to the subfolder. + * On the other hand, if we fetch only the subfolders of the children, then we won't fetch their "siblings" folder + * if one of them is added. + */ + chidrenPaths = [studyTreeNode.path].concat(chidrenPaths); } - // children paths and current element path + const [treeAfterChildrenUpdate, failedPath] = await fetchAndInsertSubfolders(chidrenPaths, treeAfterWorkspacesUpdate); if (failedPath.length > 0) { From 03c9ef0c30c482367e86d6a218be03f9ed1b21ce Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 18 Dec 2024 15:54:15 +0100 Subject: [PATCH 41/42] refactor(ui): add docs --- .../App/Studies/StudyTree/index.tsx | 10 +++++-- .../components/App/Studies/StudyTree/utils.ts | 28 +++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index 47ed72d0c5..b037f4814f 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -53,10 +53,14 @@ function StudyTree() { /** * This function is called at the initialization of the component and when the user clicks on a folder. * - * The strategy to update the tree is to fetch the subfolders for a given folder when a user click on it. + * The study tree is built from the studies in the database. There's a scan process that run on the server + * to update continuously the studies in the database. * - * Doing the fetch only when the user clicks on a folder allows us to only fetch the data when the user needs it, - * and not the whole tree, which can be very large. + * However this process can take a long time, and the user shouldn't wait for hours before he can see a study he knows is already uploaded. + * + * Instead of relying on the scan process to update the tree, we'll allow the user to walk into the tree and run a scan process only when he needs to. + * + * To enable this, we'll fetch the subfolders of a folder when the user clicks on it using the explorer API. * * @param itemId - The id of the item clicked * @param studyTreeNode - The node of the item clicked diff --git a/webapp/src/components/App/Studies/StudyTree/utils.ts b/webapp/src/components/App/Studies/StudyTree/utils.ts index 3380334e8c..14c4a5b008 100644 --- a/webapp/src/components/App/Studies/StudyTree/utils.ts +++ b/webapp/src/components/App/Studies/StudyTree/utils.ts @@ -18,14 +18,9 @@ import * as api from "../../../../services/api/study"; /** * Add a folder that was returned by the explorer into the study tree view. * - * This folder isn't a study, otherwise it wouldn't be - * returned by the explorer API, but this folder can have study in it - * and still not be in the initial study tree that's parsed from the study - * list, this happen when the studies in the folder aren't scanned yet. + * This function doesn't mutate the tree, it returns a new tree with the folder inserted. * - * However we want to allow the user to see these folder. When the user explore - * they shoudln't wait for a long running scan to complete before they're able to - * a folder in the hierarchy. + * If the folder is already in the tree, the tree returnred will be equal to the tree given to the function. * * @param studiesTree study tree to insert the folder into * @param folder folder to inert into the tree @@ -75,6 +70,10 @@ function insertFolderIfNotExist( /** * Insert several folders in the study tree if they don't exist already in the tree. * + * This function doesn't mutate the tree, it returns a new tree with the folders inserted + * + * The folders are inserted in the order they are given. + * * @param studiesTree study tree to insert the folder into * @param folders folders to inert into the tree * @param studiesTree study tree to insert the folder into @@ -121,6 +120,8 @@ async function fetchSubfolders(path: string): Promise { * * Subfolders are inserted only if they don't exist already in the tree. * + * This function doesn't mutate the tree, it returns a new tree with the subfolders inserted + * * @param paths list of paths to fetch the subfolders for * @param studiesTree study tree to insert the subfolders into * @returns a tuple with study tree with the subfolders inserted if they weren't already there and path for which @@ -149,6 +150,8 @@ export async function fetchAndInsertSubfolders( /** * Insert a workspace into the study tree if it doesn't exist already. * + * This function doesn't mutate the tree, it returns a new tree with the workspace inserted. + * * @param workspace key of the workspace * @param stydyTree study tree to insert the workspace into * @returns study tree with the empty workspace inserted if it wasn't already there. @@ -170,6 +173,10 @@ function insertWorkspaceIfNotExist( /** * Insert several workspaces into the study tree if they don't exist already in the tree. * + * This function doesn't mutate the tree, it returns a new tree with the workspaces inserted. + * + * The workspaces are inserted in the order they are given. + * * @param workspaces workspaces to insert into the tree * @param stydyTree study tree to insert the workspaces into * @returns study tree with the empty workspaces inserted if they weren't already there. @@ -184,7 +191,12 @@ export function insertWorkspacesIfNotExist( } /** - * fetch and insert the workspaces into the study tree. + * Fetch and insert the workspaces into the study tree. + * + * Workspaces are inserted only if they don't exist already in the tree. + * + * This function doesn't mutate the tree, it returns a new tree with the workspaces inserted. + * * @param studyTree study tree to insert the workspaces into * @returns study tree with the workspaces inserted if they weren't already there. */ From 442cd41d3915b2366665a780c08530481258f6ed Mon Sep 17 00:00:00 2001 From: Anis SMAIL Date: Wed, 18 Dec 2024 17:09:00 +0100 Subject: [PATCH 42/42] refactor(ui): small refactor --- .../App/Studies/StudyTree/index.tsx | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/webapp/src/components/App/Studies/StudyTree/index.tsx b/webapp/src/components/App/Studies/StudyTree/index.tsx index b037f4814f..510d70134e 100644 --- a/webapp/src/components/App/Studies/StudyTree/index.tsx +++ b/webapp/src/components/App/Studies/StudyTree/index.tsx @@ -36,20 +36,12 @@ function StudyTree() { const dispatch = useAppDispatch(); const [t] = useTranslation(); - //////////////////////////////////////////////////////////////// - // initialize - //////////////////////////////////////////////////////////////// - // Initialize folders once we have the tree // we use useUpdateEffectOnce because at first render initialStudiesTree isn't initialized useUpdateEffectOnce(() => { updateTree("root", initialStudiesTree); }, [initialStudiesTree]); - //////////////////////////////////////////////////////////////// - // utils - //////////////////////////////////////////////////////////////// - /** * This function is called at the initialization of the component and when the user clicks on a folder. * @@ -85,13 +77,12 @@ function StudyTree() { ); } } else { - /* If the user clicks on a folder, we add the path of the clicked folder to the list of paths to fetch. - * as well as the path of the children of the clicked folder. - * If we don't fetch the subfolders of the children then we won't know if they're themselves folders, which we need - * to know to display the little arrow next to the subfolder. - * On the other hand, if we fetch only the subfolders of the children, then we won't fetch their "siblings" folder - * if one of them is added. - */ + // If the user clicks on a folder, we add the path of the clicked folder to the list of paths to fetch. + // as well as the path of the children of the clicked folder. + // If we don't fetch the subfolders of the children then we won't know if they're themselves folders, which we need + // to know to display the little arrow next to the subfolder. + // On the other hand, if we fetch only the subfolders of the children, then we won't fetch their "siblings" folder + // if one of them is added. chidrenPaths = [studyTreeNode.path].concat(chidrenPaths); }