From 8444610475af2bcb84e923f9442c22a65cec99c5 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Wed, 27 Aug 2025 12:25:33 -0400 Subject: [PATCH 01/14] feat: transform JSON into hierarchical folder structure in dataset detail page; refs #88 --- .../FileTree/FileTreeRow.tsx | 65 ++++++++++++++ .../DatasetDetailPage/FileTree/types.ts | 8 +- .../DatasetDetailPage/FileTree/utils.ts | 86 +++++++++++++++---- src/components/Routes.tsx | 4 +- src/pages/UpdatedDatasetDetailPage.tsx | 84 ++++++++++-------- 5 files changed, 194 insertions(+), 53 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index cd2f900..b192474 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -1,3 +1,4 @@ +// for rendering the preview and download buttons in folder structure row import type { TreeNode } from "./types"; import { formatLeafValue, isPreviewable } from "./utils"; import DownloadIcon from "@mui/icons-material/Download"; @@ -18,6 +19,41 @@ type Props = { const FileTreeRow: React.FC = ({ node, level, onPreview }) => { const [open, setOpen] = React.useState(false); + // if (node.kind === "folder") { + // return ( + // <> + // setOpen((o) => !o)} + // > + // + // + // + // {node.name} + // {open ? : } + // + + // + // {node.children.map((child) => ( + // + // ))} + // + // + // ); + // } if (node.kind === "folder") { return ( <> @@ -36,7 +72,36 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { + {node.name} + + {/* ✅ Actions on folder if it carries a link (from linkHere) */} + {node.link?.url && ( + e.stopPropagation()} // don't toggle expand + > + + {isPreviewable(node.link.url) && ( + + )} + + )} + {open ? : } diff --git a/src/components/DatasetDetailPage/FileTree/types.ts b/src/components/DatasetDetailPage/FileTree/types.ts index 211d9c5..eeb5c50 100644 --- a/src/components/DatasetDetailPage/FileTree/types.ts +++ b/src/components/DatasetDetailPage/FileTree/types.ts @@ -2,5 +2,11 @@ export type LinkMeta = { url: string; index: number }; // this value can be one of these types export type TreeNode = - | { kind: "folder"; name: string; path: string; children: TreeNode[] } + | { + kind: "folder"; + name: string; + path: string; + children: TreeNode[]; + link?: LinkMeta; + } | { kind: "file"; name: string; path: string; value?: any; link?: LinkMeta }; diff --git a/src/components/DatasetDetailPage/FileTree/utils.ts b/src/components/DatasetDetailPage/FileTree/utils.ts index b278647..d5da77b 100644 --- a/src/components/DatasetDetailPage/FileTree/utils.ts +++ b/src/components/DatasetDetailPage/FileTree/utils.ts @@ -23,8 +23,9 @@ export const formatLeafValue = (v: any): string => { }; // ignore meta keys -export const shouldSkipKey = (key: string) => - key === "_id" || key === "_rev" || key.startsWith("."); +// export const shouldSkipKey = (key: string) => +// key === "_id" || key === "_rev" || key.startsWith("."); +export const shouldSkipKey = (_key: string) => false; // build path -> {url, index} lookup, built from extractDataLinks function // if external link objects have {path, url, index}, build a Map for the tree @@ -39,37 +40,92 @@ export const makeLinkMap = < }; // Recursively convert the dataset JSON to a file-tree +// export const buildTreeFromDoc = ( +// doc: any, +// linkMap: Map, +// curPath = "" +// ): TreeNode[] => { +// if (!doc || typeof doc !== "object") return []; +// const out: TreeNode[] = []; + +// Object.keys(doc).forEach((key) => { +// if (shouldSkipKey(key)) return; + +// const val = doc[key]; +// const path = `${curPath}/${key}`; +// const link = linkMap.get(path); + +// if (link) { +// out.push({ kind: "file", name: key, path, link }); +// return; +// } + +// if (val && typeof val === "object" && !Array.isArray(val)) { +// out.push({ +// kind: "folder", +// name: key, +// path, +// children: buildTreeFromDoc(val, linkMap, path), +// }); +// return; +// } + +// out.push({ kind: "file", name: key, path, value: val }); +// }); + +// return out; +// }; export const buildTreeFromDoc = ( doc: any, linkMap: Map, curPath = "" ): TreeNode[] => { - if (!doc || typeof doc !== "object") return []; + if (doc === null || typeof doc !== "object") return []; + const out: TreeNode[] = []; - Object.keys(doc).forEach((key) => { - if (shouldSkipKey(key)) return; + if (Array.isArray(doc)) { + doc.forEach((item, i) => { + const path = `${curPath}/[${i}]`; + const linkHere = linkMap.get(path) || linkMap.get(`${path}/_DataLink_`); + + if (item && typeof item === "object") { + out.push({ + kind: "folder", + name: `[${i}]`, + path, + link: linkHere, + children: buildTreeFromDoc(item, linkMap, path), + }); + } else { + out.push({ + kind: "file", + name: `[${i}]`, + path, + link: linkHere, + value: item, + }); + } + }); + return out; + } + Object.keys(doc).forEach((key) => { const val = doc[key]; const path = `${curPath}/${key}`; - const link = linkMap.get(path); + const linkHere = linkMap.get(path) || linkMap.get(`${path}/_DataLink_`); - if (link) { - out.push({ kind: "file", name: key, path, link }); - return; - } - - if (val && typeof val === "object" && !Array.isArray(val)) { + if (val && typeof val === "object") { out.push({ kind: "folder", name: key, path, + link: linkHere, children: buildTreeFromDoc(val, linkMap, path), }); - return; + } else { + out.push({ kind: "file", name: key, path, link: linkHere, value: val }); } - - out.push({ kind: "file", name: key, path, value: val }); }); return out; diff --git a/src/components/Routes.tsx b/src/components/Routes.tsx index 8b16554..54c3e24 100644 --- a/src/components/Routes.tsx +++ b/src/components/Routes.tsx @@ -33,8 +33,8 @@ const Routes = () => ( {/* Dataset Details Page */} } - // element={} + // element={} + element={} /> {/* Search Page */} diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 338917d..143c749 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -180,38 +180,43 @@ const UpdatedDatasetDetailPage: React.FC = () => { ); // 2) keep current subjects-only split, return subject objects list - const subjectsOnly = useMemo(() => { - const out: any = {}; - if (!datasetDocument) return out; - Object.keys(datasetDocument).forEach((k) => { - if (/^sub-/i.test(k)) out[k] = (datasetDocument as any)[k]; - }); - return out; - }, [datasetDocument]); + // const subjectsOnly = useMemo(() => { + // const out: any = {}; + // if (!datasetDocument) return out; + // Object.keys(datasetDocument).forEach((k) => { + // if (/^sub-/i.test(k)) out[k] = (datasetDocument as any)[k]; + // }); + // return out; + // }, [datasetDocument]); // 3) link maps - const subjectLinks = useMemo( - () => externalLinks.filter((l) => /^\/sub-/i.test(l.path)), - [externalLinks] - ); - const subjectLinkMap = useMemo( - () => makeLinkMap(subjectLinks), - [subjectLinks] - ); + // const subjectLinks = useMemo( + // () => externalLinks.filter((l) => /^\/sub-/i.test(l.path)), + // [externalLinks] + // ); + // const subjectLinkMap = useMemo( + // () => makeLinkMap(subjectLinks), + // [subjectLinks] + // ); + const linkMap = useMemo(() => makeLinkMap(externalLinks), [externalLinks]); // 4) build a folder/file tree with a fallback to the WHOLE doc when no subjects exist + // const treeData = useMemo( + // () => + // hasTopLevelSubjects + // ? buildTreeFromDoc(subjectsOnly, subjectLinkMap) + // : buildTreeFromDoc(datasetDocument || {}, makeLinkMap(externalLinks)), + // [ + // hasTopLevelSubjects, + // subjectsOnly, + // subjectLinkMap, + // datasetDocument, + // externalLinks, + // ] + // ); const treeData = useMemo( - () => - hasTopLevelSubjects - ? buildTreeFromDoc(subjectsOnly, subjectLinkMap) - : buildTreeFromDoc(datasetDocument || {}, makeLinkMap(externalLinks)), - [ - hasTopLevelSubjects, - subjectsOnly, - subjectLinkMap, - datasetDocument, - externalLinks, - ] + () => buildTreeFromDoc(datasetDocument || {}, linkMap, ""), + [datasetDocument, linkMap] ); // “rest” JSON only when we actually have subjects @@ -233,17 +238,26 @@ const UpdatedDatasetDetailPage: React.FC = () => { ); // 5) header title + counts also fall back - const treeTitle = hasTopLevelSubjects ? "Subjects" : "Files"; - - const { filesCount, totalBytes } = useMemo(() => { - const group = hasTopLevelSubjects ? subjectLinks : externalLinks; + // const treeTitle = hasTopLevelSubjects ? "Subjects" : "Files"; + const treeTitle = "Files"; + const filesCount = externalLinks.length; + const totalBytes = useMemo(() => { let bytes = 0; - for (const l of group) { + for (const l of externalLinks) { const m = l.url.match(/size=(\d+)/); if (m) bytes += parseInt(m[1], 10); } - return { filesCount: group.length, totalBytes: bytes }; - }, [hasTopLevelSubjects, subjectLinks, externalLinks]); + return bytes; + }, [externalLinks]); + // const { filesCount, totalBytes } = useMemo(() => { + // const group = hasTopLevelSubjects ? subjectLinks : externalLinks; + // let bytes = 0; + // for (const l of group) { + // const m = l.url.match(/size=(\d+)/); + // if (m) bytes += parseInt(m[1], 10); + // } + // return { filesCount: group.length, totalBytes: bytes }; + // }, [hasTopLevelSubjects, subjectLinks, externalLinks]); // add spinner const [isPreviewLoading, setIsPreviewLoading] = useState(false); @@ -1160,7 +1174,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { {hasTopLevelSubjects && ( Date: Wed, 27 Aug 2025 15:16:09 -0400 Subject: [PATCH 02/14] feat: show 1-based inline labels for primitive array items for folders in dataset detail page; refs #88 --- .../DatasetDetailPage/FileTree/FileTree.tsx | 6 +-- .../FileTree/FileTreeRow.tsx | 16 ++++++-- .../DatasetDetailPage/FileTree/utils.ts | 18 +++++--- src/pages/UpdatedDatasetDetailPage.tsx | 41 +++++++++++++------ 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx index e3243d1..070e6d9 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTree.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -49,11 +49,11 @@ const FileTree: React.FC = ({ flexShrink: 0, }} > - + {/* */} {title} - + {/* Files: {filesCount}   Size: {formatSize(totalBytes)} - + */} diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index b192474..1b4b336 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -8,6 +8,7 @@ import FolderIcon from "@mui/icons-material/Folder"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import VisibilityIcon from "@mui/icons-material/Visibility"; import { Box, Button, Collapse, Typography } from "@mui/material"; +import { Colors } from "design/theme"; import React from "react"; type Props = { @@ -70,10 +71,14 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { onClick={() => setOpen((o) => !o)} > - + - {node.name} + + {node.name} + {/* ✅ Actions on folder if it carries a link (from linkHere) */} {node.link?.url && ( @@ -86,6 +91,7 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { variant="text" startIcon={} onClick={() => window.open(node.link!.url, "_blank")} + sx={{ color: Colors.purple }} > Download @@ -95,6 +101,7 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { variant="text" startIcon={} onClick={() => onPreview(node.link!.url, node.link!.index)} + sx={{ color: Colors.purple }} > Preview @@ -124,7 +131,10 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { sx={{ display: "flex", alignItems: "flex-start", gap: 1, py: 0.5, px: 1 }} > - + diff --git a/src/components/DatasetDetailPage/FileTree/utils.ts b/src/components/DatasetDetailPage/FileTree/utils.ts index d5da77b..1aed4c5 100644 --- a/src/components/DatasetDetailPage/FileTree/utils.ts +++ b/src/components/DatasetDetailPage/FileTree/utils.ts @@ -88,11 +88,18 @@ export const buildTreeFromDoc = ( doc.forEach((item, i) => { const path = `${curPath}/[${i}]`; const linkHere = linkMap.get(path) || linkMap.get(`${path}/_DataLink_`); - - if (item && typeof item === "object") { + // For primitive items, show "1: value" in the *name* + const isPrimitive = + item === null || ["string", "number", "boolean"].includes(typeof item); + const label = isPrimitive + ? `${i + 1}: ${formatLeafValue(item)}` + : String(i + 1); // objects/arrays just show "1", "2", ... + + if (item && typeof item === "object" && !isPrimitive) { out.push({ kind: "folder", - name: `[${i}]`, + // name: `[${i}]`, + name: label, path, link: linkHere, children: buildTreeFromDoc(item, linkMap, path), @@ -100,10 +107,11 @@ export const buildTreeFromDoc = ( } else { out.push({ kind: "file", - name: `[${i}]`, + // name: `[${i}]`, + name: label, path, link: linkHere, - value: item, + // value: item, }); } }); diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 143c749..89d236b 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -1170,20 +1170,19 @@ const UpdatedDatasetDetailPage: React.FC = () => { overflow: "hidden", }} > - {/* 1) SUBJECTS FILE BROWSER */} - {hasTopLevelSubjects && ( - - handlePreview(url, index, false)} - /> - - )} + {/* folder structure */} + + + handlePreview(url, index, false)} + /> + - {/* 2) EVERYTHING ELSE AS JSON */} + {/* JSON */} { p: 1, }} > + + + Raw data + + + Date: Wed, 27 Aug 2025 15:25:36 -0400 Subject: [PATCH 03/14] feat: configurable color for subject folders to improve visual clarity --- .../DatasetDetailPage/FileTree/FileTreeRow.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 1b4b336..32b280e 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -56,6 +56,7 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { // ); // } if (node.kind === "folder") { + const isSubject = /^sub-/i.test(node.name); // subject folders only return ( <> = ({ node, level, onPreview }) => { onClick={() => setOpen((o) => !o)} > - + {node.name} From 3939ec67798f4daa5a4a3b2f212215e3fb22c118 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Wed, 27 Aug 2025 15:43:27 -0400 Subject: [PATCH 04/14] feat: highlight JSON nodes to distinguish from data-linked subject folders; refs #88 --- .../DatasetDetailPage/FileTree/FileTreeRow.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 32b280e..23624f0 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -56,7 +56,8 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { // ); // } if (node.kind === "folder") { - const isSubject = /^sub-/i.test(node.name); // subject folders only + // const isSubject = /^sub-/i.test(node.name); // subject folders only + const isJson = /\.json$/i.test(node.name); // end with .json only return ( <> = ({ node, level, onPreview }) => { {node.name} @@ -149,7 +152,7 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { Date: Thu, 28 Aug 2025 17:19:53 -0400 Subject: [PATCH 05/14] docs: add comments for clearer code explanation --- src/components/DatasetDetailPage/FileTree/FileTree.tsx | 3 ++- src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx | 3 ++- src/components/DatasetDetailPage/FileTree/utils.ts | 2 +- src/pages/UpdatedDatasetDetailPage.tsx | 5 +++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx index 070e6d9..2a7de6e 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTree.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -1,3 +1,4 @@ +//renders the header (title, counts, total size) and the scrollable area. import FileTreeRow from "./FileTreeRow"; import type { TreeNode } from "./types"; import FolderIcon from "@mui/icons-material/Folder"; @@ -58,7 +59,7 @@ const FileTree: React.FC = ({ {tree.map((n) => ( - + // pass the handlePreview(onPreview = handlePreview) function to FileTreeRow ))} diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 23624f0..6612773 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -123,6 +123,7 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { {open ? : } + {/*timeout controls the duration of the expand/collapse animation*/} {node.children.map((child) => ( = ({ node, level, onPreview }) => { ); } - + // if the node is a file return ( { // key === "_id" || key === "_rev" || key.startsWith("."); export const shouldSkipKey = (_key: string) => false; -// build path -> {url, index} lookup, built from extractDataLinks function +// build path -> {url, index} lookup, built from extractDataLinks function (return { name, size, path, url, index }) // if external link objects have {path, url, index}, build a Map for the tree export const makeLinkMap = < T extends { path: string; url: string; index: number } diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 89d236b..246ff71 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -1175,10 +1175,11 @@ const UpdatedDatasetDetailPage: React.FC = () => { handlePreview(url, index, false)} + // onPreview={(url, index) => handlePreview(url, index, false)} + onPreview={handlePreview} // pass the function down to FileTree /> From 5f48c1ebb456127c8f80a38f907fe375cbefb7dd Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Fri, 29 Aug 2025 11:09:36 -0400 Subject: [PATCH 06/14] test: add mesh preview in the tree row --- .../DatasetDetailPage/FileTree/FileTree.tsx | 16 +- .../FileTree/FileTreeRow.tsx | 63 +++++- .../DatasetDetailPage/FileTree/utils.ts | 4 +- src/pages/UpdatedDatasetDetailPage.tsx | 213 ++++++++++++++---- 4 files changed, 245 insertions(+), 51 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx index 2a7de6e..2ec5697 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTree.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -10,7 +10,12 @@ type Props = { tree: TreeNode[]; filesCount: number; totalBytes: number; - onPreview: (url: string, index: number) => void; + // for preview in tree row + // onPreview: (url: string, index: number) => void; + onPreview: (src: string | any, index: number, isInternal?: boolean) => void; // ← type it + getInternalByPath?: ( + path: string + ) => { data: any; index: number } | undefined; }; const formatSize = (n: number) => { @@ -27,6 +32,7 @@ const FileTree: React.FC = ({ filesCount, totalBytes, onPreview, + getInternalByPath, }) => ( = ({ {tree.map((n) => ( - // pass the handlePreview(onPreview = handlePreview) function to FileTreeRow + // pass the handlePreview(onPreview = handlePreview) function to FileTreeRow ))} diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 6612773..432feae 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -14,11 +14,23 @@ import React from "react"; type Props = { node: TreeNode; level: number; - onPreview: (url: string, index: number) => void; + // onPreview: (url: string, index: number) => void; + // for preview in the tree row + onPreview: (src: string | any, index: number, isInternal?: boolean) => void; + getInternalByPath?: ( + path: string + ) => { data: any; index: number } | undefined; }; -const FileTreeRow: React.FC = ({ node, level, onPreview }) => { +const FileTreeRow: React.FC = ({ + node, + level, + onPreview, + getInternalByPath, +}) => { const [open, setOpen] = React.useState(false); + const internal = getInternalByPath?.(node.path); // 👈 resolve by path + const externalUrl = node.link?.url; // if (node.kind === "folder") { // return ( @@ -131,6 +143,7 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { node={child} level={level + 1} onPreview={onPreview} + getInternalByPath={getInternalByPath} /> ))} @@ -188,7 +201,49 @@ const FileTreeRow: React.FC = ({ node, level, onPreview }) => { )} - {node.link?.url && ( + {(externalUrl || internal) && ( + e.stopPropagation()} + > + {externalUrl && ( + <> + + {isPreviewable(externalUrl) && ( + + )} + + )} + + {internal && ( + + )} + {/* {node.link?.url && ( - )} + )} */} )} diff --git a/src/components/DatasetDetailPage/FileTree/utils.ts b/src/components/DatasetDetailPage/FileTree/utils.ts index 5f0f7dc..4f0c109 100644 --- a/src/components/DatasetDetailPage/FileTree/utils.ts +++ b/src/components/DatasetDetailPage/FileTree/utils.ts @@ -91,9 +91,7 @@ export const buildTreeFromDoc = ( // For primitive items, show "1: value" in the *name* const isPrimitive = item === null || ["string", "number", "boolean"].includes(typeof item); - const label = isPrimitive - ? `${i + 1}: ${formatLeafValue(item)}` - : String(i + 1); // objects/arrays just show "1", "2", ... + const label = isPrimitive ? `${i}: ${formatLeafValue(item)}` : String(i); // objects/arrays just show "1", "2", ... if (item && typeof item === "object" && !isPrimitive) { out.push({ diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 246ff71..59f4ff1 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -55,6 +55,7 @@ interface InternalDataLink { data: any; index: number; arraySize?: number[]; + path: string; // for preview in tree row } // const transformJsonForDisplay = (obj: any): any => { @@ -331,65 +332,173 @@ const UpdatedDatasetDetailPage: React.FC = () => { const internalLinks: InternalDataLink[] = []; if (obj && typeof obj === "object") { + // JMesh (MeshNode + MeshSurf/Elem) if ( obj.hasOwnProperty("MeshNode") && - (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) + (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) && + typeof obj.MeshNode?._ArrayZipData_ === "string" ) { - if ( - obj.MeshNode.hasOwnProperty("_ArrayZipData_") && - typeof obj.MeshNode["_ArrayZipData_"] === "string" - ) { - internalLinks.push({ - name: `JMesh`, - data: obj, - index: internalLinks.length, // maybe can be remove - arraySize: obj.MeshNode._ArraySize_, - }); - } - } else if (obj.hasOwnProperty("NIFTIData")) { - if ( - obj.NIFTIData.hasOwnProperty("_ArrayZipData_") && - typeof obj.NIFTIData["_ArrayZipData_"] === "string" - ) { - internalLinks.push({ - name: `JNIfTI`, - data: obj, - index: internalLinks.length, //maybe can be remove - arraySize: obj.NIFTIData._ArraySize_, - }); - } - } else if ( + internalLinks.push({ + name: "JMesh", + data: obj, + index: internalLinks.length, + arraySize: obj.MeshNode._ArraySize_, + path, // <-- add path + }); + } + // JNIfTI + else if ( + obj.hasOwnProperty("NIFTIData") && + typeof obj.NIFTIData?._ArrayZipData_ === "string" + ) { + internalLinks.push({ + name: "JNIfTI", + data: obj, + index: internalLinks.length, + arraySize: obj.NIFTIData._ArraySize_, + path, // <-- add path + }); + } + // Generic JData + else if ( obj.hasOwnProperty("_ArraySize_") && - !path.match("_EnumValue_$") + !/_EnumValue_$/.test(path) && + typeof obj["_ArrayZipData_"] === "string" ) { - if ( - obj.hasOwnProperty("_ArrayZipData_") && - typeof obj["_ArrayZipData_"] === "string" - ) { - internalLinks.push({ - name: `JData`, - data: obj, - index: internalLinks.length, // maybe can be remove - arraySize: obj._ArraySize_, - }); - } + internalLinks.push({ + name: "JData", + data: obj, + index: internalLinks.length, + arraySize: obj._ArraySize_, + path, // <-- add path + }); } else { + // Recurse with slash-separated path to match buildTreeFromDoc Object.keys(obj).forEach((key) => { if (typeof obj[key] === "object") { internalLinks.push( - ...extractInternalData( - obj[key], - `${path}.${key.replace(/\./g, "\\.")}` - ) + ...extractInternalData(obj[key], `${path}/${key}`) ); } }); } } + console.log("test==========", internalLinks); return internalLinks; }; + // const extractInternalData = (obj: any, path = ""): InternalDataLink[] => { + // // const internalLinks: InternalDataLink[] = []; + // // for preview in tree row + // const res: InternalDataLink[] = []; + // const push = ( + // name: string, + // dataObj: any, + // p: string, + // arraySize?: number[] + // ) => { + // res.push({ name, data: dataObj, index: res.length, arraySize, path: p }); + // }; + + // if (!obj || typeof obj !== "object") return res; + // if (obj && typeof obj === "object") { + // // JMesh (MeshNode + MeshSurf/Elem) + // if ( + // obj.hasOwnProperty("MeshNode") && + // (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) && + // typeof obj.MeshNode?._ArrayZipData_ === "string" + // ) { + // push("JMesh", obj, `${path}/MeshNode`, obj.MeshNode._ArraySize_); + // } + // // JNIfTI + // else if ( + // obj.hasOwnProperty("NIFTIData") && + // typeof obj.NIFTIData?._ArrayZipData_ === "string" + // ) { + // push("JNIfTI", obj, `${path}/NIFTIData`, obj.NIFTIData._ArraySize_); + // } + // // Generic JData + // else if ( + // obj.hasOwnProperty("_ArrayZipData_") && + // typeof obj._ArrayZipData_ === "string" && + // !/_EnumValue_$/.test(path) + // ) { + // push("JData", obj, path, obj._ArraySize_); + // } + + // // Recurse into children + // Object.keys(obj).forEach((key) => { + // const v = obj[key]; + // if (v && typeof v === "object") { + // // IMPORTANT: use "/" to match buildTreeFromDoc paths + // res.push(...extractInternalData(v, `${path}/${key}`)); + // } + // }); + // } + + // return res; + + // // if (obj && typeof obj === "object") { + // // if ( + // // obj.hasOwnProperty("MeshNode") && + // // (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) + // // ) { + // // if ( + // // obj.MeshNode.hasOwnProperty("_ArrayZipData_") && + // // typeof obj.MeshNode["_ArrayZipData_"] === "string" + // // ) { + // // internalLinks.push({ + // // name: `JMesh`, + // // data: obj, + // // index: internalLinks.length, // maybe can be remove + // // arraySize: obj.MeshNode._ArraySize_, + // // }); + // // } + // // } else if (obj.hasOwnProperty("NIFTIData")) { + // // if ( + // // obj.NIFTIData.hasOwnProperty("_ArrayZipData_") && + // // typeof obj.NIFTIData["_ArrayZipData_"] === "string" + // // ) { + // // internalLinks.push({ + // // name: `JNIfTI`, + // // data: obj, + // // index: internalLinks.length, //maybe can be remove + // // arraySize: obj.NIFTIData._ArraySize_, + // // }); + // // } + // // } else if ( + // // obj.hasOwnProperty("_ArraySize_") && + // // !path.match("_EnumValue_$") + // // ) { + // // if ( + // // obj.hasOwnProperty("_ArrayZipData_") && + // // typeof obj["_ArrayZipData_"] === "string" + // // ) { + // // internalLinks.push({ + // // name: `JData`, + // // data: obj, + // // index: internalLinks.length, // maybe can be remove + // // arraySize: obj._ArraySize_, + // // }); + // // } + // // } else { + // // Object.keys(obj).forEach((key) => { + // // if (typeof obj[key] === "object") { + // // internalLinks.push( + // // ...extractInternalData( + // // obj[key], + // // `${path}.${key.replace(/\./g, "\\.")}` + // // ) + // // ); + // // } + // // }); + // // } + // // } + + // // return internalLinks; + // }; + useEffect(() => { const fetchData = async () => { if (dbName && docId) { @@ -750,6 +859,20 @@ const UpdatedDatasetDetailPage: React.FC = () => { } }; + // for preview in tree row + + const internalMap = React.useMemo(() => { + const m = new Map(); + for (const it of internalLinks) + m.set(it.path, { data: it.data, index: it.index }); + return m; + }, [internalLinks]); + + const getInternalByPath = React.useCallback( + (path: string) => internalMap.get(path), + [internalMap] + ); + const handleClosePreview = () => { console.log("🛑 Closing preview modal."); setPreviewOpen(false); @@ -1179,7 +1302,13 @@ const UpdatedDatasetDetailPage: React.FC = () => { filesCount={filesCount} totalBytes={totalBytes} // onPreview={(url, index) => handlePreview(url, index, false)} - onPreview={handlePreview} // pass the function down to FileTree + // onPreview={handlePreview} // pass the function down to FileTree + onPreview={( + src: string | any, + index: number, + isInternal?: boolean + ) => handlePreview(src, index, !!isInternal)} + getInternalByPath={getInternalByPath} /> From 443d38b03bc3a9098a1173f5df66ba6bbe8d29c2 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Fri, 29 Aug 2025 17:00:38 -0400 Subject: [PATCH 07/14] feat: add preview for embedded internal data in file tree; refs #88 --- .../DatasetDetailPage/FileTree/FileTree.tsx | 7 +- .../FileTree/FileTreeRow.tsx | 30 +- src/pages/UpdatedDatasetDetailPage.tsx | 379 ++++-------------- 3 files changed, 109 insertions(+), 307 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx index 2ec5697..e028596 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTree.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -11,11 +11,8 @@ type Props = { filesCount: number; totalBytes: number; // for preview in tree row - // onPreview: (url: string, index: number) => void; - onPreview: (src: string | any, index: number, isInternal?: boolean) => void; // ← type it - getInternalByPath?: ( - path: string - ) => { data: any; index: number } | undefined; + onPreview: (src: string | any, index: number, isInternal?: boolean) => void; + getInternalByPath: (path: string) => { data: any; index: number } | undefined; }; const formatSize = (n: number) => { diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 432feae..d1673da 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -14,12 +14,10 @@ import React from "react"; type Props = { node: TreeNode; level: number; - // onPreview: (url: string, index: number) => void; - // for preview in the tree row + + // src is either an external URL(string) or the internal object onPreview: (src: string | any, index: number, isInternal?: boolean) => void; - getInternalByPath?: ( - path: string - ) => { data: any; index: number } | undefined; + getInternalByPath: (path: string) => { data: any; index: number } | undefined; }; const FileTreeRow: React.FC = ({ @@ -29,7 +27,9 @@ const FileTreeRow: React.FC = ({ getInternalByPath, }) => { const [open, setOpen] = React.useState(false); - const internal = getInternalByPath?.(node.path); // 👈 resolve by path + // const internal = getInternalByPath?.(node.path); + // const internal = getInternalByPath ? getInternalByPath(node.path) : undefined; + const internal = getInternalByPath(node.path); const externalUrl = node.link?.url; // if (node.kind === "folder") { @@ -132,6 +132,24 @@ const FileTreeRow: React.FC = ({ )} + {/* internal preview action for folders */} + {internal && ( + e.stopPropagation()} + > + + + )} + {open ? : } diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 59f4ff1..7f6ea56 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -1,13 +1,10 @@ import PreviewModal from "../components/PreviewModal"; import CloudDownloadIcon from "@mui/icons-material/CloudDownload"; import DescriptionIcon from "@mui/icons-material/Description"; -import DownloadIcon from "@mui/icons-material/Download"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; import FolderIcon from "@mui/icons-material/Folder"; import HomeIcon from "@mui/icons-material/Home"; -// import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; -// import VisibilityIcon from "@mui/icons-material/Visibility"; import { Box, Typography, @@ -15,16 +12,10 @@ import { Backdrop, Alert, Button, - Card, - CardContent, Collapse, } from "@mui/material"; // new import import FileTree from "components/DatasetDetailPage/FileTree/FileTree"; -import type { - TreeNode, - LinkMeta, -} from "components/DatasetDetailPage/FileTree/types"; import { buildTreeFromDoc, makeLinkMap, @@ -231,8 +222,8 @@ const UpdatedDatasetDetailPage: React.FC = () => { }, [datasetDocument, hasTopLevelSubjects]); // JSON panel should always render: - // - if we have subjects -> show "rest" (everything except sub-*) - // - if we don't have subjects -> show the whole document + // - if we have subjects -> JSON show "rest" (everything except sub-*) + // - if we don't have subjects -> JSON show the whole document const jsonPanelData = useMemo( () => (hasTopLevelSubjects ? rest : datasetDocument || {}), [hasTopLevelSubjects, rest, datasetDocument] @@ -262,7 +253,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { // add spinner const [isPreviewLoading, setIsPreviewLoading] = useState(false); - const [readyPreviewData, setReadyPreviewData] = useState(null); const formatSize = (sizeInBytes: number): string => { if (sizeInBytes < 1024) { @@ -317,7 +307,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { }; traverse(obj, path); - // return links; const seenUrls = new Set(); const uniqueLinks = links.filter((link) => { if (seenUrls.has(link.url)) return false; @@ -332,50 +321,64 @@ const UpdatedDatasetDetailPage: React.FC = () => { const internalLinks: InternalDataLink[] = []; if (obj && typeof obj === "object") { - // JMesh (MeshNode + MeshSurf/Elem) - if ( - obj.hasOwnProperty("MeshNode") && - (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) && - typeof obj.MeshNode?._ArrayZipData_ === "string" - ) { - internalLinks.push({ - name: "JMesh", - data: obj, - index: internalLinks.length, - arraySize: obj.MeshNode._ArraySize_, - path, // <-- add path + // Handle arrays so paths match the tree (/[0], /[1], …) + if (Array.isArray(obj)) { + obj.forEach((item, i) => { + internalLinks.push(...extractInternalData(item, `${path}/[${i}]`)); }); + return internalLinks; } - // JNIfTI - else if ( - obj.hasOwnProperty("NIFTIData") && - typeof obj.NIFTIData?._ArrayZipData_ === "string" + + if ( + obj.hasOwnProperty("MeshNode") && + (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) ) { - internalLinks.push({ - name: "JNIfTI", - data: obj, - index: internalLinks.length, - arraySize: obj.NIFTIData._ArraySize_, - path, // <-- add path - }); - } - // Generic JData - else if ( + if ( + obj.MeshNode?.hasOwnProperty("_ArrayZipData_") && + typeof obj.MeshNode["_ArrayZipData_"] === "string" + ) { + console.log("path", path); + internalLinks.push({ + name: "JMesh", + data: obj, + index: internalLinks.length, + arraySize: obj.MeshNode._ArraySize_, + path: `${path}/MeshNode`, // attach to the MeshNode row in the tree + }); + } + } else if (obj.hasOwnProperty("NIFTIData")) { + if ( + obj.NIFTIData?.hasOwnProperty("_ArrayZipData_") && + typeof obj.NIFTIData["_ArrayZipData_"] === "string" + ) { + internalLinks.push({ + name: "JNIfTI", + data: obj, + index: internalLinks.length, + arraySize: obj.NIFTIData._ArraySize_, + path: `${path}/NIFTIData`, // attach to the NIFTIData row + }); + } + } else if ( obj.hasOwnProperty("_ArraySize_") && - !/_EnumValue_$/.test(path) && - typeof obj["_ArrayZipData_"] === "string" + !/_EnumValue_$/.test(path) ) { - internalLinks.push({ - name: "JData", - data: obj, - index: internalLinks.length, - arraySize: obj._ArraySize_, - path, // <-- add path - }); + if ( + obj.hasOwnProperty("_ArrayZipData_") && + typeof obj["_ArrayZipData_"] === "string" + ) { + internalLinks.push({ + name: "JData", + data: obj, + index: internalLinks.length, + arraySize: obj._ArraySize_, + path, // attach to the current node + }); + } } else { - // Recurse with slash-separated path to match buildTreeFromDoc Object.keys(obj).forEach((key) => { if (typeof obj[key] === "object") { + // use slash paths to match buildTreeFromDoc internalLinks.push( ...extractInternalData(obj[key], `${path}/${key}`) ); @@ -384,121 +387,9 @@ const UpdatedDatasetDetailPage: React.FC = () => { } } - console.log("test==========", internalLinks); return internalLinks; }; - // const extractInternalData = (obj: any, path = ""): InternalDataLink[] => { - // // const internalLinks: InternalDataLink[] = []; - // // for preview in tree row - // const res: InternalDataLink[] = []; - // const push = ( - // name: string, - // dataObj: any, - // p: string, - // arraySize?: number[] - // ) => { - // res.push({ name, data: dataObj, index: res.length, arraySize, path: p }); - // }; - - // if (!obj || typeof obj !== "object") return res; - // if (obj && typeof obj === "object") { - // // JMesh (MeshNode + MeshSurf/Elem) - // if ( - // obj.hasOwnProperty("MeshNode") && - // (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) && - // typeof obj.MeshNode?._ArrayZipData_ === "string" - // ) { - // push("JMesh", obj, `${path}/MeshNode`, obj.MeshNode._ArraySize_); - // } - // // JNIfTI - // else if ( - // obj.hasOwnProperty("NIFTIData") && - // typeof obj.NIFTIData?._ArrayZipData_ === "string" - // ) { - // push("JNIfTI", obj, `${path}/NIFTIData`, obj.NIFTIData._ArraySize_); - // } - // // Generic JData - // else if ( - // obj.hasOwnProperty("_ArrayZipData_") && - // typeof obj._ArrayZipData_ === "string" && - // !/_EnumValue_$/.test(path) - // ) { - // push("JData", obj, path, obj._ArraySize_); - // } - - // // Recurse into children - // Object.keys(obj).forEach((key) => { - // const v = obj[key]; - // if (v && typeof v === "object") { - // // IMPORTANT: use "/" to match buildTreeFromDoc paths - // res.push(...extractInternalData(v, `${path}/${key}`)); - // } - // }); - // } - - // return res; - - // // if (obj && typeof obj === "object") { - // // if ( - // // obj.hasOwnProperty("MeshNode") && - // // (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem")) - // // ) { - // // if ( - // // obj.MeshNode.hasOwnProperty("_ArrayZipData_") && - // // typeof obj.MeshNode["_ArrayZipData_"] === "string" - // // ) { - // // internalLinks.push({ - // // name: `JMesh`, - // // data: obj, - // // index: internalLinks.length, // maybe can be remove - // // arraySize: obj.MeshNode._ArraySize_, - // // }); - // // } - // // } else if (obj.hasOwnProperty("NIFTIData")) { - // // if ( - // // obj.NIFTIData.hasOwnProperty("_ArrayZipData_") && - // // typeof obj.NIFTIData["_ArrayZipData_"] === "string" - // // ) { - // // internalLinks.push({ - // // name: `JNIfTI`, - // // data: obj, - // // index: internalLinks.length, //maybe can be remove - // // arraySize: obj.NIFTIData._ArraySize_, - // // }); - // // } - // // } else if ( - // // obj.hasOwnProperty("_ArraySize_") && - // // !path.match("_EnumValue_$") - // // ) { - // // if ( - // // obj.hasOwnProperty("_ArrayZipData_") && - // // typeof obj["_ArrayZipData_"] === "string" - // // ) { - // // internalLinks.push({ - // // name: `JData`, - // // data: obj, - // // index: internalLinks.length, // maybe can be remove - // // arraySize: obj._ArraySize_, - // // }); - // // } - // // } else { - // // Object.keys(obj).forEach((key) => { - // // if (typeof obj[key] === "object") { - // // internalLinks.push( - // // ...extractInternalData( - // // obj[key], - // // `${path}.${key.replace(/\./g, "\\.")}` - // // ) - // // ); - // // } - // // }); - // // } - // // } - - // // return internalLinks; - // }; - useEffect(() => { const fetchData = async () => { if (dbName && docId) { @@ -528,8 +419,8 @@ const UpdatedDatasetDetailPage: React.FC = () => { }) ); - console.log(" Extracted external links:", links); - console.log(" Extracted internal data:", internalData); + // console.log(" Extracted external links:", links); + // console.log(" Extracted internal data:", internalData); setExternalLinks(links); setInternalLinks(internalData); @@ -548,7 +439,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { let totalSize = 0; - // 1️⃣ Sum external link sizes (from URL like ...?size=12345678) + // 1. Sum external link sizes (from URL like ...?size=12345678) links.forEach((link) => { const sizeMatch = link.url.match(/size=(\d+)/); if (sizeMatch) { @@ -556,7 +447,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { } }); - // 2️⃣ Estimate internal size from _ArraySize_ (assume Float32 = 4 bytes) + // 2. Estimate internal size from _ArraySize_ (assume Float32 = 4 bytes) internalData.forEach((link) => { if (link.arraySize && Array.isArray(link.arraySize)) { const count = link.arraySize.reduce((acc, val) => acc * val, 1); @@ -564,37 +455,17 @@ const UpdatedDatasetDetailPage: React.FC = () => { } }); - // setTotalFileSize(totalSize); - - // const minifiedBlob = new Blob([JSON.stringify(datasetDocument)], { - // type: "application/json", - // }); - // setJsonSize(minifiedBlob.size); - const blob = new Blob([JSON.stringify(datasetDocument, null, 2)], { type: "application/json", }); setJsonSize(blob.size); - // // ✅ Construct download script dynamically + // Construct download script dynamically let script = `curl -L --create-dirs "https://neurojson.io:7777/${dbName}/${docId}" -o "${docId}.json"\n`; links.forEach((link) => { const url = link.url; - // console.log("url", url); const match = url.match(/file=([^&]+)/); - // console.log("match", match); - // console.log("match[1]", match?.[1]); - // try { - // const decoded = match?.[1] ? decodeURIComponent(match[1]) : "N/A"; - // console.log("decode", decoded); - // } catch (err) { - // console.warn("⚠️ Failed to decode match[1]:", match?.[1], err); - // } - - // const filename = match - // ? decodeURIComponent(match[1]) - // : `file-${link.index}`; const filename = match ? (() => { @@ -605,14 +476,13 @@ const UpdatedDatasetDetailPage: React.FC = () => { } })() : `file-${link.index}`; - // console.log("filename", filename); const outputPath = `$HOME/.neurojson/io/${dbName}/${docId}/${filename}`; script += `curl -L --create-dirs "${url}" -o "${outputPath}"\n`; }); setDownloadScript(script); - // ✅ Calculate and set script size + // Calculate and set script size const scriptBlob = new Blob([script], { type: "text/plain" }); setDownloadScriptSize(scriptBlob.size); } @@ -693,27 +563,16 @@ const UpdatedDatasetDetailPage: React.FC = () => { setPreviewDataKey(dataOrUrl); setPreviewIsInternal(isInternal); - // setPreviewOpen(false); // IMPORTANT: Keep modal closed for now - - // This callback will be triggered by the legacy script when data is ready - // (window as any).__onPreviewReady = (decodedData: any) => { - // console.log("✅ Data is ready! Opening modal."); - // setReadyPreviewData(decodedData); // Store the final data for the modal - // setIsPreviewLoading(false); // Hide the spinner - // setPreviewOpen(true); // NOW it's time to open the modal - // }; - const is2DPreviewCandidate = (obj: any): boolean => { if (typeof window !== "undefined" && (window as any).__previewType) { - // console.log("preview type: 2d"); return (window as any).__previewType === "2d"; } // if (window.__previewType) { // console.log("work~~~~~~~"); // return window.__previewType === "2d"; // } - console.log("is 2d preview candidate !== 2d"); - console.log("obj", obj); + // console.log("is 2d preview candidate !== 2d"); + // console.log("obj", obj); // if (typeof obj === "string" && obj.includes("db=optics-at-martinos")) { // return false; // } @@ -723,14 +582,13 @@ const UpdatedDatasetDetailPage: React.FC = () => { if (!obj || typeof obj !== "object") { return false; } - console.log("=======after first condition"); + if (!obj._ArrayType_ || !obj._ArraySize_ || !obj._ArrayZipData_) { - console.log("inside second condition"); return false; } const dim = obj._ArraySize_; - console.log("array.isarray(dim)", Array.isArray(dim)); - console.log("dim.length", dim.length === 1 || dim.length === 2); + // console.log("array.isarray(dim)", Array.isArray(dim)); + // console.log("dim.length", dim.length === 1 || dim.length === 2); return ( Array.isArray(dim) && @@ -753,7 +611,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { const extractFileName = (url: string): string => { const match = url.match(/file=([^&]+)/); - // return match ? decodeURIComponent(match[1]) : url; if (match) { // Strip any trailing query parameters const raw = decodeURIComponent(match[1]); @@ -771,32 +628,12 @@ const UpdatedDatasetDetailPage: React.FC = () => { const fileName = typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : ""; - console.log("🔍 Extracted fileName:", fileName); + // console.log("🔍 Extracted fileName:", fileName); const isPreviewableFile = (fileName: string): boolean => { return /\.(nii\.gz|jdt|jdb|bmsh|jmsh|bnii)$/i.test(fileName); }; - console.log("🧪 isPreviewableFile:", isPreviewableFile(fileName)); - - // test for add spinner - // if (isInternal) { - // if (is2DPreviewCandidate(dataOrUrl)) { - // // inline 2D - // window.dopreview(dataOrUrl, idx, true); - // } else { - // // 3D - // window.previewdata(dataOrUrl, idx, true, []); - // } - // } else { - // // external - // window.previewdataurl(dataOrUrl, idx); - // } - - // for test so command out the below - // setPreviewIndex(idx); - // setPreviewDataKey(dataOrUrl); - // setPreviewIsInternal(isInternal); - // setPreviewOpen(true); + // console.log("🧪 isPreviewableFile:", isPreviewableFile(fileName)); if (isInternal) { try { @@ -814,47 +651,34 @@ const UpdatedDatasetDetailPage: React.FC = () => { console.log("📊 2D data → rendering inline with dopreview()"); (window as any).dopreview(dataOrUrl, idx, true); const panel = document.getElementById("chartpanel"); - if (panel) panel.style.display = "block"; // 🔓 Show it! - setPreviewOpen(false); // ⛔ Don't open modal - // setPreviewLoading(false); // stop spinner + if (panel) panel.style.display = "block"; // Show it! + setPreviewOpen(false); // Don't open modal } else { - console.log("🎬 3D data → rendering in modal"); + // console.log("🎬 3D data → rendering in modal"); (window as any).previewdata(dataOrUrl, idx, true, []); - // add spinner - // setPreviewDataKey(dataOrUrl); - // setPreviewOpen(true); - // setPreviewIsInternal(true); } } catch (err) { console.error("❌ Error in internal preview:", err); - // setPreviewLoading(false); // add spinner } } else { - // external - // if (/\.(nii\.gz|jdt|jdb|bmsh|jmsh|bnii)$/i.test(dataOrUrl)) { const fileName = typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : ""; if (isPreviewableFile(fileName)) { (window as any).previewdataurl(dataOrUrl, idx); const is2D = is2DPreviewCandidate(dataOrUrl); const panel = document.getElementById("chartpanel"); - console.log("is2D", is2D); - console.log("panel", panel); + // console.log("is2D", is2D); + // console.log("panel", panel); if (is2D) { - console.log("📊 2D data → rendering inline with dopreview()"); - if (panel) panel.style.display = "block"; // 🔓 Show it! - setPreviewOpen(false); // ⛔ Don't open modal + // console.log("📊 2D data → rendering inline with dopreview()"); + if (panel) panel.style.display = "block"; // Show it! + setPreviewOpen(false); // Don't open modal } else { - if (panel) panel.style.display = "none"; // 🔒 Hide chart panel on 3D external + if (panel) panel.style.display = "none"; // Hide chart panel on 3D external } - //add spinner - // setPreviewDataKey(dataOrUrl); - // setPreviewOpen(true); - // setPreviewIsInternal(false); } else { console.warn("⚠️ Unsupported file format for preview:", dataOrUrl); - // setPreviewLoading(false); // add spinner } } }; @@ -868,13 +692,9 @@ const UpdatedDatasetDetailPage: React.FC = () => { return m; }, [internalLinks]); - const getInternalByPath = React.useCallback( - (path: string) => internalMap.get(path), - [internalMap] - ); + const getInternalByPath = (path: string) => internalMap.get(path); const handleClosePreview = () => { - console.log("🛑 Closing preview modal."); setPreviewOpen(false); setPreviewDataKey(null); @@ -1019,30 +839,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { return ( <> - {/* 🔧 Inline CSS for string formatting */} - {/* */} @@ -1204,7 +998,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { > {/* Script to Download All Files ({downloadScript.length} Bytes) */} Script to Download All Files ({formatSize(downloadScriptSize)}) - {/* (links: {externalLinks.length}) */} {externalLinks.length > 0 && ` (links: ${externalLinks.length}, total: ${formatSize( totalFileSize @@ -1301,13 +1094,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { tree={treeData} filesCount={filesCount} totalBytes={totalBytes} - // onPreview={(url, index) => handlePreview(url, index, false)} - // onPreview={handlePreview} // pass the function down to FileTree - onPreview={( - src: string | any, - index: number, - isInternal?: boolean - ) => handlePreview(src, index, !!isInternal)} + onPreview={handlePreview} // pass the function down to FileTree getInternalByPath={getInternalByPath} /> @@ -1393,7 +1180,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { // overflowY: "auto", }} > - {/* ✅ Collapsible header */} + {/* Collapsible header */} { - {/* ✅ Scrollable area */} + {/* Scrollable area */} { // overflowY: "auto", }} > - {/* ✅ Header with toggle */} + {/* Header with toggle */} Date: Wed, 3 Sep 2025 09:56:05 -0400 Subject: [PATCH 08/14] chore: comment out all console.log statements for clean up --- .../DatasetDetailPage/LoadDatasetTabs.tsx | 8 +- src/pages/DatabasePage.tsx | 1 - src/pages/DatasetDetailPage.tsx | 2 +- src/pages/UpdatedDatasetDetailPage.tsx | 4 +- src/utils/preview.js | 131 ++++++++---------- 5 files changed, 68 insertions(+), 78 deletions(-) diff --git a/src/components/DatasetDetailPage/LoadDatasetTabs.tsx b/src/components/DatasetDetailPage/LoadDatasetTabs.tsx index 17c00e3..b480155 100644 --- a/src/components/DatasetDetailPage/LoadDatasetTabs.tsx +++ b/src/components/DatasetDetailPage/LoadDatasetTabs.tsx @@ -91,10 +91,10 @@ const LoadDatasetTabs: React.FC = ({ const datasetName = datasetDesc?.Name?.includes(" - ") ? datasetDesc.Name.split(" - ")[1] : datasetDesc?.Name || datasetDocument?._id || docname; - console.log("datasetName", datasetName); - console.log("dbname", dbname); - console.log("pagename", pagename); - console.log("onekey", onekey); + // console.log("datasetName", datasetName); + // console.log("dbname", dbname); + // console.log("pagename", pagename); + // console.log("onekey", onekey); // const datasetUrl = datasetName // ? `${serverUrl}${dbname}/${encodeURIComponent(datasetName)}/` // : `${serverUrl}${dbname}/`; diff --git a/src/pages/DatabasePage.tsx b/src/pages/DatabasePage.tsx index 859f2ba..a266594 100644 --- a/src/pages/DatabasePage.tsx +++ b/src/pages/DatabasePage.tsx @@ -12,7 +12,6 @@ const DatabasePage: React.FC = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const { registry } = useAppSelector(NeurojsonSelector); - console.log("registry", registry); useEffect(() => { dispatch(fetchRegistry()); diff --git a/src/pages/DatasetDetailPage.tsx b/src/pages/DatasetDetailPage.tsx index 7b47878..0999906 100644 --- a/src/pages/DatasetDetailPage.tsx +++ b/src/pages/DatasetDetailPage.tsx @@ -364,7 +364,7 @@ const DatasetDetailPage: React.FC = () => { // }); // setJsonSize(minifiedBlob.size); - const blob = new Blob([JSON.stringify(datasetDocument, null, 2)], { + const blob = new Blob([JSON.stringify(datasetDocument)], { type: "application/json", }); setJsonSize(blob.size); diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 7f6ea56..878a882 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -403,7 +403,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { useEffect(() => { if (datasetDocument) { // Extract External Data & Assign `index` - console.log("datasetDocument", datasetDocument); + // console.log("datasetDocument", datasetDocument); const links = extractDataLinks(datasetDocument, "").map( (link, index) => ({ ...link, @@ -828,7 +828,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { ); } - console.log("datasetDocument", datasetDocument); + // console.log("datasetDocument", datasetDocument); const onekey = datasetDocument ? datasetDocument.hasOwnProperty("README") ? "README" diff --git a/src/utils/preview.js b/src/utils/preview.js index 3a44b5e..9409351 100644 --- a/src/utils/preview.js +++ b/src/utils/preview.js @@ -98,15 +98,12 @@ function destroyPreview() { function drawpreview(cfg) { // xyzscale = undefined; // add for test - console.log("🛠️ Rendering in drawpreview()"); - console.log("🟢 Data received:", cfg); + // console.log("🟢 Data received:", cfg); initcanvas(); scene.remove.apply(scene, scene.children); if (cfg.hasOwnProperty("Shapes")) { - console.log("📦 Drawing Shapes..."); if (cfg.Shapes instanceof nj.NdArray) { - console.log("🟢 Detected NumJS Array. Calling drawvolume()"); if (isWebGL2Available()) { let box = { Grid: { Size: cfg.Shapes.shape } }; drawshape(box, 0); @@ -136,12 +133,12 @@ function drawpreview(cfg) { } else { if (cfg.hasOwnProperty("MeshNode") && cfg.hasOwnProperty("MeshSurf")) { if (cfg.MeshNode instanceof nj.NdArray) { - console.log("✅ Rendering MeshNode & MeshSurf!"); - console.log("🔍 Rendering MeshNode & MeshSurf!"); - console.log("📌 MeshNode Data:", cfg.MeshNode); - console.log("📌 MeshSurf Data:", cfg.MeshSurf); - console.log("📌 MeshNode Shape:", cfg.MeshNode.shape); - console.log("📌 MeshSurf Shape:", cfg.MeshSurf.shape); + // console.log("✅ Rendering MeshNode & MeshSurf!"); + // console.log("🔍 Rendering MeshNode & MeshSurf!"); + // console.log("📌 MeshNode Data:", cfg.MeshNode); + // console.log("📌 MeshSurf Data:", cfg.MeshSurf); + // console.log("📌 MeshNode Shape:", cfg.MeshNode.shape); + // console.log("📌 MeshSurf Shape:", cfg.MeshSurf.shape); drawsurf(cfg.MeshNode, cfg.MeshSurf); } else { if (cfg.MeshNode.hasOwnProperty("_ArraySize_")) { @@ -149,13 +146,13 @@ function drawpreview(cfg) { let surfsize = cfg.MeshSurf._ArraySize_; let jd = new jdata(cfg, {}); cfg = jd.decode().data; - console.log("🔄 Converting MeshNode & MeshSurf to ndarrays..."); + // console.log("🔄 Converting MeshNode & MeshSurf to ndarrays..."); drawsurf( nj.array(cfg.MeshNode, "float32"), nj.array(cfg.MeshSurf, "uint32") ); } else { - console.log("🔄 Converting MeshNode & MeshSurf from plain arrays..."); + // console.log("🔄 Converting MeshNode & MeshSurf from plain arrays..."); drawsurf( nj .array(Array.from(cfg.MeshNode), "float32") @@ -241,13 +238,13 @@ function drawpreview(cfg) { } function previewdata(key, idx, isinternal, hastime) { - console.log("📦 previewdata() input:", { - key, - idx, - isinternal, - intdata: window.intdata, - }); - console.log("key in previewdata", key); + // console.log("📦 previewdata() input:", { + // key, + // idx, + // isinternal, + // intdata: window.intdata, + // }); + // console.log("key in previewdata", key); if (!hasthreejs) { $.when( $.getScript("https://mcx.space/cloud/js/OrbitControls.js"), @@ -257,21 +254,19 @@ function previewdata(key, idx, isinternal, hastime) { ).done(function () { hasthreejs = true; dopreview(key, idx, isinternal, hastime); - console.log("into the previewdata function if"); }); } else { dopreview(key, idx, isinternal, hastime); - console.log("into the previewdata function else"); } } function dopreview(key, idx, isinternal, hastime) { - console.log("🧪 dopreview input:", { - key, - idx, - isinternal, - intdata: intdata[idx], - }); + // console.log("🧪 dopreview input:", { + // key, + // idx, + // isinternal, + // intdata: intdata[idx], + // }); let ndim = 0; if (hastime === undefined) hastime = []; @@ -281,7 +276,7 @@ function dopreview(key, idx, isinternal, hastime) { if (window.intdata && window.intdata[idx] && window.intdata[idx][2]) { dataroot = window.intdata[idx][2]; } else { - console.error("❌ Internal data not ready for index", idx); + // console.error("❌ Internal data not ready for index", idx); return; } } else { @@ -291,15 +286,15 @@ function dopreview(key, idx, isinternal, hastime) { if (window.extdata && window.extdata[idx] && window.extdata[idx][2]) { if (typeof key === "object") { dataroot = key; - console.log("if key is object", typeof key); + // console.log("if key is object", typeof key); } else { dataroot = window.extdata[idx][2]; - console.log("type of key", typeof key); + // console.log("type of key", typeof key); } // dataroot = key; - console.log("into dopreview external data's dataroot", dataroot); + // console.log("into dopreview external data's dataroot", dataroot); } else { console.error("❌ External data not ready for index", idx); return; @@ -319,9 +314,9 @@ function dopreview(key, idx, isinternal, hastime) { dataroot = window.extdata[idx][2]; } } else if (dataroot instanceof nj.NdArray) { - console.log("dataroot before ndim", dataroot); + // console.log("dataroot before ndim", dataroot); ndim = dataroot.shape.length; - console.log("ndim", ndim); + // console.log("ndim", ndim); } if (ndim < 3 && ndim > 0) { @@ -346,16 +341,16 @@ function dopreview(key, idx, isinternal, hastime) { '

