From 3dd23884ec338a37c9061453d0f6fc11369c92ca Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 2 Dec 2024 15:52:05 +0100 Subject: [PATCH 1/3] feat(ui-debug): add unsupported files handling --- webapp/public/locales/en/main.json | 1 + webapp/public/locales/fr/main.json | 1 + .../Singlestudy/explore/Debug/Data/Image.tsx | 34 --------- .../Singlestudy/explore/Debug/Data/Matrix.tsx | 2 +- .../explore/Debug/Data/Unsupported.tsx | 75 +++++++++++++++++++ .../Singlestudy/explore/Debug/Data/index.tsx | 20 ++--- .../App/Singlestudy/explore/Debug/utils.ts | 35 +++++++-- 7 files changed, 115 insertions(+), 53 deletions(-) delete mode 100644 webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx create mode 100644 webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 952091b0d4..d6bbea522e 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -252,6 +252,7 @@ "study.bindingconstraints": "Binding Constraints", "study.debug": "Debug", "study.debug.file.image": "Image file", + "study.debug.file.unsupported": "Unsupported file type", "study.debug.file.deleteConfirm.title": "Delete File?", "study.debug.file.deleteConfirm.message": "Are you sure you want to delete the file?", "study.debug.folder.empty": "Folder is empty", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 81ffca6679..9ddb68910e 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -253,6 +253,7 @@ "study.debug": "Debug", "study.debug.file.image": "Fichier image", "study.debug.folder.empty": "Le dossier est vide", + "study.debug.file.unsupported": "Type de fichier non supporté", "study.debug.file.deleteConfirm.title": "Supprimer le fichier ?", "study.debug.file.deleteConfirm.message": "Êtes-vous sûr de vouloir supprimer le fichier ?", "study.debug.folder.upload.replaceFileConfirm.title": "Remplacer le fichier ?", diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx deleted file mode 100644 index 23aa34e993..0000000000 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Image.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 { 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({ filename }: DataCompProps) { - const { t } = useTranslation(); - - return ( - - - {filename} - - - - ); -} - -export default Image; diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx index a140fe71e8..26a211251d 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Matrix.tsx @@ -15,7 +15,7 @@ import Matrix from "../../../../../common/Matrix"; import type { DataCompProps } from "../utils"; -function DebugMatrix({ studyId, filename, filePath, canEdit }: DataCompProps) { +function DebugMatrix({ filename, filePath, canEdit }: DataCompProps) { return ; } diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx new file mode 100644 index 0000000000..59c4f48950 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx @@ -0,0 +1,75 @@ +/** + * 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 { useTranslation } from "react-i18next"; +import EmptyView from "../../../../../common/page/SimpleContent"; +import BlockIcon from "@mui/icons-material/Block"; +import { Filename, Flex, Menubar } from "./styles"; +import type { DataCompProps } from "../utils"; +import DownloadButton from "@/components/common/buttons/DownloadButton"; +import UploadFileButton from "@/components/common/buttons/UploadFileButton"; +import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError"; +import { getStudyData } from "@/services/api/study"; +import { downloadFile } from "@/utils/fileUtils"; + +function Unsupported({ studyId, filePath, filename }: DataCompProps) { + const { t } = useTranslation(); + + const res = usePromiseWithSnackbarError( + () => getStudyData(studyId, filePath), + { + errorMessage: t("studies.error.retrieveData"), + deps: [studyId, filePath], + }, + ); + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleDownload = () => { + if (res.data) { + downloadFile( + res.data, + filename.endsWith(".txt") ? filename : `${filename}.txt`, + ); + } + }; + + const handleUploadSuccessful = () => { + res.reload(); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + + {filename} + + + + + + ); +} + +export default Unsupported; 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 f387b7ef9c..de7e0ca24e 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx @@ -12,15 +12,16 @@ * This file is part of the Antares project. */ +import { ComponentType } from "react"; import Text from "./Text"; -import Image from "./Image"; -import Json from "./Json"; +import Unsupported from "./Unsupported"; import Matrix from "./Matrix"; import Folder from "./Folder"; import { canEditFile, type FileInfo, type FileType } from "../utils"; import type { DataCompProps } from "../utils"; import ViewWrapper from "../../../../../common/page/ViewWrapper"; import type { StudyMetadata } from "../../../../../../common/types"; +import Json from "./Json"; interface Props extends FileInfo { study: StudyMetadata; @@ -28,28 +29,23 @@ interface Props extends FileInfo { reloadTreeData: () => void; } -type DataComponent = React.ComponentType; - -const componentByFileType: Record = { +const componentByFileType: Record> = { matrix: Matrix, json: Json, text: Text, - image: Image, + unsupported: Unsupported, folder: Folder, } as const; -function Data(props: Props) { - const { study, setSelectedFile, reloadTreeData, ...fileInfo } = props; - const { fileType, filePath } = fileInfo; - const canEdit = canEditFile(study, filePath); - const DataViewer = componentByFileType[fileType]; +function Data({ study, setSelectedFile, reloadTreeData, ...fileInfo }: Props) { + const DataViewer = componentByFileType[fileInfo.fileType]; return ( diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts index b1d8653f07..d192aad6d9 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts @@ -14,7 +14,7 @@ import DataObjectIcon from "@mui/icons-material/DataObject"; import TextSnippetIcon from "@mui/icons-material/TextSnippet"; -import ImageIcon from "@mui/icons-material/Image"; +import BlockIcon from "@mui/icons-material/Block"; import FolderIcon from "@mui/icons-material/Folder"; import DatasetIcon from "@mui/icons-material/Dataset"; import { SvgIconComponent } from "@mui/icons-material"; @@ -33,7 +33,7 @@ export interface TreeFolder { export type TreeData = TreeFolder | TreeFile; -export type FileType = "json" | "matrix" | "text" | "image" | "folder"; +export type FileType = "json" | "matrix" | "text" | "folder" | "unsupported"; export interface FileInfo { fileType: FileType; @@ -58,8 +58,8 @@ const iconByFileType: Record = { matrix: DatasetIcon, json: DataObjectIcon, text: TextSnippetIcon, - image: ImageIcon, folder: FolderIcon, + unsupported: BlockIcon, } as const; /** @@ -83,21 +83,44 @@ export function isFolder(treeData: TreeData): treeData is TreeFolder { * @returns The corresponding file type. */ export function getFileType(treeData: TreeData): FileType { + if (isFolder(treeData)) { + return "folder"; + } + if (typeof treeData === "string") { + // Handle matrix files if ( treeData.startsWith("matrix://") || treeData.startsWith("matrixfile://") ) { return "matrix"; } + + // Handle files displayed as JSON by the API even though they are .ini files in the filesystem. + // The json:// prefix or .json extension indicates the content should be viewed as JSON. if (treeData.startsWith("json://") || treeData.endsWith(".json")) { return "json"; } - if (treeData.startsWith("file://") && treeData.endsWith(".ico")) { - return "image"; + + // Handle regular files with file:// prefix + // All files except matrices and json-formatted content use this prefix + // We filter to only allow extensions that can be properly displayed (.txt, .log, .csv, .tsv, .ini) + // Other extensions (like .RDS or .xlsx) are marked as unsupported since they can't be shown in the UI + if (treeData.startsWith("file://")) { + const supportedTextExtensions = [".txt", ".log", ".csv", ".tsv", ".ini"]; + + // Check if the file ends with any of the supported extensions + if (supportedTextExtensions.some((ext) => treeData.endsWith(ext))) { + return "text"; + } + + // Any other extension with file:// prefix is unsupported + return "unsupported"; } } - return isFolder(treeData) ? "folder" : "text"; + + // Default to text for any other string content + return "text"; } //////////////////////////////////////////////////////////////// From aa373d5f0fb326d8621613853191fca897f33a41 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Mon, 2 Dec 2024 15:52:28 +0100 Subject: [PATCH 2/3] fix(ui-debug): prevent empty text files display --- webapp/public/locales/en/main.json | 1 - webapp/public/locales/fr/main.json | 1 - .../Singlestudy/explore/Debug/Data/Text.tsx | 53 +++++++++++++------ .../explore/Debug/Data/Unsupported.tsx | 6 +-- .../App/Singlestudy/explore/Debug/utils.ts | 42 ++++++--------- 5 files changed, 54 insertions(+), 49 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index d6bbea522e..df948d3920 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -251,7 +251,6 @@ "study.district": "District", "study.bindingconstraints": "Binding Constraints", "study.debug": "Debug", - "study.debug.file.image": "Image file", "study.debug.file.unsupported": "Unsupported file type", "study.debug.file.deleteConfirm.title": "Delete File?", "study.debug.file.deleteConfirm.message": "Are you sure you want to delete the file?", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 9ddb68910e..3dacacb6e0 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -251,7 +251,6 @@ "study.district": "District", "study.bindingconstraints": "Contraintes Couplantes", "study.debug": "Debug", - "study.debug.file.image": "Fichier image", "study.debug.folder.empty": "Le dossier est vide", "study.debug.file.unsupported": "Type de fichier non supporté", "study.debug.file.deleteConfirm.title": "Supprimer le fichier ?", diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx index 7380186bc3..f2ac0a31e1 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx @@ -31,6 +31,8 @@ import DownloadButton from "../../../../../common/buttons/DownloadButton"; import { downloadFile } from "../../../../../../utils/fileUtils"; import { Filename, Flex, Menubar } from "./styles"; import UploadFileButton from "../../../../../common/buttons/UploadFileButton"; +import EmptyView from "@/components/common/page/SimpleContent"; +import GridOffIcon from "@mui/icons-material/GridOff"; SyntaxHighlighter.registerLanguage("xml", xml); SyntaxHighlighter.registerLanguage("plaintext", plaintext); @@ -42,6 +44,16 @@ const logsRegex = /^(\[[^\]]*\]){3}/; // Ex: "EXP : 0" const propertiesRegex = /^[^:]+ : [^:]+/; +function isEmptyContent(text: string | string[]): boolean { + if (Array.isArray(text)) { + return ( + !text || text.every((line) => typeof line === "string" && !line.trim()) + ); + } + + return typeof text !== "string" || !text.trim(); +} + function getSyntaxProps(data: string | string[]): SyntaxHighlighterProps { const isArray = Array.isArray(data); const text = isArray ? data.join("\n") : data; @@ -111,24 +123,31 @@ function Text({ studyId, filePath, filename, canEdit }: DataCompProps) { onUploadSuccessful={handleUploadSuccessful} /> )} - - - - - + + {isEmptyContent(text) ? ( + + ) : ( + + + + )} )} /> diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx index 59c4f48950..163235ba6f 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx @@ -40,10 +40,7 @@ function Unsupported({ studyId, filePath, filename }: DataCompProps) { const handleDownload = () => { if (res.data) { - downloadFile( - res.data, - filename.endsWith(".txt") ? filename : `${filename}.txt`, - ); + downloadFile(res.data, filename); } }; @@ -62,7 +59,6 @@ function Unsupported({ studyId, filePath, filename }: DataCompProps) { diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts index d192aad6d9..f0e9c428b8 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts @@ -50,9 +50,17 @@ export interface DataCompProps extends FileInfo { } //////////////////////////////////////////////////////////////// -// File Info +// Utils //////////////////////////////////////////////////////////////// +const URL_SCHEMES = { + MATRIX: ["matrix://", "matrixfile://"], + JSON: "json://", + FILE: "file://", +} as const; + +const SUPPORTED_EXTENSIONS = [".txt", ".log", ".csv", ".tsv", ".ini"] as const; + // Maps file types to their corresponding icon components. const iconByFileType: Record = { matrix: DatasetIcon, @@ -88,17 +96,11 @@ export function getFileType(treeData: TreeData): FileType { } if (typeof treeData === "string") { - // Handle matrix files - if ( - treeData.startsWith("matrix://") || - treeData.startsWith("matrixfile://") - ) { + if (URL_SCHEMES.MATRIX.some((scheme) => treeData.startsWith(scheme))) { return "matrix"; } - // Handle files displayed as JSON by the API even though they are .ini files in the filesystem. - // The json:// prefix or .json extension indicates the content should be viewed as JSON. - if (treeData.startsWith("json://") || treeData.endsWith(".json")) { + if (treeData.startsWith(URL_SCHEMES.JSON)) { return "json"; } @@ -106,27 +108,17 @@ export function getFileType(treeData: TreeData): FileType { // All files except matrices and json-formatted content use this prefix // We filter to only allow extensions that can be properly displayed (.txt, .log, .csv, .tsv, .ini) // Other extensions (like .RDS or .xlsx) are marked as unsupported since they can't be shown in the UI - if (treeData.startsWith("file://")) { - const supportedTextExtensions = [".txt", ".log", ".csv", ".tsv", ".ini"]; - - // Check if the file ends with any of the supported extensions - if (supportedTextExtensions.some((ext) => treeData.endsWith(ext))) { - return "text"; - } - - // Any other extension with file:// prefix is unsupported - return "unsupported"; + if ( + treeData.startsWith(URL_SCHEMES.FILE) && + SUPPORTED_EXTENSIONS.some((ext) => treeData.endsWith(ext)) + ) { + return "text"; } } - // Default to text for any other string content - return "text"; + return "unsupported"; } -//////////////////////////////////////////////////////////////// -// Rights -//////////////////////////////////////////////////////////////// - /** * Checks if a study's file can be edited. * From 2c7eeac5d9fda9aefad0d502ba068979257efba1 Mon Sep 17 00:00:00 2001 From: hatim dinia Date: Wed, 11 Dec 2024 15:32:07 +0100 Subject: [PATCH 3/3] feat(ui-debug): display output matrices as raw text --- .../Singlestudy/explore/Debug/Data/Text.tsx | 44 ++--- .../explore/Debug/Data/Unsupported.tsx | 14 +- .../Singlestudy/explore/Debug/Data/index.tsx | 10 +- .../App/Singlestudy/explore/Debug/utils.ts | 168 ++++++++++++++++-- 4 files changed, 198 insertions(+), 38 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx index f2ac0a31e1..1f7a7f4909 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Text.tsx @@ -26,7 +26,7 @@ import plaintext from "react-syntax-highlighter/dist/esm/languages/hljs/plaintex import ini from "react-syntax-highlighter/dist/esm/languages/hljs/ini"; import properties from "react-syntax-highlighter/dist/esm/languages/hljs/properties"; import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; -import type { DataCompProps } from "../utils"; +import { isEmptyContent, parseContent, type DataCompProps } from "../utils"; import DownloadButton from "../../../../../common/buttons/DownloadButton"; import { downloadFile } from "../../../../../../utils/fileUtils"; import { Filename, Flex, Menubar } from "./styles"; @@ -44,16 +44,6 @@ const logsRegex = /^(\[[^\]]*\]){3}/; // Ex: "EXP : 0" const propertiesRegex = /^[^:]+ : [^:]+/; -function isEmptyContent(text: string | string[]): boolean { - if (Array.isArray(text)) { - return ( - !text || text.every((line) => typeof line === "string" && !line.trim()) - ); - } - - return typeof text !== "string" || !text.trim(); -} - function getSyntaxProps(data: string | string[]): SyntaxHighlighterProps { const isArray = Array.isArray(data); const text = isArray ? data.join("\n") : data; @@ -75,15 +65,24 @@ function getSyntaxProps(data: string | string[]): SyntaxHighlighterProps { }; } -function Text({ studyId, filePath, filename, canEdit }: DataCompProps) { +function Text({ + studyId, + filePath, + filename, + fileType, + canEdit, +}: DataCompProps) { const { t } = useTranslation(); const theme = useTheme(); const res = usePromiseWithSnackbarError( - () => getStudyData(studyId, filePath), + () => + getStudyData(studyId, filePath).then((text) => + parseContent(text, { filePath, fileType }), + ), { errorMessage: t("studies.error.retrieveData"), - deps: [studyId, filePath], + deps: [studyId, filePath, fileType], }, ); @@ -123,15 +122,19 @@ function Text({ studyId, filePath, filename, canEdit }: DataCompProps) { onUploadSuccessful={handleUploadSuccessful} /> )} - + - {isEmptyContent(text) ? ( + {isEmptyContent(text) ? ( // TODO remove when files become editable ) : ( - + diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx index 163235ba6f..e4bcfccc5c 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/Unsupported.tsx @@ -23,7 +23,7 @@ import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError"; import { getStudyData } from "@/services/api/study"; import { downloadFile } from "@/utils/fileUtils"; -function Unsupported({ studyId, filePath, filename }: DataCompProps) { +function Unsupported({ studyId, filePath, filename, canEdit }: DataCompProps) { const { t } = useTranslation(); const res = usePromiseWithSnackbarError( @@ -56,11 +56,13 @@ function Unsupported({ studyId, filePath, filename }: DataCompProps) { {filename} - + {canEdit && ( + + )} 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 de7e0ca24e..9d09c246a5 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Debug/Data/index.tsx @@ -17,7 +17,12 @@ import Text from "./Text"; import Unsupported from "./Unsupported"; import Matrix from "./Matrix"; import Folder from "./Folder"; -import { canEditFile, type FileInfo, type FileType } from "../utils"; +import { + canEditFile, + getEffectiveFileType, + type FileInfo, + type FileType, +} from "../utils"; import type { DataCompProps } from "../utils"; import ViewWrapper from "../../../../../common/page/ViewWrapper"; import type { StudyMetadata } from "../../../../../../common/types"; @@ -38,7 +43,8 @@ const componentByFileType: Record> = { } as const; function Data({ study, setSelectedFile, reloadTreeData, ...fileInfo }: Props) { - const DataViewer = componentByFileType[fileInfo.fileType]; + const fileType = getEffectiveFileType(fileInfo.filePath, fileInfo.fileType); + const DataViewer = componentByFileType[fileType]; return ( diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts index f0e9c428b8..d546a1e4dc 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts @@ -20,6 +20,7 @@ import DatasetIcon from "@mui/icons-material/Dataset"; import { SvgIconComponent } from "@mui/icons-material"; import * as RA from "ramda-adjunct"; import type { StudyMetadata } from "../../../../../common/types"; +import { MatrixDataDTO } from "@/components/common/Matrix/shared/types"; //////////////////////////////////////////////////////////////// // Types @@ -49,8 +50,13 @@ export interface DataCompProps extends FileInfo { reloadTreeData: () => void; } +interface ContentParsingOptions { + filePath: string; + fileType: string; +} + //////////////////////////////////////////////////////////////// -// Utils +// Constants //////////////////////////////////////////////////////////////// const URL_SCHEMES = { @@ -59,7 +65,14 @@ const URL_SCHEMES = { FILE: "file://", } as const; -const SUPPORTED_EXTENSIONS = [".txt", ".log", ".csv", ".tsv", ".ini"] as const; +const SUPPORTED_EXTENSIONS = [ + ".txt", + ".log", + ".csv", + ".tsv", + ".ini", + ".yml", +] as const; // Maps file types to their corresponding icon components. const iconByFileType: Record = { @@ -70,6 +83,10 @@ const iconByFileType: Record = { unsupported: BlockIcon, } as const; +//////////////////////////////////////////////////////////////// +// Functions +//////////////////////////////////////////////////////////////// + /** * Gets the icon component for a given file type. * @@ -108,15 +125,15 @@ export function getFileType(treeData: TreeData): FileType { // All files except matrices and json-formatted content use this prefix // We filter to only allow extensions that can be properly displayed (.txt, .log, .csv, .tsv, .ini) // Other extensions (like .RDS or .xlsx) are marked as unsupported since they can't be shown in the UI - if ( - treeData.startsWith(URL_SCHEMES.FILE) && - SUPPORTED_EXTENSIONS.some((ext) => treeData.endsWith(ext)) - ) { - return "text"; - } + return treeData.startsWith(URL_SCHEMES.FILE) && + SUPPORTED_EXTENSIONS.some((ext) => + treeData.toLowerCase().endsWith(ext.toLowerCase()), + ) + ? "text" + : "unsupported"; } - return "unsupported"; + return "text"; } /** @@ -130,7 +147,138 @@ export function canEditFile(study: StudyMetadata, filePath: string): boolean { return ( !study.archived && (filePath === "user" || filePath.startsWith("user/")) && - // To remove when Xpansion tool configuration will be moved to "input/expansion" directory + // TODO: remove when Xpansion tool configuration will be moved to "input/expansion" directory !(filePath === "user/expansion" || filePath.startsWith("user/expansion/")) ); } + +/** + * Checks if a file path is within the output folder + * + * @param path - The file path to check + * @returns Whether the path is in the output folder + */ +export function isInOutputFolder(path: string): boolean { + return path.startsWith("output/"); +} + +/** + * Determines if .txt files content is empty + * + * @param text - Content of .txt to check + * @returns boolean indicating if content is effectively empty + */ +export function isEmptyContent(text: string | string[]): boolean { + if (Array.isArray(text)) { + return ( + !text || text.every((line) => typeof line === "string" && !line.trim()) + ); + } + + return typeof text === "string" && !text.trim(); +} + +/** + * !Temporary workaround for matrix data display in output folders. + * + * Context: + * In output folders, matrix data can be returned by the API in two different formats: + * 1. As an unparsed JSON string containing the matrix data + * 2. As an already parsed MatrixDataDTO object + * + * This inconsistency exists because the API's matrix parsing behavior differs between + * output folders and other locations. Additionally, there's a UI requirement to display + * matrices from output folders as raw text rather than formatted tables. + * + * The workaround consists of three functions: + * 1. getEffectiveFileType: Forces matrix files in output folders to use text display + * 2. parseResponse: Handles the dual format nature of the API response + * 3. parseContent: Orchestrates the parsing logic based on file location and type + * + * TODO: This temporary solution will be removed once: + * - The API provides consistent matrix parsing across all folders + * - UI requirements for matrix display are finalized + */ + +/** + * Forces matrix files in output folders to be displayed as text + * This is necessary because matrices in output folders need to be shown + * in their raw format rather than as formatted tables + * + * @param filePath - Path to the file being displayed + * @param originalType - Original file type as determined by the system + * @returns Modified file type (forces 'text' for matrices in output folders) + */ +export function getEffectiveFileType( + filePath: string, + originalType: FileType, +): FileType { + if (isInOutputFolder(filePath) && originalType === "matrix") { + return "text"; + } + + return originalType; +} + +/** + * Formats a 2D number array into a string representation + * + * @param matrix - 2D array of numbers to format + * @returns String representation of the matrix + */ +function formatMatrixToString(matrix: number[][]): string { + return matrix + .map((row) => row.map((val) => val.toString()).join("\t")) + .join("\n"); +} + +/** + * Handles parsing of matrix data from the API, dealing with both + * string and pre-parsed object formats + * + * @param res - API response containing matrix data (either MatrixDataDTO or string) + * @returns Extracted matrix data as a string + */ +function parseResponse(res: string | MatrixDataDTO): string { + if (typeof res === "object") { + // Handle case where API has already parsed the JSON into MatrixDataDTO + return formatMatrixToString(res.data); + } + + try { + // Handle case where API returns unparsed JSON string + // Replace special numeric values with their string representations + const sanitizedJson = res + .replace(/NaN/g, '"NaN"') + .replace(/Infinity/g, '"Infinity"'); + + const parsed = JSON.parse(sanitizedJson); + return formatMatrixToString(parsed.data); + } catch (e) { + // If JSON parsing fails, assume it's plain text + return res; + } +} + +/** + * Main content parsing function that orchestrates the matrix display workaround + * + * @param content - Raw content from the API (either string or parsed object) + * @param options - Configuration options including file path and type + * @returns Processed content ready for display + */ +export function parseContent( + content: string, + options: ContentParsingOptions, +): string { + const { filePath, fileType } = options; + + if (isInOutputFolder(filePath) && fileType === "matrix") { + // Apply special handling for matrices in output folders + return parseResponse(content); + } + + return content || ""; +} + +// !End of Matrix Display Workaround