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 index ad63381..b4b7573 100644 --- a/src/components/DatasetDetailPage/MetaDataPanel.tsx +++ b/src/components/DatasetDetailPage/MetaDataPanel.tsx @@ -18,6 +18,10 @@ type Props = { 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 }; @@ -27,16 +31,37 @@ const MetaDataPanel: React.FC = ({ datasetDocument, dbName, docId, + currentRev, + onChangeRev, + revsList = [], // default empty }) => { - const revs: RevInfo[] = useMemo( - () => - Array.isArray(datasetDocument?.["_revs_info"]) - ? (datasetDocument!["_revs_info"] as RevInfo[]) - : [], - [datasetDocument] - ); - const [revIdx, setRevIdx] = useState(0); + // 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 ( = ({ labelId="rev-select-label" label="Select revision" value={revIdx} - onChange={(e) => setRevIdx(Number(e.target.value))} + onChange={(e) => { + const idx = Number(e.target.value); + setRevIdx(idx); + const chosen = revs[idx]?.rev; + // update URL -> parent will refetch with ?rev=chosen + onChangeRev?.(chosen || null); + }} + // onChange={(e) => setRevIdx(Number(e.target.value))} > {revs.map((r, idx) => { const [verNum, hash] = r.rev.split("-", 2); 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 index 0a76afb..f1525e8 100644 --- a/src/components/SearchPage/DatabaseCard.tsx +++ b/src/components/SearchPage/DatabaseCard.tsx @@ -102,7 +102,18 @@ const DatabaseCard: React.FC = ({ }; return ( - + = ({ gap: 2, }} > - {/* Logo as Avatar */} - - {logo && ( - + + + {logo && ( + + )} + + - )} - - {/* database card */} - - - Database:{" "} - {highlightKeyword(fullName || "Untitled Database", keyword)} - - + component={Link} + to={databaseLink} + target="_blank" + > + {highlightKeyword(fullName || "Untitled Database", keyword)} + + + + = ({ {Array.isArray(modalities) && modalities.length > 0 ? ( modalities.map(renderDatatype) ) : ( - // ( - // modalities.map((mod, idx) => ( - // onChipClick("modality", mod)} - // sx={{ - // "& .MuiChip-label": { - // paddingX: "6px", - // fontSize: "0.8rem", - // }, - // height: "24px", - // 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, - // }, - // }} - // /> - // )) - // ) N/A diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index 6ccf638..ef303a2 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -1,5 +1,6 @@ import { generateSchemaWithDatabaseEnum } from "../utils/SearchPageFunctions/searchformSchema"; import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import { Typography, Container, @@ -9,11 +10,14 @@ import { Pagination, Chip, Drawer, + Tooltip, + IconButton, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import Form from "@rjsf/mui"; import validator from "@rjsf/validator-ajv8"; +import ClickTooltip from "components/SearchPage/ClickTooltip"; import DatabaseCard from "components/SearchPage/DatabaseCard"; import DatasetCard from "components/SearchPage/DatasetCard"; import SubjectCard from "components/SearchPage/SubjectCard"; @@ -73,18 +77,13 @@ 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(); - console.log("keyword", keywordInput); - - // const registryMatches: RegistryItem[] = React.useMemo(() => { - // if (!Array.isArray(registry) || !keywordInput) return []; - // return (registry as RegistryItem[]).filter((r) => - // matchesKeyword(r, keywordInput) - // ); - // }, [registry, keywordInput]); const registryMatches: RegistryItem[] = React.useMemo(() => { if (!Array.isArray(registry)) return []; @@ -299,6 +298,7 @@ const SearchPage: React.FC = () => { { }, }} > - Submit + Search - - ) - : 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."} - - )} - - )} */} {/* mobile version filters */} diff --git a/src/pages/UpdatedDatasetDetailPage.tsx b/src/pages/UpdatedDatasetDetailPage.tsx index 0e946e7..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, @@ -27,12 +26,13 @@ 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 { 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 { @@ -54,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, @@ -257,10 +270,21 @@ const UpdatedDatasetDetailPage: React.FC = () => { if (!dbName || !docId) return; (async () => { - await dispatch(fetchDocumentDetails({ dbName, docId })); // render tree ASAP - dispatch(fetchDbInfoByDatasetId({ dbName, docId })); // don't await + await dispatch(fetchDocumentDetails({ dbName, docId, rev })); // for dataset detail + dispatch(fetchDbInfoByDatasetId({ dbName, docId })); // for metadata panel (include modality) })(); - }, [dbName, docId, dispatch]); + }, [dbName, docId, rev, dispatch]); + // for revs list storage + const [revsList, setRevsList] = React.useState<{ rev: string }[]>([]); + + 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) { @@ -840,6 +864,10 @@ const UpdatedDatasetDetailPage: React.FC = () => { dbViewInfo={dbViewInfo} dbName={dbName} docId={docId} + // new props: + currentRev={rev} // reflect URL selection + onChangeRev={handleSelectRevision} + revsList={revsList} /> 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 8e5b706..fe6859d 100644 --- a/src/services/neurojson.service.ts +++ b/src/services/neurojson.service.ts @@ -28,11 +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}?revs_info=true` - ); + 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(