From 9c2e258acfeebadda1e800d5aacc85c58fc7ef57 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 29 Sep 2025 12:32:58 -0400 Subject: [PATCH 1/4] feat: highlight and scroll for any row via focus param --- .../FileTree/FileTreeRow.tsx | 26 +++++++++---------- src/components/SearchPage/SubjectCard.tsx | 9 ++----- src/pages/UpdatedDatasetDetailPage.tsx | 11 +++----- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index e9ee706..5854192 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -124,25 +124,15 @@ const FileTreeRow: React.FC = ({ 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); + // const isSubjectFolder = + // node.kind === "folder" && /^sub-[A-Za-z0-9]+$/i.test(node.name); 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 +284,15 @@ const FileTreeRow: React.FC = ({ // if the node is a file return ( = ({ }) => { 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..80bcdc6 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -57,13 +57,8 @@ 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 highlight + const focus = searchParams.get("focus") || undefined; // for revision const rev = searchParams.get("rev") || undefined; @@ -902,7 +897,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { onPreview={handlePreview} // pass the function down to FileTree getInternalByPath={getInternalByPath} getJsonByPath={getJsonByPath} - highlightText={focusSubj} // for subject highlight + highlightText={focus} // for highlight /> From eaa3675ee17065c4cc2d0e62007a60d0cce1e524 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 29 Sep 2025 12:45:38 -0400 Subject: [PATCH 2/4] feat: simplify the isExactHit logic to highlightText comparison --- src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx index 5854192..4aceee8 100644 --- a/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx +++ b/src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx @@ -123,12 +123,10 @@ 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 && - node.name.trim().toLowerCase() === highlightText.trim().toLowerCase(); + node.name.trim().toLowerCase() === + (highlightText ?? "").trim().toLowerCase(); React.useEffect(() => { if (isExactHit && rowRef.current) { From cf874ec5f590e75fe5323d87fcd72d2a70efa94d Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Wed, 1 Oct 2025 12:01:27 -0400 Subject: [PATCH 3/4] feat: add shareable copy url button to previewable data --- .../DatasetDetailPage/FileTree/FileTree.tsx | 22 ++--- .../DatasetDetailPage/FileTree/utils.ts | 20 ++++- src/pages/UpdatedDatasetDetailPage.tsx | 89 +++++++++---------- 3 files changed, 70 insertions(+), 61 deletions(-) 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/utils.ts b/src/components/DatasetDetailPage/FileTree/utils.ts index 4f0c109..835ab78 100644 --- a/src/components/DatasetDetailPage/FileTree/utils.ts +++ b/src/components/DatasetDetailPage/FileTree/utils.ts @@ -87,8 +87,15 @@ export const buildTreeFromDoc = ( if (Array.isArray(doc)) { doc.forEach((item, i) => { 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/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 80bcdc6..0dfd819 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -13,7 +13,7 @@ import { Alert, Button, Collapse, - Snackbar, + Tooltip, } from "@mui/material"; import FileTree from "components/DatasetDetailPage/FileTree/FileTree"; import { @@ -56,22 +56,6 @@ interface InternalDataLink { const UpdatedDatasetDetailPage: React.FC = () => { const { dbName, docId } = useParams<{ dbName: string; docId: string }>(); const navigate = useNavigate(); - const [searchParams, setSearchParams] = useSearchParams(); - // for highlight - const focus = searchParams.get("focus") || undefined; - - // 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, @@ -79,6 +63,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([]); @@ -90,6 +78,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { const [isExternalExpanded, setIsExternalExpanded] = useState(true); const [jsonSize, setJsonSize] = useState(0); const [previewIndex, setPreviewIndex] = useState(0); + const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [copiedToast, setCopiedToast] = useState<{ open: boolean; text: string; @@ -98,6 +87,14 @@ const UpdatedDatasetDetailPage: React.FC = () => { text: "", }); 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 @@ -107,18 +104,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) { @@ -159,7 +155,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, }); @@ -203,7 +199,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { obj.MeshNode?.hasOwnProperty("_ArrayZipData_") && typeof obj.MeshNode["_ArrayZipData_"] === "string" ) { - // console.log("path", path); internalLinks.push({ name: "JMesh", data: obj, @@ -252,7 +247,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { }); } } - return internalLinks; }; // Build a shareable preview URL for a JSON path in this dataset @@ -442,14 +436,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; @@ -588,7 +582,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { }, [datasetDocument] ); - + // check if the url has preview param useEffect(() => { const p = searchParams.get("preview"); if (!p || !datasetDocument) return; @@ -613,8 +607,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { externalLinks, searchParams, internalMap, - // externalMap, - linkMap, + linkMap, // externalMap ]); const handleClosePreview = () => { @@ -892,8 +885,8 @@ const UpdatedDatasetDetailPage: React.FC = () => { { > Preview - {/* */} + )) @@ -1205,7 +1198,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { Preview )} - {/* {isPreviewable && ( + {isPreviewable && ( - )} */} + )} ); @@ -1282,7 +1275,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { - {/* Preview Modal Component - Add Here */} + {/* Preview Modal Component */} Date: Thu, 2 Oct 2025 18:09:26 -0400 Subject: [PATCH 4/4] feat: change copy button to show copied state after click with auto-revert; closes #98 --- src/pages/UpdatedDatasetDetailPage.tsx | 246 ++++++++++++++++--------- 1 file changed, 155 insertions(+), 91 deletions(-) diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 0dfd819..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"; @@ -14,6 +15,7 @@ import { Button, Collapse, 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 { @@ -79,13 +81,16 @@ const UpdatedDatasetDetailPage: React.FC = () => { const [jsonSize, setJsonSize] = useState(0); const [previewIndex, setPreviewIndex] = useState(0); const [isPreviewLoading, setIsPreviewLoading] = useState(false); - const [copiedToast, setCopiedToast] = useState<{ - open: boolean; - text: string; - }>({ - open: false, - text: "", - }); + // 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) => { @@ -263,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"); @@ -272,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); + // }; - // fetchData(); - // }, [dbName, docId, dispatch]); + 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); + }; + + React.useEffect(() => { + return () => { + if (copyTimer.current) clearTimeout(copyTimer.current); + }; + }, []); useEffect(() => { if (!dbName || !docId) return; @@ -990,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. @@ -1120,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 = @@ -1199,6 +1242,11 @@ const UpdatedDatasetDetailPage: React.FC = () => { )} {isPreviewable && ( + // + // )}