Data preview

×
' ); if (dataroot instanceof nj.NdArray) { - console.log("dataroot", dataroot); + // console.log("dataroot", dataroot); if (dataroot.shape[0] > dataroot.shape[1]) dataroot = dataroot.transpose(); - console.log("is nj.NdArray:", dataroot instanceof nj.NdArray); - console.log("dtype:", dataroot.dtype); - console.log("shape:", dataroot.shape); - console.log("size:", dataroot.size); + // console.log("is nj.NdArray:", dataroot instanceof nj.NdArray); + // console.log("dtype:", dataroot.dtype); + // console.log("shape:", dataroot.shape); + // console.log("size:", dataroot.size); let plotdata = dataroot.tolist(); - console.log("plotdata", plotdata); + // console.log("plotdata", plotdata); if (hastime.length == 0) { if (plotdata[0] instanceof Array) plotdata.unshift([...Array(plotdata[0].length).keys()]); @@ -383,14 +378,12 @@ function dopreview(key, idx, isinternal, hastime) { : hastime[i]; } let u = new uPlot(opts, plotdata, document.getElementById("plotchart")); - console.log("first u", u); } else { let u = new uPlot( opts, [[...Array(dataroot.length).keys()], dataroot], document.getElementById("plotchart") ); - console.log("second u", u); } // add spinner // --- NEW LOGIC for 2D plot --- @@ -542,7 +535,7 @@ function drawshape(shape, index) { wireframe: true, transparent: true, }); - console.log("📌 Mesh Material:", material); + // console.log("📌 Mesh Material:", material); obj = new THREE.Mesh(geometry, material); obj.position.set(shape.Sphere.O[0], shape.Sphere.O[1], shape.Sphere.O[2]); boundingbox.add(obj); @@ -576,7 +569,7 @@ function drawshape(shape, index) { wireframe: true, transparent: false, }); - console.log("📌 Mesh Material:", material); + // console.log("📌 Mesh Material:", material); obj = new THREE.Mesh(geometry, material); boundingbox.add(obj); break; @@ -591,11 +584,11 @@ function mulberry32(a) { } function drawsurf(node, tri) { - console.log("🔷 Inside drawsurf()"); - console.log("📌 Received MeshNode:", node); - console.log("📌 Received MeshSurf:", tri); - console.log("📌 MeshNode Shape:", node.shape); - console.log("📌 MeshSurf Shape:", tri.shape); + // console.log("🔷 Inside drawsurf()"); + // console.log("📌 Received MeshNode:", node); + // console.log("📌 Received MeshSurf:", tri); + // console.log("📌 MeshNode Shape:", node.shape); + // console.log("📌 MeshSurf Shape:", tri.shape); $("#mip-radio-button,#iso-radio-button,#interp-radio-button").prop( "disabled", true @@ -621,14 +614,14 @@ function drawsurf(node, tri) { side: THREE.DoubleSide, wireframe: false, }); - console.log("📌 Mesh Material:", material); + // console.log("📌 Mesh Material:", material); lastvolume = new THREE.Mesh(geometry, material); scene.add(lastvolume); - console.log("🟢 Mesh Added to Scene:", lastvolume); - console.log("📌 Mesh Position:", lastvolume.position); - console.log("📌 Mesh Bounding Box:", lastvolume.geometry.boundingBox); - console.log("📌 Mesh Bounding Sphere:", lastvolume.geometry.boundingSphere); + // console.log("🟢 Mesh Added to Scene:", lastvolume); + // console.log("📌 Mesh Position:", lastvolume.position); + // console.log("📌 Mesh Bounding Box:", lastvolume.geometry.boundingBox); + // console.log("📌 Mesh Bounding Sphere:", lastvolume.geometry.boundingSphere); var geo = new THREE.WireframeGeometry(lastvolume.geometry); var mat = new THREE.LineBasicMaterial({ color: 0x666666 }); @@ -656,11 +649,11 @@ function drawsurf(node, tri) { boundingbox.add(lastvolume); - console.log("👁 Camera Pos:", camera.position); - console.log("👁 Controls Target:", controls.target); - console.log("👁 Mesh Pos:", lastvolume.position); - console.log("👁 Bounding Sphere:", geometry.boundingSphere); - console.log("👁 Canvas size:", canvas.width(), canvas.height()); + // console.log("👁 Camera Pos:", camera.position); + // console.log("👁 Controls Target:", controls.target); + // console.log("👁 Mesh Pos:", lastvolume.position); + // console.log("👁 Bounding Sphere:", geometry.boundingSphere); + // console.log("👁 Canvas size:", canvas.width(), canvas.height()); } function resetscene(s) { @@ -916,7 +909,6 @@ function initcanvas() { } if (renderer) { - console.log("♻️ Resetting renderer and canvas..."); renderer.dispose(); $("#canvas").empty(); } @@ -943,7 +935,7 @@ function initcanvas() { renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(canvas.width(), canvas.height()); canvas.append(renderer.domElement); - console.log("✅ appended:", renderer.domElement); + // console.log("✅ appended:", renderer.domElement); controls = new THREE.OrbitControls(camera, renderer.domElement); controls.minZoom = 0.5; @@ -1721,8 +1713,8 @@ function update() { } function previewdataurl(url, idx) { - console.log("🌍 Fetching external data from:", url); - console.log("🔍 Index:", idx); + // console.log("🌍 Fetching external data from:", url); + // console.log("🔍 Index:", idx); // if (!/\.(nii|nii\.gz|jdt|jdb|bmsh|jmsh|bnii|gz)$/i.test(url)) { if (!/file=.*\.(nii(\.gz)?|jdt|jdb|bmsh|jmsh|bnii)(?=(&|$))/i.test(url)) { @@ -1801,9 +1793,9 @@ function previewdataurl(url, idx) { let bjd; if (url.match(/\.nii\.gz/)) { - console.log("🔄 Processing NIfTI file..."); + // console.log("🔄 Processing NIfTI file..."); var origdata = pako.ungzip(arrayBuffer); - console.log("✅ Unzipped Data Length:", origdata.byteLength); + // console.log("✅ Unzipped Data Length:", origdata.byteLength); const header = new DataView(origdata.buffer); let headerlen = header.getUint32(0, true); let ndim = header.getUint16(40, true); @@ -1841,7 +1833,7 @@ function previewdataurl(url, idx) { NIFTIData: bjd.reshape(dims.reverse()).transpose(), }; } else { - console.log("🔄 Processing BJData..."); + // console.log("🔄 Processing BJData..."); bjd = bjdata.decode(buffer.Buffer.from(arrayBuffer)); // bjd = bjdata.decode(new Uint8Array(arrayBuffer)); let jd = new jdata(bjd[0], { base64: false }); @@ -1849,7 +1841,7 @@ function previewdataurl(url, idx) { } var plotdata = bjd; - console.log("plotdata", plotdata); + // console.log("plotdata", plotdata); if (linkpath.length > 1 && !linkpath[1].match(/^Mesh[NSEVT]/)) { let objpath = linkpath[1].split(/(? Date: Wed, 3 Sep 2025 12:24:28 -0400 Subject: [PATCH 09/14] feat: add copy button to tree rows; refs #88 --- .../DatasetDetailPage/CopyButton.tsx | 55 ++++++++++++++ .../DatasetDetailPage/FileTree/FileTree.tsx | 3 + .../FileTree/FileTreeRow.tsx | 75 ++++++++++++++++++- src/pages/UpdatedDatasetDetailPage.tsx | 24 ++++++ 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/components/DatasetDetailPage/CopyButton.tsx diff --git a/src/components/DatasetDetailPage/CopyButton.tsx b/src/components/DatasetDetailPage/CopyButton.tsx new file mode 100644 index 0000000..d25d30f --- /dev/null +++ b/src/components/DatasetDetailPage/CopyButton.tsx @@ -0,0 +1,55 @@ +import CheckIcon from "@mui/icons-material/Check"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { IconButton, Tooltip } from "@mui/material"; +import React from "react"; + +const write = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // Fallback + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + const ok = document.execCommand("copy"); + document.body.removeChild(ta); + return ok; + } +}; + +export default function CopyButton({ + text, + title = "Copy", + size = "small", +}: { + text: string; + title?: string; + size?: "small" | "medium" | "large"; +}) { + const [ok, setOk] = React.useState(false); + + const onClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (await write(text)) { + setOk(true); + setTimeout(() => setOk(false), 1200); + } + }; + + return ( + + + {ok ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx index e028596..1b5b8de 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTree.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -13,6 +13,7 @@ type Props = { // for preview in tree row onPreview: (src: string | any, index: number, isInternal?: boolean) => void; getInternalByPath: (path: string) => { data: any; index: number } | undefined; + getJsonByPath?: (path: string) => any; }; const formatSize = (n: number) => { @@ -30,6 +31,7 @@ const FileTree: React.FC = ({ totalBytes, onPreview, getInternalByPath, + getJsonByPath, }) => ( = ({ level={0} onPreview={onPreview} getInternalByPath={getInternalByPath} + getJsonByPath={getJsonByPath} /> // pass the handlePreview(onPreview = handlePreview) function to FileTreeRow ))} diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index d1673da..15981ef 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -1,6 +1,10 @@ // for rendering the preview and download buttons in folder structure row import type { TreeNode } from "./types"; import { formatLeafValue, isPreviewable } from "./utils"; +import CheckIcon from "@mui/icons-material/Check"; +// for copy button +// add to imports +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import DownloadIcon from "@mui/icons-material/Download"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; @@ -8,6 +12,7 @@ import FolderIcon from "@mui/icons-material/Folder"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import VisibilityIcon from "@mui/icons-material/Visibility"; import { Box, Button, Collapse, Typography } from "@mui/material"; +import { Tooltip, IconButton } from "@mui/material"; import { Colors } from "design/theme"; import React from "react"; @@ -18,6 +23,26 @@ type Props = { // src is either an external URL(string) or the internal object onPreview: (src: string | any, index: number, isInternal?: boolean) => void; getInternalByPath: (path: string) => { data: any; index: number } | undefined; + getJsonByPath?: (path: string) => any; +}; + +// copy helper function +const copyText = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // fallback if the copy api not working + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + const ok = document.execCommand("copy"); + document.body.removeChild(ta); + return ok; + } }; const FileTreeRow: React.FC = ({ @@ -25,13 +50,26 @@ const FileTreeRow: React.FC = ({ level, onPreview, getInternalByPath, + getJsonByPath, }) => { const [open, setOpen] = React.useState(false); + const [copied, setCopied] = React.useState(false); // const internal = getInternalByPath?.(node.path); // const internal = getInternalByPath ? getInternalByPath(node.path) : undefined; const internal = getInternalByPath(node.path); const externalUrl = node.link?.url; + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); // prevent expand/ collapse from firing when click the copy button + const json = getJsonByPath?.(node.path); // call getJsonByPath(node.path) + const asText = JSON.stringify(json, null, 2); // subtree at this row + if (await copyText(asText ?? "null")) { + // call copyText function + setCopied(true); + setTimeout(() => setCopied(false), 1200); + } + }; + // if (node.kind === "folder") { // return ( // <> @@ -103,7 +141,7 @@ const FileTreeRow: React.FC = ({ {node.name} - {/* ✅ Actions on folder if it carries a link (from linkHere) */} + {/* Actions on folder if it carries a link (from linkHere) */} {node.link?.url && ( = ({ )} + {/* Copy subtree JSON button */} + e.stopPropagation()} + > + + + {copied ? ( + + ) : ( + + )} + + + + {open ? : }
@@ -162,6 +216,7 @@ const FileTreeRow: React.FC = ({ level={level + 1} onPreview={onPreview} getInternalByPath={getInternalByPath} + getJsonByPath={getJsonByPath} /> ))}
@@ -218,7 +273,22 @@ const FileTreeRow: React.FC = ({ )} - + {/* ALWAYS show copy for files, even when no external/internal */} + + + + {copied ? ( + + ) : ( + + )} + + + {(externalUrl || internal) && ( = ({ Preview )} + {/* {node.link?.url && ( )} - - {/* {node.link?.url && ( - - - {isPreviewable(node.link.url) && ( - - )} */} )} From bc573005e8760fdf2241987384724ed7a9921884 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Thu, 4 Sep 2025 11:48:15 -0400 Subject: [PATCH 11/14] feat: add show more and show less for long string values in node tree; refs #88 --- .../FileTree/FileTreeRow.tsx | 138 +++++++++++++++--- src/pages/UpdatedDatasetDetailPage.tsx | 14 +- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index ad2279f..2f89247 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -12,7 +12,65 @@ import VisibilityIcon from "@mui/icons-material/Visibility"; import { Box, Button, Collapse, Typography } from "@mui/material"; import { Tooltip, IconButton } from "@mui/material"; import { Colors } from "design/theme"; -import React from "react"; +import React, { useState } from "react"; + +// FileTreeRow.tsx (top of file, below imports) +const LeafString: React.FC<{ value: string }> = ({ value }) => { + const LIMIT = 120; + const [expanded, setExpanded] = React.useState(false); + + const isLong = value.length > LIMIT; + const display = expanded + ? value + : isLong + ? value.slice(0, LIMIT) + "…" + : value; + + return ( + + + {display} + + + {isLong && ( + + )} + + ); +}; type Props = { node: TreeNode; @@ -50,8 +108,9 @@ const FileTreeRow: React.FC = ({ getInternalByPath, getJsonByPath, }) => { - const [open, setOpen] = React.useState(false); - const [copied, setCopied] = React.useState(false); + const [open, setOpen] = useState(false); + const [copied, setCopied] = useState(false); + const [showFull, setShowFull] = useState(false); // const internal = getInternalByPath?.(node.path); // const internal = getInternalByPath ? getInternalByPath(node.path) : undefined; const internal = getInternalByPath(node.path); @@ -69,14 +128,13 @@ const FileTreeRow: React.FC = ({ }; if (node.kind === "folder") { - // const isSubject = /^sub-/i.test(node.name); // subject folders only const isJson = /\.json$/i.test(node.name); // end with .json only return ( <> = ({ } // if the node is a file return ( - + = ({ = ({ {node.name} - {!node.link && node.value !== undefined && ( + {!node.link && + node.value !== undefined && + (typeof node.value === "string" ? ( + + ) : ( + + {node.name === "_ArrayZipData_" + ? "[compressed data]" + : formatLeafValue(node.value)} + + ))} + + {/* {!node.link && node.value !== undefined && ( = ({ ? "[compressed data]" : formatLeafValue(node.value)} - )} + )} */} {/* ALWAYS show copy for files, even when no external/internal */} - - - - {copied ? ( - - ) : ( - - )} - - - + + + + + {copied ? ( + + ) : ( + + )} + + + + {/* Placeholder to align with folder chevron */} {(externalUrl || internal) && ( diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index cfdf6f9..c763396 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -1124,7 +1124,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { {/* JSON */} - { collapsed={1} style={{ fontSize: "14px", fontFamily: "monospace" }} /> - + */} - - {/* = 3 ? false : 1} // 🔍 Expand during search - style={{ fontSize: "14px", fontFamily: "monospace" }} - /> */} {/* Data panels (right panel) */} From 9e7fca341f61ac2ab23f50ebf02e6b23d4fafbf3 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Thu, 4 Sep 2025 15:32:00 -0400 Subject: [PATCH 12/14] feat: clamp leaf text to 1 line by default; closes #88 --- .../DatasetDetailPage/FileTree/FileTreeRow.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 2f89247..bc43d20 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -14,10 +14,10 @@ import { Tooltip, IconButton } from "@mui/material"; import { Colors } from "design/theme"; import React, { useState } from "react"; -// FileTreeRow.tsx (top of file, below imports) +// show more / show less button for long string const LeafString: React.FC<{ value: string }> = ({ value }) => { const LIMIT = 120; - const [expanded, setExpanded] = React.useState(false); + const [expanded, setExpanded] = useState(false); const isLong = value.length > LIMIT; const display = expanded @@ -45,7 +45,7 @@ const LeafString: React.FC<{ value: string }> = ({ value }) => { // collapsed: clamp to 2 lines; expanded: fully wrap display: expanded ? "block" : "-webkit-box", WebkitBoxOrient: "vertical", - WebkitLineClamp: expanded ? ("unset" as any) : 2, + WebkitLineClamp: expanded ? ("unset" as any) : 1, whiteSpace: expanded ? "pre-wrap" : "normal", overflow: expanded ? "visible" : "hidden", textOverflow: expanded ? "unset" : "ellipsis", @@ -63,7 +63,11 @@ const LeafString: React.FC<{ value: string }> = ({ value }) => { e.stopPropagation(); // don’t toggle the row setExpanded((v) => !v); }} - sx={{ px: 0.5, minWidth: "auto" }} + sx={{ + px: 0.5, + minWidth: "auto", + color: Colors.purple, + }} > {expanded ? "Show less" : "Show more"} @@ -110,7 +114,6 @@ const FileTreeRow: React.FC = ({ }) => { const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); - const [showFull, setShowFull] = useState(false); // const internal = getInternalByPath?.(node.path); // const internal = getInternalByPath ? getInternalByPath(node.path) : undefined; const internal = getInternalByPath(node.path); @@ -154,7 +157,6 @@ const FileTreeRow: React.FC = ({ = ({ title={ node.name === "_ArrayZipData_" ? "[compressed data]" - : typeof node.value === "string" - ? node.value : JSON.stringify(node.value) } sx={{ From d9183569527bcc2f300156b734b9312e0bdefcc1 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Fri, 5 Sep 2025 23:53:23 -0400 Subject: [PATCH 13/14] feat: add metadata panel to dataset detail page --- .../FileTree/FileTreeRow.tsx | 16 +- src/pages/UpdatedDatasetDetailPage.tsx | 550 ++++++++++-------- src/redux/neurojson/neurojson.action.ts | 16 + src/redux/neurojson/neurojson.slice.ts | 17 + .../neurojson/types/neurojson.interface.ts | 1 + src/services/neurojson.service.ts | 16 + src/utils/preview.js | 24 +- 7 files changed, 391 insertions(+), 249 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index bc43d20..d490104 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -7,6 +7,7 @@ import DownloadIcon from "@mui/icons-material/Download"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; import FolderIcon from "@mui/icons-material/Folder"; +import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import VisibilityIcon from "@mui/icons-material/Visibility"; import { Box, Button, Collapse, Typography } from "@mui/material"; @@ -147,12 +148,23 @@ const FileTreeRow: React.FC = ({ onClick={() => setOpen((o) => !o)} > - + /> */} + {open ? ( + + ) : ( + + )} { selectedDocument: datasetDocument, loading, error, + datasetViewInfo: dbViewInfo, } = useAppSelector(NeurojsonSelector); const [externalLinks, setExternalLinks] = useState([]); @@ -394,16 +398,22 @@ const UpdatedDatasetDetailPage: React.FC = () => { const fetchData = async () => { if (dbName && docId) { await dispatch(fetchDocumentDetails({ dbName, docId })); + await dispatch(fetchDbInfoByDatasetId({ dbName, docId })); } }; fetchData(); }, [dbName, docId, dispatch]); + useEffect(() => { + if (dbViewInfo) { + console.log("yeeeeeeee", dbViewInfo); + } + }); useEffect(() => { if (datasetDocument) { // Extract External Data & Assign `index` - // console.log("datasetDocument", datasetDocument); + console.log("datasetDocument", datasetDocument); const links = extractDataLinks(datasetDocument, "").map( (link, index) => ({ ...link, @@ -665,18 +675,18 @@ const UpdatedDatasetDetailPage: React.FC = () => { typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : ""; if (isPreviewableFile(fileName)) { (window as any).previewdataurl(dataOrUrl, idx); - const is2D = is2DPreviewCandidate(dataOrUrl); - const panel = document.getElementById("chartpanel"); + // const is2D = is2DPreviewCandidate(dataOrUrl); + // const panel = document.getElementById("chartpanel"); // console.log("is2D", is2D); // console.log("panel", panel); - if (is2D) { - // console.log("📊 2D data → rendering inline with dopreview()"); - if (panel) panel.style.display = "block"; // Show it! - setPreviewOpen(false); // Don't open modal - } else { - if (panel) panel.style.display = "none"; // Hide chart panel on 3D external - } + // if (is2D) { + // // console.log("📊 2D data → rendering inline with dopreview()"); + // if (panel) panel.style.display = "block"; // Show it! + // setPreviewOpen(false); // Don't open modal + // } else { + // if (panel) panel.style.display = "none"; // Hide chart panel on 3D external + // } } else { console.warn("⚠️ Unsupported file format for preview:", dataOrUrl); } @@ -851,7 +861,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { ); } - // console.log("datasetDocument", datasetDocument); + const onekey = datasetDocument ? datasetDocument.hasOwnProperty("README") ? "README" @@ -1050,18 +1060,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { -
{ }, height: { xs: "auto", - md: "960px", // fixed height container + md: "560px", // fixed height container }, }} > @@ -1122,46 +1120,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { getJsonByPath={getJsonByPath} /> - - {/* JSON */} - {/* - - - Raw data - - - - - */} @@ -1187,48 +1145,259 @@ const UpdatedDatasetDetailPage: React.FC = () => { > - {/* Collapsible header */} + + + Modalities + + + {dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"} + + + + + + DOI + + + {datasetDocument?.["dataset_description.json"]?.DatasetDOI || + datasetDocument?.["dataset_description.json"] + ?.ReferenceDOI || + "N/A"} + + + + + + Subjects + + + {datasetDocument?.["participants.tsv"]?.["participant_id"] + ?.length ?? "N/A"} + + + + + License + + + {datasetDocument?.["dataset_description.json"]?.License ?? + "N/A"} + + + + + BIDS Version + + + {datasetDocument?.["dataset_description.json"]?.BIDSVersion ?? + "N/A"} + + + + + References and Links + + + {Array.isArray( + datasetDocument?.["dataset_description.json"] + ?.ReferencesAndLinks + ) + ? datasetDocument["dataset_description.json"] + .ReferencesAndLinks.length > 0 + ? datasetDocument[ + "dataset_description.json" + ].ReferencesAndLinks.join(", ") + : "N/A" + : datasetDocument?.["dataset_description.json"] + ?.ReferencesAndLinks ?? "N/A"} + + + + + + + + + {/* Collapsible header */} + setIsInternalExpanded(!isInternalExpanded)} + > + + Internal Data ({internalLinks.length} objects) + + {isInternalExpanded ? : } + + + + {/* Scrollable area */} setIsInternalExpanded(!isInternalExpanded)} > - - Internal Data ({internalLinks.length} objects) - - {isInternalExpanded ? : } + {internalLinks.length > 0 ? ( + internalLinks.map((link, index) => ( + + + {link.name}{" "} + {link.arraySize ? `[${link.arraySize.join("x")}]` : ""} + + + + )) + ) : ( + + No internal data found. + + )} + + + + {/* Header with toggle */} + setIsExternalExpanded(!isExternalExpanded)} + > + + External Data ({externalLinks.length} links) + + {isExternalExpanded ? : } + - - {/* Scrollable area */} - - {internalLinks.length > 0 ? ( - internalLinks.map((link, index) => ( + + {/* Scrollable card container */} + + {externalLinks.length > 0 ? ( + externalLinks.map((link, index) => { + const match = link.url.match(/file=([^&]+)/); + const fileName = match ? match[1] : ""; + const isPreviewable = + /\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test(fileName); + + return ( { - {link.name}{" "} - {link.arraySize - ? `[${link.arraySize.join("x")}]` - : ""} + {link.name} - - - )) - ) : ( - - No internal data found. - - )} - - - - - {/* Header with toggle */} - setIsExternalExpanded(!isExternalExpanded)} - > - - External Data ({externalLinks.length} links) - - {isExternalExpanded ? : } - - - - {/* Scrollable card container */} - - {externalLinks.length > 0 ? ( - externalLinks.map((link, index) => { - const match = link.url.match(/file=([^&]+)/); - const fileName = match ? match[1] : ""; - const isPreviewable = - /\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test( - fileName - ); - - return ( - - + + {isPreviewable && ( - {isPreviewable && ( - - )} - + )} - ); - }) - ) : ( - - No external links found. - - )} - - - + + ); + }) + ) : ( + + No external links found. + + )} + + +
+ {/*
*/} { + try { + const data = await NeurojsonService.getDbInfoByDatasetId(dbName, docId); + console.log("data in action", data); + return { ...data, dbName, docId }; + } catch (error: any) { + return rejectWithValue(error.message || "Failed to fetch dataset info."); + } + } +); diff --git a/src/redux/neurojson/neurojson.slice.ts b/src/redux/neurojson/neurojson.slice.ts index b211e92..fdcd2ce 100644 --- a/src/redux/neurojson/neurojson.slice.ts +++ b/src/redux/neurojson/neurojson.slice.ts @@ -6,6 +6,7 @@ import { fetchDocumentDetails, fetchDbStats, fetchMetadataSearchResults, + fetchDbInfoByDatasetId, } from "./neurojson.action"; import { DBDatafields, INeuroJsonState } from "./types/neurojson.interface"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; @@ -24,6 +25,7 @@ const initialState: INeuroJsonState = { dbInfo: null, // add dbInfo in neurojson.interface.ts dbStats: null, searchResults: null, + datasetViewInfo: null, }; const neurojsonSlice = createSlice({ @@ -148,6 +150,21 @@ const neurojsonSlice = createSlice({ .addCase(fetchMetadataSearchResults.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; + }) + .addCase(fetchDbInfoByDatasetId.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase( + fetchDbInfoByDatasetId.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.datasetViewInfo = action.payload; + } + ) + .addCase(fetchDbInfoByDatasetId.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; }); }, }); diff --git a/src/redux/neurojson/types/neurojson.interface.ts b/src/redux/neurojson/types/neurojson.interface.ts index fd06a52..365566d 100644 --- a/src/redux/neurojson/types/neurojson.interface.ts +++ b/src/redux/neurojson/types/neurojson.interface.ts @@ -12,6 +12,7 @@ export interface INeuroJsonState { dbInfo: DBParticulars | null; // add dbInfo type dbStats: DbStatsItem[] | null; // for dbStats on landing page searchResults: any[] | { status: string; msg: string } | null; + datasetViewInfo: any | null; } export interface DBParticulars { diff --git a/src/services/neurojson.service.ts b/src/services/neurojson.service.ts index d2a55d3..02aa590 100644 --- a/src/services/neurojson.service.ts +++ b/src/services/neurojson.service.ts @@ -100,4 +100,20 @@ export const NeurojsonService = { return response.data; }, + + getDbInfoByDatasetId: async (dbName: string, dsId: string): Promise => { + const response = await api.get( + `${baseURL}/${dbName}/_design/qq/_view/dbinfo`, + { + params: { + // CouchDB expects a JSON value; this produces %22ds000001%22 + key: JSON.stringify(dsId), + // include_docs is optional; keep it if your view needs the full doc + include_docs: true, + // reduce: false, // uncomment if your view has a reduce function + }, + } + ); + return response.data; + }, }; diff --git a/src/utils/preview.js b/src/utils/preview.js index 9409351..912c8f8 100644 --- a/src/utils/preview.js +++ b/src/utils/preview.js @@ -281,24 +281,36 @@ function dopreview(key, idx, isinternal, hastime) { } } else { // dataroot = key; - // console.log("into dopreview external data's dataroot", dataroot); + // new code start---- + // if (typeof key === "object") { + // dataroot = key; + // console.log("dataroot======", dataroot); + // } else if ( + // window.extdata && + // window.extdata[idx] && + // window.extdata[idx][2] + // ) { + // dataroot = window.extdata[idx][2]; + // console.log("dataroot======>", dataroot); + // } else { + // console.error("External data not ready for index", idx); + // return; + // } + // new code end---- + // original code start----- if (window.extdata && window.extdata[idx] && window.extdata[idx][2]) { if (typeof key === "object") { dataroot = key; - // console.log("if key is object", typeof key); } else { dataroot = window.extdata[idx][2]; - // console.log("type of key", typeof key); } - // dataroot = key; - - // console.log("into dopreview external data's dataroot", dataroot); } else { console.error("❌ External data not ready for index", idx); return; } + // original code end---- } if (dataroot.hasOwnProperty("_ArraySize_")) { From 0f8b71d01ec824b3d4779e246e69ca821899923a Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 8 Sep 2025 15:32:21 -0400 Subject: [PATCH 14/14] style: adjust the four panels layout in dataset detail page --- src/pages/UpdatedDatasetDetailPage.tsx | 507 +++++++----------------- src/redux/neurojson/neurojson.action.ts | 1 - 2 files changed, 136 insertions(+), 372 deletions(-) diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 0e41a0e..c7b2af3 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -3,7 +3,7 @@ import CloudDownloadIcon from "@mui/icons-material/CloudDownload"; import DescriptionIcon from "@mui/icons-material/Description"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; -import FolderIcon from "@mui/icons-material/Folder"; +// import FolderIcon from "@mui/icons-material/Folder"; import HomeIcon from "@mui/icons-material/Home"; import { Box, @@ -14,13 +14,11 @@ import { Button, Collapse, } from "@mui/material"; -// new import import FileTree from "components/DatasetDetailPage/FileTree/FileTree"; import { buildTreeFromDoc, makeLinkMap, } from "components/DatasetDetailPage/FileTree/utils"; -// import { TextField } from "@mui/material"; import LoadDatasetTabs from "components/DatasetDetailPage/LoadDatasetTabs"; import ReadMoreText from "design/ReadMoreText"; import { Colors } from "design/theme"; @@ -52,89 +50,6 @@ interface InternalDataLink { path: string; // for preview in tree row } -// const transformJsonForDisplay = (obj: any): any => { -// if (typeof obj !== "object" || obj === null) return obj; - -// const transformed: any = Array.isArray(obj) ? [] : {}; - -// for (const key in obj) { -// if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; - -// const value = obj[key]; - -// // Match README, CHANGES, or file extensions -// const isLongTextKey = /^(README|CHANGES)$|\.md$|\.txt$|\.m$/i.test(key); - -// if (typeof value === "string" && isLongTextKey) { -// transformed[key] = `${value}`; -// } else if (typeof value === "object") { -// transformed[key] = transformJsonForDisplay(value); -// } else { -// transformed[key] = value; -// } -// } - -// return transformed; -// }; - -// const formatAuthorsWithDOI = ( -// authors: string[] | string, -// doi: string -// ): JSX.Element => { -// let authorText = ""; - -// if (Array.isArray(authors)) { -// if (authors.length === 1) { -// authorText = authors[0]; -// } else if (authors.length === 2) { -// authorText = authors.join(", "); -// } else { -// authorText = `${authors.slice(0, 2).join("; ")} et al.`; -// } -// } else { -// authorText = authors; -// } - -// let doiUrl = ""; -// if (doi) { -// if (/^[0-9]/.test(doi)) { -// doiUrl = `https://doi.org/${doi}`; -// } else if (/^doi\./.test(doi)) { -// doiUrl = `https://${doi}`; -// } else if (/^doi:/.test(doi)) { -// doiUrl = doi.replace(/^doi:/, "https://doi.org/"); -// } else { -// doiUrl = doi; -// } -// } - -// return ( -// <> -// {authorText} -// {doiUrl && ( -// -// (e.currentTarget.style.textDecoration = "underline") -// } -// onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")} -// > -// {doiUrl} -// -// )} -// -// ); -// }; - const UpdatedDatasetDetailPage: React.FC = () => { const { dbName, docId } = useParams<{ dbName: string; docId: string }>(); const navigate = useNavigate(); @@ -148,24 +63,13 @@ const UpdatedDatasetDetailPage: React.FC = () => { const [externalLinks, setExternalLinks] = useState([]); const [internalLinks, setInternalLinks] = useState([]); - // const [isExpanded, setIsExpanded] = useState(false); const [isInternalExpanded, setIsInternalExpanded] = useState(true); - // const [searchTerm, setSearchTerm] = useState(""); - // const [matches, setMatches] = useState([]); - // const [highlightedIndex, setHighlightedIndex] = useState(-1); const [downloadScript, setDownloadScript] = useState(""); const [downloadScriptSize, setDownloadScriptSize] = useState(0); const [totalFileSize, setTotalFileSize] = useState(0); - const [previewIsInternal, setPreviewIsInternal] = useState(false); const [isExternalExpanded, setIsExternalExpanded] = useState(true); - // const [expandedPaths, setExpandedPaths] = useState([]); - // const [originalTextMap, setOriginalTextMap] = useState< - // Map - // >(new Map()); - // const [jsonViewerKey, setJsonViewerKey] = useState(0); const [jsonSize, setJsonSize] = useState(0); - // const [transformedDataset, setTransformedDataset] = useState(null); const [previewIndex, setPreviewIndex] = useState(0); const aiSummary = datasetDocument?.[".datainfo"]?.AISummary ?? ""; @@ -175,41 +79,8 @@ const UpdatedDatasetDetailPage: React.FC = () => { [datasetDocument] ); - // 2) keep current subjects-only split, return subject objects list - // const subjectsOnly = useMemo(() => { - // const out: any = {}; - // if (!datasetDocument) return out; - // Object.keys(datasetDocument).forEach((k) => { - // if (/^sub-/i.test(k)) out[k] = (datasetDocument as any)[k]; - // }); - // return out; - // }, [datasetDocument]); - - // 3) link maps - // const subjectLinks = useMemo( - // () => externalLinks.filter((l) => /^\/sub-/i.test(l.path)), - // [externalLinks] - // ); - // const subjectLinkMap = useMemo( - // () => makeLinkMap(subjectLinks), - // [subjectLinks] - // ); const linkMap = useMemo(() => makeLinkMap(externalLinks), [externalLinks]); - // 4) build a folder/file tree with a fallback to the WHOLE doc when no subjects exist - // const treeData = useMemo( - // () => - // hasTopLevelSubjects - // ? buildTreeFromDoc(subjectsOnly, subjectLinkMap) - // : buildTreeFromDoc(datasetDocument || {}, makeLinkMap(externalLinks)), - // [ - // hasTopLevelSubjects, - // subjectsOnly, - // subjectLinkMap, - // datasetDocument, - // externalLinks, - // ] - // ); const treeData = useMemo( () => buildTreeFromDoc(datasetDocument || {}, linkMap, ""), [datasetDocument, linkMap] @@ -245,15 +116,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { } return bytes; }, [externalLinks]); - // const { filesCount, totalBytes } = useMemo(() => { - // const group = hasTopLevelSubjects ? subjectLinks : externalLinks; - // let bytes = 0; - // for (const l of group) { - // const m = l.url.match(/size=(\d+)/); - // if (m) bytes += parseInt(m[1], 10); - // } - // return { filesCount: group.length, totalBytes: bytes }; - // }, [hasTopLevelSubjects, subjectLinks, externalLinks]); // add spinner const [isPreviewLoading, setIsPreviewLoading] = useState(false); @@ -405,11 +267,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { fetchData(); }, [dbName, docId, dispatch]); - useEffect(() => { - if (dbViewInfo) { - console.log("yeeeeeeee", dbViewInfo); - } - }); useEffect(() => { if (datasetDocument) { // Extract External Data & Assign `index` @@ -429,13 +286,8 @@ const UpdatedDatasetDetailPage: React.FC = () => { }) ); - // console.log(" Extracted external links:", links); - // console.log(" Extracted internal data:", internalData); - setExternalLinks(links); setInternalLinks(internalData); - // const transformed = transformJsonForDisplay(datasetDocument); - // setTransformedDataset(transformed); // Calculate total file size from size= query param let total = 0; @@ -501,33 +353,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { const [previewOpen, setPreviewOpen] = useState(false); const [previewDataKey, setPreviewDataKey] = useState(null); - // useEffect(() => { - // highlightMatches(searchTerm); - - // // Cleanup to reset highlights when component re-renders or unmounts - // return () => { - // document.querySelectorAll(".highlighted").forEach((el) => { - // const element = el as HTMLElement; - // const text = element.textContent || ""; - // element.innerHTML = text; - // element.classList.remove("highlighted"); - // }); - // }; - // }, [searchTerm, datasetDocument]); - - // useEffect(() => { - // if (!transformedDataset) return; - - // const spans = document.querySelectorAll(".string-value"); - - // spans.forEach((el) => { - // if (el.textContent?.includes('')) { - // // Inject as HTML so it renders code block correctly - // el.innerHTML = el.textContent ?? ""; - // } - // }); - // }, [transformedDataset]); - const handleDownloadDataset = () => { if (!datasetDocument) return; const jsonData = JSON.stringify(datasetDocument); @@ -741,102 +566,12 @@ const UpdatedDatasetDetailPage: React.FC = () => { const panel = document.getElementById("chartpanel"); if (panel) panel.style.display = "none"; - // Remove canvas children - // const canvasDiv = document.getElementById("canvas"); - // if (canvasDiv) { - // while (canvasDiv.firstChild) { - // canvasDiv.removeChild(canvasDiv.firstChild); - // } - // } - // Reset Three.js global refs window.scene = undefined; window.camera = undefined; window.renderer = undefined; }; - // const handleSearch = (e: React.ChangeEvent) => { - // setSearchTerm(e.target.value); - // setHighlightedIndex(-1); - // highlightMatches(e.target.value); - // }; - - // const highlightMatches = (keyword: string) => { - // const spans = document.querySelectorAll( - // ".react-json-view span.string-value, .react-json-view span.object-key" - // ); - - // // Clean up all existing highlights - // spans.forEach((el) => { - // const element = el as HTMLElement; - // if (originalTextMap.has(element)) { - // element.innerHTML = originalTextMap.get(element)!; // Restore original HTML - // element.classList.remove("highlighted"); - // } - // }); - - // // Clear old state - // setMatches([]); - // setHighlightedIndex(-1); - // setExpandedPaths([]); - // setOriginalTextMap(new Map()); - - // if (!keyword.trim() || keyword.length < 3) return; - - // const regex = new RegExp(`(${keyword})`, "gi"); - // const matchedElements: HTMLElement[] = []; - // const matchedPaths: Set = new Set(); - // const newOriginalMap = new Map(); - - // spans.forEach((el) => { - // const element = el as HTMLElement; - // const original = element.innerHTML; - // const text = element.textContent || ""; - - // if (text.toLowerCase().includes(keyword.toLowerCase())) { - // newOriginalMap.set(element, original); // Store original HTML - // const highlighted = text.replace( - // regex, - // `$1` - // ); - // element.innerHTML = highlighted; - // matchedElements.push(element); - - // const parent = element.closest(".variable-row"); - // const path = parent?.getAttribute("data-path"); - // if (path) matchedPaths.add(path); - // } - // }); - - // // Update state - // setOriginalTextMap(newOriginalMap); - // setMatches(matchedElements); - // setExpandedPaths(Array.from(matchedPaths)); - // }; - - // const findNext = () => { - // if (matches.length === 0) return; - - // setHighlightedIndex((prevIndex) => { - // const nextIndex = (prevIndex + 1) % matches.length; - - // matches.forEach((match) => { - // match - // .querySelector("mark") - // ?.setAttribute("style", "background: yellow; color: black;"); - // }); - - // const current = matches[nextIndex]; - // current.scrollIntoView({ behavior: "smooth", block: "center" }); - - // current - // .querySelector("mark") - // ?.setAttribute("style", "background: orange; color: black;"); - - // return nextIndex; - // }); - // }; - if (loading) { return ( { totalFileSize )})`} - - {/* - - - */} @@ -1076,7 +790,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { }, }} > - {/* JSON Viewer (left panel) */} + {/* tree viewer (left panel) */} { - {/* Data panels (right panel) */} + {/* MetaData panels (right panel) */} { }, display: "flex", flexDirection: "column", - gap: 2, }} > - - - Modalities - - - {dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"} - - + + + + Modalities + + + {dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? + "N/A"} + + - - - DOI - - - {datasetDocument?.["dataset_description.json"]?.DatasetDOI || - datasetDocument?.["dataset_description.json"] - ?.ReferenceDOI || - "N/A"} - - + + + DOI + + + {(() => { + const doi = + datasetDocument?.["dataset_description.json"] + ?.DatasetDOI || + datasetDocument?.["dataset_description.json"] + ?.ReferenceDOI; + + if (!doi) return "N/A"; + + // Normalize into a clickable URL + let url = doi; + if (/^10\./.test(doi)) { + url = `https://doi.org/${doi}`; + } else if (/^doi:/.test(doi)) { + url = `https://doi.org/${doi.replace(/^doi:/, "")}`; + } + + return ( + + {url} + + ); + })()} + + - - - Subjects - - - {datasetDocument?.["participants.tsv"]?.["participant_id"] - ?.length ?? "N/A"} - - - - - License - - - {datasetDocument?.["dataset_description.json"]?.License ?? - "N/A"} - - - - - BIDS Version - - - {datasetDocument?.["dataset_description.json"]?.BIDSVersion ?? - "N/A"} - - - - - References and Links - - - {Array.isArray( - datasetDocument?.["dataset_description.json"] - ?.ReferencesAndLinks - ) - ? datasetDocument["dataset_description.json"] - .ReferencesAndLinks.length > 0 - ? datasetDocument[ - "dataset_description.json" - ].ReferencesAndLinks.join(", ") - : "N/A" - : datasetDocument?.["dataset_description.json"] - ?.ReferencesAndLinks ?? "N/A"} - + + + Subjects + + + {datasetDocument?.["participants.tsv"]?.["participant_id"] + ?.length ?? "N/A"} + + + + + License + + + {datasetDocument?.["dataset_description.json"]?.License ?? + "N/A"} + + + + + BIDS Version + + + {datasetDocument?.["dataset_description.json"] + ?.BIDSVersion ?? "N/A"} + + + + + References and Links + + + {Array.isArray( + datasetDocument?.["dataset_description.json"] + ?.ReferencesAndLinks + ) + ? datasetDocument["dataset_description.json"] + .ReferencesAndLinks.length > 0 + ? datasetDocument[ + "dataset_description.json" + ].ReferencesAndLinks.join(", ") + : "N/A" + : datasetDocument?.["dataset_description.json"] + ?.ReferencesAndLinks ?? "N/A"} + + @@ -1241,9 +993,21 @@ const UpdatedDatasetDetailPage: React.FC = () => { { borderRadius: "8px", flex: 1, // overflowY: "auto", + overflow: "hidden", }} > {/* Header with toggle */} diff --git a/src/redux/neurojson/neurojson.action.ts b/src/redux/neurojson/neurojson.action.ts index c3b019c..813cd29 100644 --- a/src/redux/neurojson/neurojson.action.ts +++ b/src/redux/neurojson/neurojson.action.ts @@ -116,7 +116,6 @@ export const fetchDbInfoByDatasetId = createAsyncThunk( ) => { try { const data = await NeurojsonService.getDbInfoByDatasetId(dbName, docId); - console.log("data in action", data); return { ...data, dbName, docId }; } catch (error: any) { return rejectWithValue(error.message || "Failed to fetch dataset info.");