From 55263f3fb8690965918b76362d83d435cb75e37c Mon Sep 17 00:00:00 2001
From: Samir Kamal <1954121+skamril@users.noreply.github.com>
Date: Mon, 16 Sep 2024 11:17:31 +0200
Subject: [PATCH] feat(ui-debug): add import and export buttons on data views
* add `path` URL parameter
* display filename
---
.../Singlestudy/explore/Debug/Data/Folder.tsx | 143 ++++++++++++------
.../Singlestudy/explore/Debug/Data/Image.tsx | 13 +-
.../Singlestudy/explore/Debug/Data/Json.tsx | 23 ++-
.../Singlestudy/explore/Debug/Data/Text.tsx | 23 ++-
.../Singlestudy/explore/Debug/Data/index.tsx | 8 +-
.../Singlestudy/explore/Debug/Data/styles.ts | 13 --
.../Singlestudy/explore/Debug/Data/styles.tsx | 23 +++
.../Singlestudy/explore/Debug/Tree/index.tsx | 31 ++--
.../App/Singlestudy/explore/Debug/index.tsx | 45 +++++-
.../App/Singlestudy/explore/Debug/utils.ts | 21 +++
10 files changed, 249 insertions(+), 94 deletions(-)
delete mode 100644 webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.ts
create mode 100644 webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.tsx
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Folder.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Folder.tsx
index e52a403c90..33ac0465d6 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Folder.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Folder.tsx
@@ -16,56 +16,109 @@ import {
import { Fragment } from "react";
import EmptyView from "../../../../../common/page/SimpleContent";
import { useTranslation } from "react-i18next";
+import { Filename, Menubar } from "./styles";
+import UploadFileButton from "../../../../../common/buttons/UploadFileButton";
+import ConfirmationDialog from "../../../../../common/dialogs/ConfirmationDialog";
+import useConfirm from "../../../../../../hooks/useConfirm";
+
+function Folder(props: DataCompProps) {
+ const {
+ filename,
+ filePath,
+ treeData,
+ enableImport,
+ setSelectedFile,
+ reloadTreeData,
+ studyId,
+ } = props;
-function Folder({
- filename,
- filePath,
- treeData,
- setSelectedFile,
-}: DataCompProps) {
const { t } = useTranslation();
- const list = Object.entries(treeData as TreeFolder);
+ const replaceFile = useConfirm();
+ const treeFolder = treeData as TreeFolder;
+ const list = Object.entries(treeFolder);
+
+ ////////////////////////////////////////////////////////////////
+ // Event Handlers
+ ////////////////////////////////////////////////////////////////
+
+ const handleValidateUpload = (file: File) => {
+ if (treeFolder[file.name]) {
+ return replaceFile.showConfirm();
+ }
+ };
+
+ ////////////////////////////////////////////////////////////////
+ // JSX
+ ////////////////////////////////////////////////////////////////
return (
- {filename}}
- sx={{
- height: 1,
- overflow: "auto",
- }}
- dense
- >
- {list.length > 0 ? (
- list.map(([filename, data], index, arr) => {
- const fileType = getFileType(data);
- const Icon = getFileIcon(fileType);
- const isLast = index === arr.length - 1;
+ <>
+
+
+ {filename}
+ {enableImport && (
+ `${filePath}/${file.name}`}
+ onUploadSuccessful={reloadTreeData}
+ validate={handleValidateUpload}
+ />
+ )}
+
+
+ }
+ sx={{
+ height: 1,
+ overflow: "auto",
+ }}
+ dense
+ >
+ {list.length > 0 ? (
+ list.map(([filename, data], index, arr) => {
+ const fileType = getFileType(data);
+ const Icon = getFileIcon(fileType);
+ const isLast = index === arr.length - 1;
- return (
-
-
- setSelectedFile({
- fileType,
- filename,
- filePath: `${filePath}/${filename}`,
- treeData: data,
- })
- }
- >
-
-
-
-
-
- {!isLast && }
-
- );
- })
- ) : (
-
- )}
-
+ return (
+
+
+ setSelectedFile({
+ fileType,
+ filename,
+ filePath: `${filePath}/${filename}`,
+ treeData: data,
+ })
+ }
+ >
+
+
+
+
+
+ {!isLast && }
+
+ );
+ })
+ ) : (
+
+ )}
+
+
+ Another file with the same name already exists. Replacing it will
+ overwrite its content.
+
+ >
);
}
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx
index 8ef8ea39c1..87fcdbb3c8 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx
@@ -1,11 +1,20 @@
import { useTranslation } from "react-i18next";
import EmptyView from "../../../../../common/page/SimpleContent";
import ImageIcon from "@mui/icons-material/Image";
+import { Filename, Flex, Menubar } from "./styles";
+import type { DataCompProps } from "../utils";
-function Image() {
+function Image({ filename }: DataCompProps) {
const { t } = useTranslation();
- return ;
+ return (
+
+
+ {filename}
+
+
+
+ );
}
export default Image;
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx
index 6863fd92b1..0bd555f103 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Json.tsx
@@ -8,9 +8,10 @@ import type { DataCompProps } from "../utils";
import DownloadButton from "../../../../../common/buttons/DownloadButton";
import { downloadFile } from "../../../../../../utils/fileUtils";
import { useEffect, useState } from "react";
-import { Flex, Menubar } from "./styles";
+import { Filename, Flex, Menubar } from "./styles";
+import UploadFileButton from "../../../../../common/buttons/UploadFileButton";
-function Json({ filePath, filename, studyId }: DataCompProps) {
+function Json({ filePath, filename, studyId, enableImport }: DataCompProps) {
const [t] = useTranslation();
const { enqueueSnackbar } = useSnackbar();
const [currentJson, setCurrentJson] = useState();
@@ -45,10 +46,17 @@ function Json({ filePath, filename, studyId }: DataCompProps) {
const handleDownload = () => {
if (currentJson !== undefined) {
- downloadFile(JSON.stringify(currentJson, null, 2), `${filename}.json`);
+ downloadFile(
+ JSON.stringify(currentJson, null, 2),
+ filename.endsWith(".json") ? filename : `${filename}.json`,
+ );
}
};
+ const handleUploadSuccessful = () => {
+ res.reload();
+ };
+
////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////
@@ -59,6 +67,15 @@ function Json({ filePath, filename, studyId }: DataCompProps) {
ifResolved={(json) => (
+ {filename}
+ {enableImport && (
+
+ )}
{
if (res.data) {
- downloadFile(res.data, `${filename}.txt`);
+ downloadFile(
+ res.data,
+ filename.endsWith(".txt") ? filename : `${filename}.txt`,
+ );
}
};
+ const handleUploadSuccessful = () => {
+ res.reload();
+ };
+
////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////
@@ -80,6 +88,15 @@ function Text({ studyId, filePath, filename }: DataCompProps) {
ifResolved={(text) => (
+ {filename}
+ {enableImport && (
+
+ )}
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
index 8271049f04..6a46036da6 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx
@@ -10,6 +10,7 @@ import ViewWrapper from "../../../../../common/page/ViewWrapper";
interface Props extends FileInfo {
studyId: string;
setSelectedFile: (file: FileInfo) => void;
+ reloadTreeData: () => void;
}
type DataComponent = React.ComponentType;
@@ -22,13 +23,15 @@ const componentByFileType: Record = {
folder: Folder,
} as const;
-function Data({ studyId, setSelectedFile, ...fileInfo }: Props) {
+function Data(props: Props) {
+ const { studyId, setSelectedFile, reloadTreeData, ...fileInfo } = props;
const { fileType, filePath } = fileInfo;
+ const DataViewer = componentByFileType[fileType];
+
const enableImport =
(filePath === "user" || filePath.startsWith("user/")) &&
// To remove when Xpansion tool configuration will be moved to "input/expansion" directory
!(filePath === "user/expansion" || filePath.startsWith("user/expansion/"));
- const DataViewer = componentByFileType[fileType];
return (
@@ -37,6 +40,7 @@ function Data({ studyId, setSelectedFile, ...fileInfo }: Props) {
studyId={studyId}
enableImport={enableImport}
setSelectedFile={setSelectedFile}
+ reloadTreeData={reloadTreeData}
/>
);
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.ts b/webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.ts
deleted file mode 100644
index 416233c03c..0000000000
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { styled } from "@mui/material";
-
-export const Flex = styled("div")(({ theme }) => ({
- height: "100%",
- display: "flex",
- flexDirection: "column",
- gap: theme.spacing(1),
-}));
-
-export const Menubar = styled("div")({
- display: "flex",
- justifyContent: "flex-end",
-});
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.tsx
new file mode 100644
index 0000000000..8969d1ae8a
--- /dev/null
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/styles.tsx
@@ -0,0 +1,23 @@
+import { styled } from "@mui/material";
+
+export const Flex = styled("div")(({ theme }) => ({
+ height: "100%",
+ display: "flex",
+ flexDirection: "column",
+ gap: theme.spacing(1),
+}));
+
+export const Menubar = styled("div")(({ theme }) => ({
+ display: "flex",
+ justifyContent: "flex-end",
+ alignItems: "center",
+ gap: theme.spacing(1),
+}));
+
+export const Filename = styled((props: { children?: string }) => (
+
+))({
+ flex: 1,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+});
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Tree/index.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Tree/index.tsx
index c7ad5146cb..4bec856a77 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/Tree/index.tsx
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/Tree/index.tsx
@@ -1,24 +1,17 @@
import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
import FileTreeItem from "./FileTreeItem";
-import type { TreeFolder } from "../utils";
-import { useState } from "react";
+import { getParentPaths, type TreeFolder } from "../utils";
interface Props {
data: TreeFolder;
- // `selectedItems` must not be undefined to make `SimpleTreeView` controlled
- selectedItemId: string | null;
+ // `currentPath` must not be undefined to make `SimpleTreeView` controlled
+ currentPath: string | null;
+ expandedItems: string[];
+ setExpandedItems: React.Dispatch>;
}
-function getParentItemIds(itemId: string) {
- // "a/b/c/d" -> ["a", "a/b", "a/b/c"]
- return itemId
- .split("/")
- .slice(0, -1) // Remove the last item
- .map((_, index, arr) => arr.slice(0, index + 1).join("/"));
-}
-
-function Tree({ data, selectedItemId }: Props) {
- const [expandedItems, setExpandedItems] = useState([]);
+function Tree(props: Props) {
+ const { data, currentPath, expandedItems, setExpandedItems } = props;
////////////////////////////////////////////////////////////////
// Event Handlers
@@ -36,14 +29,16 @@ function Tree({ data, selectedItemId }: Props) {
////////////////////////////////////////////////////////////////
// `SimpleTreeView` must be controlled because selected item can be changed manually
- // by `Folder` component
+ // by `Folder` component, or by the `path` URL parameter at view mount.
+ // The use of `selectedItems` and `expandedItems` make the component controlled.
return (
();
const [selectedFile, setSelectedFile] = useState(null);
+ // Allow to keep expanded items when the tree is reloaded with `reloadTreeData`
+ const [expandedItems, setExpandedItems] = useState([]);
+ const [searchParams, setSearchParams] = useSearchParams();
+ const pathInUrl = searchParams.get("path");
const res = usePromiseWithSnackbarError(
async () => {
@@ -45,21 +49,43 @@ function Debug() {
useUpdateEffect(() => {
const firstChildName = Object.keys(res.data ?? {})[0];
- const treeData = R.path([firstChildName], res.data);
+ const firstChildTreeData = R.path([firstChildName], res.data);
- if (treeData) {
- const fileInfo = {
- fileType: getFileType(treeData),
+ const pathInUrlParts = pathInUrl?.split("/");
+ const urlPathTreeData = pathInUrlParts
+ ? R.path(pathInUrlParts, res.data)
+ : null;
+
+ let fileInfo: FileInfo | null = null;
+
+ if (urlPathTreeData) {
+ fileInfo = {
+ fileType: getFileType(urlPathTreeData),
+ treeData: urlPathTreeData,
+ filename: R.last(pathInUrlParts!)!,
+ filePath: pathInUrl!,
+ };
+ } else if (firstChildTreeData) {
+ fileInfo = {
+ fileType: getFileType(firstChildTreeData),
+ treeData: firstChildTreeData,
filename: firstChildName,
filePath: firstChildName,
- treeData,
};
+ }
+ if (fileInfo) {
setSelectedFile(fileInfo);
} else {
setSelectedFile(null);
}
- }, [res?.data]);
+ }, [res.data, pathInUrl]);
+
+ useUpdateEffect(() => {
+ if (selectedFile?.filePath !== pathInUrl) {
+ setSearchParams({ path: selectedFile?.filePath || "" });
+ }
+ }, [selectedFile?.filePath]);
////////////////////////////////////////////////////////////////
// JSX
@@ -74,7 +100,9 @@ function Debug() {
)}
@@ -85,6 +113,7 @@ function Debug() {
)}
diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
index 50d3ae73cf..86be694f83 100644
--- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
+++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
@@ -31,6 +31,7 @@ export interface DataCompProps extends FileInfo {
studyId: string;
enableImport: boolean;
setSelectedFile: (file: FileInfo) => void;
+ reloadTreeData: () => void;
}
////////////////////////////////////////////////////////////////
@@ -83,3 +84,23 @@ export function getFileType(treeData: TreeData): FileType {
}
return isFolder(treeData) ? "folder" : "text";
}
+
+////////////////////////////////////////////////////////////////
+// Tree
+////////////////////////////////////////////////////////////////
+
+/**
+ * Get parent paths of a given path.
+ *
+ * @example
+ * getParentPaths("a/b/c/d"); // Returns: ["a", "a/b", "a/b/c"]
+ *
+ * @param path - The path from which to get the parent paths.
+ * @returns The parent paths.
+ */
+export function getParentPaths(path: string) {
+ return path
+ .split("/")
+ .slice(0, -1) // Remove the last item
+ .map((_, index, arr) => arr.slice(0, index + 1).join("/"));
+}