diff --git a/public/img/search_page/search.png b/public/img/search_page/search.png new file mode 100644 index 0000000..65bc86d Binary files /dev/null and b/public/img/search_page/search.png differ diff --git a/src/components/DatasetDetailPage/MetaDataPanel.tsx b/src/components/DatasetDetailPage/MetaDataPanel.tsx new file mode 100644 index 0000000..b4b7573 --- /dev/null +++ b/src/components/DatasetDetailPage/MetaDataPanel.tsx @@ -0,0 +1,290 @@ +import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight"; +import { + Box, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + Chip, + IconButton, + Tooltip, +} from "@mui/material"; +import { Colors } from "design/theme"; +import React, { useMemo, useState } from "react"; + +type Props = { + dbViewInfo: any; + datasetDocument: any; + dbName: string | undefined; + docId: string | undefined; + // NEW: + currentRev?: string; // from URL (?rev=...) + onChangeRev?: (rev?: string | null) => void; // to update URL + revsList?: { rev: string }[]; +}; + +type RevInfo = { rev: string }; + +const MetaDataPanel: React.FC = ({ + dbViewInfo, + datasetDocument, + dbName, + docId, + currentRev, + onChangeRev, + revsList = [], // default empty +}) => { + // const revs: RevInfo[] = useMemo( + // () => + // Array.isArray(datasetDocument?.["_revs_info"]) + // ? (datasetDocument!["_revs_info"] as RevInfo[]) + // : [], + // [datasetDocument] + // ); + const revs = revsList; + + // derive index from currentRev; fallback to 0 (latest) + const deriveIdx = React.useCallback((revList: RevInfo[], cur?: string) => { + if (!revList.length) return 0; + if (!cur) return 0; + const idx = revList.findIndex((r) => r.rev === cur); + return idx >= 0 ? idx : 0; + }, []); + + const [revIdx, setRevIdx] = useState(deriveIdx(revs, currentRev)); + + // keep local idx synced when URL rev or list changes + React.useEffect(() => { + setRevIdx(deriveIdx(revs, currentRev)); + }, [revs, currentRev, deriveIdx]); + + const selected = revs[revIdx]; + // const [revIdx, setRevIdx] = useState(0); + // const selected = revs[revIdx]; + + return ( + + + + + Modalities + + + {dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "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 + + + {dbViewInfo?.rows?.[0]?.value?.subj?.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"} + + + + {revs.length > 0 && ( + + + Revisions + + + + Select revision + + + + {selected && ( + + + + Selected rev: + + + {selected.rev} + + + + + window.open( + `https://neurojson.io:7777/${dbName}/${docId}?rev=${selected.rev}`, + "_blank" + ) + } + > + + + + + )} + + )} + + + ); +}; + +export default MetaDataPanel; diff --git a/src/components/DatasetPageCard.tsx b/src/components/DatasetPageCard.tsx index c3d9d17..e7aff0b 100644 --- a/src/components/DatasetPageCard.tsx +++ b/src/components/DatasetPageCard.tsx @@ -15,6 +15,24 @@ import { useNavigate } from "react-router-dom"; import { Row } from "redux/neurojson/types/neurojson.interface"; import RoutesEnum from "types/routes.enum"; +const formatSize = (sizeInBytes: number): string => { + if (sizeInBytes < 1024) { + return `${sizeInBytes} Bytes`; + } else if (sizeInBytes < 1024 * 1024) { + return `${(sizeInBytes / 1024).toFixed(1)} KB`; + } else if (sizeInBytes < 1024 * 1024 * 1024) { + return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`; + } else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) { + return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } else { + return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`; + } +}; + +// for showing the size +const jsonBytes = (obj: unknown) => + obj ? new TextEncoder().encode(JSON.stringify(obj)).length : 0; + interface DatasetPageCardProps { doc: Row; index: number; @@ -32,6 +50,11 @@ const DatasetPageCard: React.FC = ({ }) => { const navigate = useNavigate(); const datasetIndex = (page - 1) * pageSize + index + 1; + const sizeInBytes = React.useMemo(() => { + const len = (doc as any)?.value?.length; // bytes from length key + if (typeof len === "number" && Number.isFinite(len)) return len; + return jsonBytes(doc.value); // fallback: summary object size + }, [doc.value]); return ( = ({ Size:{" "} - {doc.value.length + {/* {doc.value.length ? `${(doc.value.length / 1024 / 1024).toFixed(2)} MB` - : "Unknown"} + : "Unknown"} */} + {formatSize(sizeInBytes)} {doc.value.info?.DatasetDOI && ( diff --git a/src/components/SearchPage/ClickTooltip.tsx b/src/components/SearchPage/ClickTooltip.tsx new file mode 100644 index 0000000..4fb423b --- /dev/null +++ b/src/components/SearchPage/ClickTooltip.tsx @@ -0,0 +1,40 @@ +import { Box, Tooltip, ClickAwayListener, TooltipProps } from "@mui/material"; +import { useState, PropsWithChildren } from "react"; + +type ClickTooltipProps = PropsWithChildren<{ + title: TooltipProps["title"]; + placement?: TooltipProps["placement"]; + componentsProps?: TooltipProps["componentsProps"]; +}>; + +export default function ClickTooltip({ + title, + placement = "right", + componentsProps, + children, +}: ClickTooltipProps) { + const [open, setOpen] = useState(false); + const toggle = () => setOpen((o) => !o); + const close = () => setOpen(false); + + return ( + + + + {/* span to ensure Tooltip always has a single DOM child */} + {children} + + + + ); +} diff --git a/src/components/SearchPage/DatabaseCard.tsx b/src/components/SearchPage/DatabaseCard.tsx new file mode 100644 index 0000000..f1525e8 --- /dev/null +++ b/src/components/SearchPage/DatabaseCard.tsx @@ -0,0 +1,195 @@ +import { + Box, + Typography, + Chip, + Card, + CardContent, + Stack, + Avatar, +} from "@mui/material"; +import { Colors } from "design/theme"; +import React from "react"; +import { Link } from "react-router-dom"; +import RoutesEnum from "types/routes.enum"; +import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels"; + +type Props = { + dbId?: string; + fullName?: string; + datasets?: number; + modalities?: string[]; + logo?: string; + keyword?: string; + onChipClick: (key: string, value: string) => void; +}; + +const DatabaseCard: React.FC = ({ + dbId, + fullName, + datasets, + modalities, + logo, + keyword, + onChipClick, +}) => { + const databaseLink = `${RoutesEnum.DATABASES}/${dbId}`; + // keyword hightlight functional component + const highlightKeyword = (text: string, keyword?: string) => { + if (!keyword || !text?.toLowerCase().includes(keyword.toLowerCase())) { + return text; + } + + const regex = new RegExp(`(${keyword})`, "gi"); // for case-insensitive and global + const parts = text.split(regex); + + return ( + <> + {parts.map((part, i) => + part.toLowerCase() === keyword.toLowerCase() ? ( + + {part} + + ) : ( + {part} + ) + )} + + ); + }; + + // for datatype rendering + const isClickableModality = (raw: string) => + !!modalityValueToEnumLabel[raw.toLowerCase()]; + + const renderDatatype = (raw: string, idx: number) => { + const key = raw.toLowerCase(); + const label = modalityValueToEnumLabel[key]; + + if (label) { + // Clickable modality → drives the "modality" filter + return ( + onChipClick("modality", key)} // pass normalized key + sx={{ + "& .MuiChip-label": { px: "6px", fontSize: "0.8rem" }, + height: 24, + color: Colors.darkPurple, + border: `1px solid ${Colors.darkPurple}`, + fontWeight: "bold", + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: `${Colors.purple} !important`, + color: "white", + borderColor: Colors.purple, + }, + }} + /> + ); + } + + // Not a modality → render as plain text (or a disabled/outlined chip if you prefer) + return ( + + {raw} + + ); + }; + + return ( + + + + + + + {logo && ( + + )} + + + {highlightKeyword(fullName || "Untitled Database", keyword)} + + + + + + + Data Type: + + + {Array.isArray(modalities) && modalities.length > 0 ? ( + modalities.map(renderDatatype) + ) : ( + + N/A + + )} + + + + + Datasets: {datasets ?? "N/A"} + + + + + + + + ); +}; + +export default DatabaseCard; diff --git a/src/pages/DatasetPage.tsx b/src/pages/DatasetPage.tsx index 978eb6d..21cd777 100644 --- a/src/pages/DatasetPage.tsx +++ b/src/pages/DatasetPage.tsx @@ -39,6 +39,20 @@ const DatasetPage: React.FC = () => { const totalPages = Math.ceil(limit / pageSize); const [searchQuery, setSearchQuery] = useState(""); + const formatSize = (sizeInBytes: number): string => { + if (sizeInBytes < 1024) { + return `${sizeInBytes} Bytes`; + } else if (sizeInBytes < 1024 * 1024) { + return `${(sizeInBytes / 1024).toFixed(1)} KB`; + } else if (sizeInBytes < 1024 * 1024 * 1024) { + return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`; + } else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) { + return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } else { + return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`; + } + }; + useEffect(() => { if (dbName) { dispatch(fetchDbInfo(dbName.toLowerCase())); @@ -326,11 +340,12 @@ const DatasetPage: React.FC = () => { {filteredData.map((doc: any, index: number) => { const datasetIndex = (currentPage - 1) * pageSize + index + 1; + return ( { + if (!keyword) return false; + const needle = keyword.toLowerCase(); + return ( + item.name?.toLowerCase().includes(needle) || + item.fullname?.toLowerCase().includes(needle) || + item.datatype?.some((dt) => dt.toLowerCase().includes(needle)) + ); +}; + const SearchPage: React.FC = () => { const dispatch = useAppDispatch(); const [hasSearched, setHasSearched] = useState(false); @@ -53,6 +77,32 @@ const SearchPage: React.FC = () => { const [showMobileFilters, setShowMobileFilters] = useState(false); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + + const placement = upMd ? "right" : "top"; + + // for database card + const keywordInput = String(formData?.keyword ?? "").trim(); + const selectedDbId = String(formData?.database ?? "").trim(); + + const registryMatches: RegistryItem[] = React.useMemo(() => { + if (!Array.isArray(registry)) return []; + const list = registry as RegistryItem[]; + + const fromId = + selectedDbId && selectedDbId !== "any" + ? list.filter((r) => r.id === selectedDbId) + : []; + + const fromKeyword = keywordInput + ? list.filter((r) => matchesKeyword(r, keywordInput)) + : []; + + // merge the db results of selectedDB and keywordInput --> de duplicates + const map = new Map(); + [...fromId, ...fromKeyword].forEach((r) => map.set(r.id, r)); + return Array.from(map.values()); // return matched registry + }, [registry, selectedDbId, keywordInput]); // to show the applied chips on the top of results const activeFilters = Object.entries(appliedFilters).filter( @@ -248,6 +298,7 @@ const SearchPage: React.FC = () => { { }, }} > - Submit + Search + + )} + + + - Load Extra 50 Results - + /> - ) - : null} - - - - - {results.length > 0 && - paginatedResults.length > 0 && - paginatedResults.map((item, idx) => { - try { - const parsedJson = JSON.parse(item.json); - const globalIndex = (page - 1) * itemsPerPage + idx; - - const isDataset = - parsedJson?.value?.subj && - Array.isArray(parsedJson.value.subj); - - return isDataset ? ( - - ) : ( - - ); - } catch (e) { - console.error( - `Failed to parse JSON for item #${idx}`, - e - ); - return null; - } - })} - - ) : ( - - {results?.msg === "empty output" - ? "No results found based on your criteria. Please adjust the filters and try again." - : "Something went wrong. Please try again later."} - - )} - - )} + {results.length > 0 && + paginatedResults.length > 0 && + paginatedResults.map((item, idx) => { + try { + const parsedJson = JSON.parse(item.json); + const globalIndex = + (page - 1) * itemsPerPage + idx; + + const isDataset = + parsedJson?.value?.subj && + Array.isArray(parsedJson.value.subj); + + return isDataset ? ( + + ) : ( + + ); + } catch (e) { + console.error( + `Failed to parse JSON for item #${idx}`, + e + ); + return null; + } + })} + + )} + + {/* Single place to show the red message */} + {showNoResults && ( + + No results found based on your criteria. Please adjust + the filters and try again. + + )} + + {hasSearched && + !loading && + !Array.isArray(results) && + results?.msg !== "empty output" && ( + + Something went wrong. Please try again later. + + )} + + )} + + )} + {/* mobile version filters */} diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index c7b2af3..feb8b8e 100644 --- a/src/pages/UpdatedDatasetDetailPage.tsx +++ b/src/pages/UpdatedDatasetDetailPage.tsx @@ -3,7 +3,6 @@ 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 HomeIcon from "@mui/icons-material/Home"; import { Box, @@ -20,18 +19,20 @@ import { makeLinkMap, } from "components/DatasetDetailPage/FileTree/utils"; import LoadDatasetTabs from "components/DatasetDetailPage/LoadDatasetTabs"; +import MetaDataPanel from "components/DatasetDetailPage/MetaDataPanel"; 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 ReactJson from "react-json-view"; -import { useParams, useNavigate } from "react-router-dom"; +// import ReactJson from "react-json-view"; +import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { fetchDocumentDetails, fetchDbInfoByDatasetId, } from "redux/neurojson/neurojson.action"; import { NeurojsonSelector } from "redux/neurojson/neurojson.selector"; +import { NeurojsonService } from "services/neurojson.service"; import RoutesEnum from "types/routes.enum"; interface ExternalDataLink { @@ -53,6 +54,19 @@ interface InternalDataLink { const UpdatedDatasetDetailPage: React.FC = () => { const { dbName, docId } = useParams<{ dbName: string; docId: string }>(); const navigate = useNavigate(); + // for revision + const [searchParams, setSearchParams] = useSearchParams(); + const rev = searchParams.get("rev") || undefined; + + const handleSelectRevision = (newRev?: string | null) => { + setSearchParams((prev) => { + const p = new URLSearchParams(prev); + if (newRev) p.set("rev", newRev); + else p.delete("rev"); + return p; + }); + }; + const dispatch = useAppDispatch(); const { selectedDocument: datasetDocument, @@ -73,11 +87,16 @@ const UpdatedDatasetDetailPage: React.FC = () => { const [previewIndex, setPreviewIndex] = useState(0); const aiSummary = datasetDocument?.[".datainfo"]?.AISummary ?? ""; - // 1) detect subjects at the top level, return true or false - const hasTopLevelSubjects = useMemo( - () => Object.keys(datasetDocument || {}).some((k) => /^sub-/i.test(k)), - [datasetDocument] - ); + // useEffect(() => { + // if (!datasetDocument) { + // setJsonSize(0); + // return; + // } + // const bytes = new TextEncoder().encode( + // JSON.stringify(datasetDocument) + // ).length; + // setJsonSize(bytes); + // }, [datasetDocument]); const linkMap = useMemo(() => makeLinkMap(externalLinks), [externalLinks]); @@ -86,26 +105,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { [datasetDocument, linkMap] ); - // “rest” JSON only when we actually have subjects - const rest = useMemo(() => { - if (!datasetDocument || !hasTopLevelSubjects) return {}; - const r: any = {}; - Object.keys(datasetDocument).forEach((k) => { - if (!/^sub-/i.test(k)) r[k] = (datasetDocument as any)[k]; - }); - return r; - }, [datasetDocument, hasTopLevelSubjects]); - - // JSON panel should always render: - // - 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] - ); - - // 5) header title + counts also fall back - // const treeTitle = hasTopLevelSubjects ? "Subjects" : "Files"; const treeTitle = "Files"; const filesCount = externalLinks.length; const totalBytes = useMemo(() => { @@ -256,16 +255,36 @@ const UpdatedDatasetDetailPage: React.FC = () => { return internalLinks; }; + // useEffect(() => { + // const fetchData = async () => { + // if (dbName && docId) { + // await dispatch(fetchDocumentDetails({ dbName, docId })); + // await dispatch(fetchDbInfoByDatasetId({ dbName, docId })); + // } + // }; + + // fetchData(); + // }, [dbName, docId, dispatch]); + useEffect(() => { - const fetchData = async () => { - if (dbName && docId) { - await dispatch(fetchDocumentDetails({ dbName, docId })); - await dispatch(fetchDbInfoByDatasetId({ dbName, docId })); - } - }; + if (!dbName || !docId) return; + + (async () => { + await dispatch(fetchDocumentDetails({ dbName, docId, rev })); // for dataset detail + dispatch(fetchDbInfoByDatasetId({ dbName, docId })); // for metadata panel (include modality) + })(); + }, [dbName, docId, rev, dispatch]); + // for revs list storage + const [revsList, setRevsList] = React.useState<{ rev: string }[]>([]); - fetchData(); - }, [dbName, docId, dispatch]); + useEffect(() => { + const fromDoc = Array.isArray(datasetDocument?._revs_info) + ? (datasetDocument._revs_info as { rev: string }[]) + : []; + if (fromDoc.length && revsList.length === 0) { + setRevsList(fromDoc); + } + }, [datasetDocument, revsList.length]); useEffect(() => { if (datasetDocument) { @@ -278,6 +297,15 @@ const UpdatedDatasetDetailPage: React.FC = () => { }) ); + const bytes = new Blob([JSON.stringify(datasetDocument)], { + type: "application/json", + }); + setJsonSize(bytes.size); + + // const bytes = new TextEncoder().encode( + // JSON.stringify(datasetDocument) + // ).length; + // setJsonSize(bytes); // Extract Internal Data & Assign `index` const internalData = extractInternalData(datasetDocument).map( (data, index) => ({ @@ -317,10 +345,10 @@ const UpdatedDatasetDetailPage: React.FC = () => { } }); - const blob = new Blob([JSON.stringify(datasetDocument, null, 2)], { - type: "application/json", - }); - setJsonSize(blob.size); + // const blob = new Blob([JSON.stringify(datasetDocument, null, 2)], { + // type: "application/json", + // }); + // setJsonSize(blob.size); // Construct download script dynamically let script = `curl -L --create-dirs "https://neurojson.io:7777/${dbName}/${docId}" -o "${docId}.json"\n`; @@ -402,18 +430,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { if (typeof window !== "undefined" && (window as any).__previewType) { 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); - // if (typeof obj === "string" && obj.includes("db=optics-at-martinos")) { - // return false; - // } - // if (typeof obj === "string" && obj.endsWith(".jdb")) { - // return true; - // } + if (!obj || typeof obj !== "object") { return false; } @@ -422,8 +439,6 @@ const UpdatedDatasetDetailPage: React.FC = () => { return false; } const dim = obj._ArraySize_; - // console.log("array.isarray(dim)", Array.isArray(dim)); - // console.log("dim.length", dim.length === 1 || dim.length === 2); return ( Array.isArray(dim) && @@ -500,18 +515,6 @@ 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"); - // 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 - // } } else { console.warn("⚠️ Unsupported file format for preview:", dataOrUrl); } @@ -856,137 +859,16 @@ const UpdatedDatasetDetailPage: React.FC = () => { flexDirection: "column", }} > - - - - - Modalities - - - {dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? - "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"} - - - - + @@ -1089,12 +971,15 @@ const UpdatedDatasetDetailPage: React.FC = () => { variant="contained" size="small" sx={{ - backgroundColor: "#1976d2", + backgroundColor: Colors.purple, flexShrink: 0, minWidth: "70px", fontSize: "0.7rem", padding: "2px 6px", lineHeight: 1, + "&:hover": { + backgroundColor: Colors.secondaryPurple, + }, }} onClick={() => handlePreview(link.data, link.index, true) @@ -1114,7 +999,7 @@ const UpdatedDatasetDetailPage: React.FC = () => { { variant="contained" size="small" sx={{ - backgroundColor: "#1976d2", + backgroundColor: Colors.purple, minWidth: "70px", fontSize: "0.7rem", padding: "2px 6px", lineHeight: 1, + "&:hover": { + backgroundColor: Colors.secondaryPurple, + }, }} onClick={() => window.open(link.url, "_blank")} > @@ -1214,10 +1102,16 @@ const UpdatedDatasetDetailPage: React.FC = () => { variant="outlined" size="small" sx={{ + color: Colors.purple, + borderColor: Colors.purple, minWidth: "65px", fontSize: "0.7rem", padding: "2px 6px", lineHeight: 1, + "&:hover": { + color: Colors.secondaryPurple, + borderColor: Colors.secondaryPurple, + }, }} onClick={() => handlePreview(link.url, link.index, false) diff --git a/src/redux/neurojson/neurojson.action.ts b/src/redux/neurojson/neurojson.action.ts index 813cd29..c050ad8 100644 --- a/src/redux/neurojson/neurojson.action.ts +++ b/src/redux/neurojson/neurojson.action.ts @@ -69,14 +69,15 @@ export const loadAllDocuments = createAsyncThunk( } ); +// fetch dataset detail for dataset detail page export const fetchDocumentDetails = createAsyncThunk( "neurojson/fetchDocumentDetails", async ( - { dbName, docId }: { dbName: string; docId: string }, + { dbName, docId, rev }: { dbName: string; docId: string; rev?: string }, { rejectWithValue } ) => { try { - const data = await NeurojsonService.getDocumentById(dbName, docId); + const data = await NeurojsonService.getDocumentById(dbName, docId, rev); return data; } catch (error: any) { return rejectWithValue("Failed to fetch document details."); @@ -108,6 +109,7 @@ export const fetchMetadataSearchResults = createAsyncThunk( } ); +// fetch data for metadata panel in dataset detail page export const fetchDbInfoByDatasetId = createAsyncThunk( "neurojson/fetchDbInfoByDatasetId", async ( diff --git a/src/services/neurojson.service.ts b/src/services/neurojson.service.ts index 02aa590..fe6859d 100644 --- a/src/services/neurojson.service.ts +++ b/src/services/neurojson.service.ts @@ -28,9 +28,22 @@ export const NeurojsonService = { return response.data; }, - getDocumentById: async (dbName: string, documentId: string): Promise => { + getDocumentById: async ( + dbName: string, + documentId: string, + rev?: string + ): Promise => { try { - const response = await api.get(`${baseURL}/${dbName}/${documentId}`); + const url = `${baseURL}/${dbName}/${documentId}`; + // const response = await api.get( + // `${baseURL}/${dbName}/${documentId}?revs_info=true` + // ); + const response = await api.get(url, { + params: { + revs_info: true, + ...(rev ? { rev } : {}), // add ?rev=... only when provided + }, + }); return response.data; } catch (error) { console.error( diff --git a/src/utils/SearchPageFunctions/modalityLabels.ts b/src/utils/SearchPageFunctions/modalityLabels.ts index b4c6f9a..532c3be 100644 --- a/src/utils/SearchPageFunctions/modalityLabels.ts +++ b/src/utils/SearchPageFunctions/modalityLabels.ts @@ -1,6 +1,8 @@ export const modalityValueToEnumLabel: Record = { anat: "Structural MRI (anat)", + mri: "Structural MRI (anat)", func: "fMRI (func)", + fmri: "fMRI (func)", dwi: "DWI (dwi)", fmap: "Field maps (fmap)", perf: "Perfusion (perf)", @@ -11,5 +13,6 @@ export const modalityValueToEnumLabel: Record = { pet: "PET (pet)", micr: "microscopy (micr)", nirs: "fNIRS (nirs)", + fnirs: "fNIRS (nirs)", motion: "motion (motion)", };