diff --git a/src/components/DatasetDetailPage/FileTree/FileTree.tsx b/src/components/DatasetDetailPage/FileTree/FileTree.tsx index d00456b..eb6689b 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTree.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTree.tsx @@ -8,8 +8,8 @@ import React from "react"; type Props = { title: string; tree: TreeNode[]; - filesCount: number; - totalBytes: number; + // filesCount: number; + // totalBytes: number; // for preview in tree row onPreview: (src: string | any, index: number, isInternal?: boolean) => void; getInternalByPath: (path: string) => { data: any; index: number } | undefined; @@ -17,19 +17,19 @@ type Props = { highlightText?: string; }; -const formatSize = (n: number) => { - if (n < 1024) return `${n} B`; - if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`; - if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(2)} MB`; - if (n < 1024 ** 4) return `${(n / 1024 ** 3).toFixed(2)} GB`; - return `${(n / 1024 ** 4).toFixed(2)} TB`; -}; +// const formatSize = (n: number) => { +// if (n < 1024) return `${n} B`; +// if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`; +// if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(2)} MB`; +// if (n < 1024 ** 4) return `${(n / 1024 ** 3).toFixed(2)} GB`; +// return `${(n / 1024 ** 4).toFixed(2)} TB`; +// }; const FileTree: React.FC = ({ title, tree, - filesCount, - totalBytes, + // filesCount, + // totalBytes, onPreview, getInternalByPath, getJsonByPath, diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index e9ee706..4aceee8 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -123,26 +123,14 @@ const FileTreeRow: React.FC = ({ const externalUrl = node.link?.url; const rowRef = React.useRef(null); - // Highlight only if this row is exactly the subject folder (e.g., "sub-04") - const isSubjectFolder = - node.kind === "folder" && /^sub-[A-Za-z0-9]+$/i.test(node.name); + // Highlight only if this row is exactly the same as the focus highlightText const isExactHit = - !!highlightText && - isSubjectFolder && - node.name.toLowerCase() === highlightText.toLowerCase(); + node.name.trim().toLowerCase() === + (highlightText ?? "").trim().toLowerCase(); React.useEffect(() => { if (isExactHit && rowRef.current) { rowRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); - // subtle flash - // rowRef.current.animate( - // [ - // { backgroundColor: `${Colors.yellow}`, offset: 0 }, // turn yellow - // { backgroundColor: `${Colors.yellow}`, offset: 0.85 }, // stay yellow 85% of time - // { backgroundColor: "transparent", offset: 1 }, // then fade out - // ], - // { duration: 8000, easing: "ease", fill: "forwards" } - // ); } }, [isExactHit]); @@ -294,7 +282,15 @@ const FileTreeRow: React.FC = ({ // if the node is a file return ( { const path = `${curPath}/[${i}]`; + if (linkMap.has(path)) { + // console.log("PATH", path); + // console.log("has exact", linkMap.has(path)); + // console.log("has _DataLink_", linkMap.has(`${path}/_DataLink_`)); + } else { + // console.log("nothing matching in array docs"); + } + const linkHere = linkMap.get(path) || linkMap.get(`${path}/_DataLink_`); - // For primitive items, show "1: value" in the *name* const isPrimitive = item === null || ["string", "number", "boolean"].includes(typeof item); const label = isPrimitive ? `${i}: ${formatLeafValue(item)}` : String(i); // objects/arrays just show "1", "2", ... @@ -97,7 +104,7 @@ export const buildTreeFromDoc = ( out.push({ kind: "folder", // name: `[${i}]`, - name: label, + name: label, // For primitive items, show "1: value" in the name path, link: linkHere, children: buildTreeFromDoc(item, linkMap, path), @@ -113,12 +120,21 @@ export const buildTreeFromDoc = ( }); } }); + // console.log("out", out); return out; } Object.keys(doc).forEach((key) => { const val = doc[key]; const path = `${curPath}/${key}`; + if (linkMap.has(path)) { + // console.log("PATH", path); + // console.log("has exact", linkMap.has(path)); + // console.log("has _DataLink_", linkMap.has(`${path}/_DataLink_`)); + } else { + // console.log("nothing match in object keys"); + } + const linkHere = linkMap.get(path) || linkMap.get(`${path}/_DataLink_`); if (val && typeof val === "object") { diff --git a/src/components/SearchPage/SubjectCard.tsx b/src/components/SearchPage/SubjectCard.tsx index 5aeb6e0..866e0ae 100644 --- a/src/components/SearchPage/SubjectCard.tsx +++ b/src/components/SearchPage/SubjectCard.tsx @@ -34,12 +34,7 @@ const SubjectCard: React.FC = ({ }) => { const { modalities, tasks, sessions, types } = parsedJson.value; const subjectLink = `${RoutesEnum.DATABASES}/${dbname}/${dsname}`; - const canonicalSubj = /^sub-/i.test(subj) - ? subj - : `sub-${String(subj) - .replace(/^sub-/i, "") - .replace(/^0+/, "") - .padStart(2, "0")}`; + const formattedSubj = /^sub-/i.test(subj) ? subj : `sub-${String(subj)}`; // get the gender of subject const genderCode = parsedJson?.key?.[1]; @@ -91,7 +86,7 @@ const SubjectCard: React.FC = ({ }} component={Link} // to={subjectLink} - to={`${subjectLink}?focusSubj=${encodeURIComponent(canonicalSubj)}`} + to={`${subjectLink}?focus=${encodeURIComponent(formattedSubj)}`} // target="_blank" > diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index d1f8735..e5516b9 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -1,4 +1,5 @@ import PreviewModal from "../components/PreviewModal"; +import CheckIcon from "@mui/icons-material/Check"; import CloudDownloadIcon from "@mui/icons-material/CloudDownload"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import DescriptionIcon from "@mui/icons-material/Description"; @@ -13,7 +14,8 @@ import { Alert, Button, Collapse, - Snackbar, + Tooltip, + IconButton, } from "@mui/material"; import FileTree from "components/DatasetDetailPage/FileTree/FileTree"; import { @@ -26,7 +28,7 @@ import ReadMoreText from "design/ReadMoreText"; import { Colors } from "design/theme"; import { useAppDispatch } from "hooks/useAppDispatch"; import { useAppSelector } from "hooks/useAppSelector"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState, useRef } from "react"; // import ReactJson from "react-json-view"; import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { @@ -56,27 +58,6 @@ interface InternalDataLink { const UpdatedDatasetDetailPage: React.FC = () => { const { dbName, docId } = useParams<{ dbName: string; docId: string }>(); const navigate = useNavigate(); - const [searchParams, setSearchParams] = useSearchParams(); - // for subject highlight - const focusSubjRaw = searchParams.get("focusSubj") || undefined; - const focusSubj = !focusSubjRaw - ? undefined - : /^sub-/i.test(focusSubjRaw) - ? focusSubjRaw - : `sub-${focusSubjRaw.replace(/^0+/, "").padStart(2, "0")}`; - - // for revision - const rev = searchParams.get("rev") || undefined; - - const handleSelectRevision = (newRev?: string | null) => { - setSearchParams((prev) => { - const p = new URLSearchParams(prev); // copy of the query url - if (newRev) p.set("rev", newRev); - else p.delete("rev"); - return p; - }); - }; - const dispatch = useAppDispatch(); const { selectedDocument: datasetDocument, @@ -84,6 +65,10 @@ const UpdatedDatasetDetailPage: React.FC = () => { error, datasetViewInfo: dbViewInfo, } = useAppSelector(NeurojsonSelector); + // get params from url + const [searchParams, setSearchParams] = useSearchParams(); + const focus = searchParams.get("focus") || undefined; // get highlight from url + const rev = searchParams.get("rev") || undefined; // get revision from url const [externalLinks, setExternalLinks] = useState([]); const [internalLinks, setInternalLinks] = useState([]); @@ -95,14 +80,26 @@ const UpdatedDatasetDetailPage: React.FC = () => { const [isExternalExpanded, setIsExternalExpanded] = useState(true); const [jsonSize, setJsonSize] = useState(0); const [previewIndex, setPreviewIndex] = useState(0); - const [copiedToast, setCopiedToast] = useState<{ - open: boolean; - text: string; - }>({ - open: false, - text: "", - }); + const [isPreviewLoading, setIsPreviewLoading] = useState(false); + // const [copiedToast, setCopiedToast] = useState<{ + // open: boolean; + // text: string; + // }>({ + // open: false, + // text: "", + // }); + // const [copiedUrlOpen, setCopiedUrlOpen] = useState(false); + const [copiedKey, setCopiedKey] = useState(null); + const copyTimer = useRef(null); const aiSummary = datasetDocument?.[".datainfo"]?.AISummary ?? ""; + const handleSelectRevision = (newRev?: string | null) => { + setSearchParams((prev) => { + const p = new URLSearchParams(prev); // copy of the query url + if (newRev) p.set("rev", newRev); + else p.delete("rev"); + return p; + }); + }; const linkMap = useMemo(() => makeLinkMap(externalLinks), [externalLinks]); // => external Link Map @@ -112,18 +109,17 @@ const UpdatedDatasetDetailPage: React.FC = () => { ); const treeTitle = "Files"; - const filesCount = externalLinks.length; - const totalBytes = useMemo(() => { - let bytes = 0; - for (const l of externalLinks) { - const m = l.url.match(/size=(\d+)/); - if (m) bytes += parseInt(m[1], 10); - } - return bytes; - }, [externalLinks]); + // const filesCount = externalLinks.length; + // const totalBytes = useMemo(() => { + // let bytes = 0; + // for (const l of externalLinks) { + // const m = l.url.match(/size=(\d+)/); + // if (m) bytes += parseInt(m[1], 10); + // } + // return bytes; + // }, [externalLinks]); // add spinner - const [isPreviewLoading, setIsPreviewLoading] = useState(false); const formatSize = (sizeInBytes: number): string => { if (sizeInBytes < 1024) { @@ -164,7 +160,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { links.push({ name: `${label} (${size}) [/${subpath}]`, size, - path: currentPath, // keep full JSON path for file placement + path: currentPath, // parent path (not include _DataLink_) url: correctedUrl, index: links.length, }); @@ -208,7 +204,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { obj.MeshNode?.hasOwnProperty("_ArrayZipData_") && typeof obj.MeshNode["_ArrayZipData_"] === "string" ) { - // console.log("path", path); internalLinks.push({ name: "JMesh", data: obj, @@ -257,7 +252,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { }); } } - return internalLinks; }; // Build a shareable preview URL for a JSON path in this dataset @@ -274,7 +268,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { const url = buildPreviewUrl(path); try { await navigator.clipboard.writeText(url); - setCopiedToast({ open: true, text: "Preview link copied" }); + // setCopiedToast({ open: true, text: "Preview link copied" }); } catch { // fallback const ta = document.createElement("textarea"); @@ -283,20 +277,31 @@ const UpdatedDatasetDetailPage: React.FC = () => { ta.select(); document.execCommand("copy"); document.body.removeChild(ta); - setCopiedToast({ open: true, text: "Preview link copied" }); + // setCopiedToast({ open: true, text: "Preview link copied" }); } }; - // useEffect(() => { - // const fetchData = async () => { - // if (dbName && docId) { - // await dispatch(fetchDocumentDetails({ dbName, docId })); - // await dispatch(fetchDbInfoByDatasetId({ dbName, docId })); - // } - // }; + // const handleUrlCopyClick = async (e: React.MouseEvent, path: string) => { + // await copyPreviewUrl(path); + // setCopiedUrlOpen(true); + // setTimeout(() => setCopiedUrlOpen(false), 2500); + // }; + + const handleUrlCopyClick = async ( + e: React.MouseEvent, + path: string + ) => { + await copyPreviewUrl(path); + setCopiedKey(path); // mark this button as "copied" + if (copyTimer.current) clearTimeout(copyTimer.current); + copyTimer.current = window.setTimeout(() => setCopiedKey(null), 1500); + }; - // fetchData(); - // }, [dbName, docId, dispatch]); + React.useEffect(() => { + return () => { + if (copyTimer.current) clearTimeout(copyTimer.current); + }; + }, []); useEffect(() => { if (!dbName || !docId) return; @@ -447,14 +452,14 @@ const UpdatedDatasetDetailPage: React.FC = () => { idx: number, isInternal: boolean = false ) => { - console.log( - "🟢 Preview button clicked for:", - dataOrUrl, - "Index:", - idx, - "Is Internal:", - isInternal - ); + // console.log( + // "🟢 Preview button clicked for:", + // dataOrUrl, + // "Index:", + // idx, + // "Is Internal:", + // isInternal + // ); // Clear any stale preview type from last run delete (window as any).__previewType; @@ -593,7 +598,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { }, [datasetDocument] ); - + // check if the url has preview param useEffect(() => { const p = searchParams.get("preview"); if (!p || !datasetDocument) return; @@ -618,8 +623,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { externalLinks, searchParams, internalMap, - // externalMap, - linkMap, + linkMap, // externalMap ]); const handleClosePreview = () => { @@ -897,12 +901,12 @@ const UpdatedDatasetDetailPage: React.FC = () => { @@ -1002,83 +1006,109 @@ const UpdatedDatasetDetailPage: React.FC = () => { }} > {internalLinks.length > 0 ? ( - internalLinks.map((link, index) => ( - - { + const key = link.path; + return ( + - {link.name}{" "} - {link.arraySize ? `[${link.arraySize.join("x")}]` : ""} - - - - {/* */} + {link.name}{" "} + {link.arraySize + ? `[${link.arraySize.join("x")}]` + : ""} + + + + {/* */} + + {/* */} + - - )) + ); + }) ) : ( No internal data found. @@ -1132,6 +1162,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { > {externalLinks.length > 0 ? ( externalLinks.map((link, index) => { + const key = link.path; const match = link.url.match(/file=([^&]+)/); const fileName = match ? match[1] : ""; const isPreviewable = @@ -1210,7 +1241,12 @@ const UpdatedDatasetDetailPage: React.FC = () => { Preview )} - {/* {isPreviewable && ( + {isPreviewable && ( + // - )} */} + // + )} ); @@ -1287,7 +1339,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { - {/* Preview Modal Component - Add Here */} + {/* Preview Modal Component */}