Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/DatasetDetailPage/FileTree/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Props = {
onPreview: (src: string | any, index: number, isInternal?: boolean) => void;
getInternalByPath: (path: string) => { data: any; index: number } | undefined;
getJsonByPath?: (path: string) => any;
highlightText?: string;
};

const formatSize = (n: number) => {
Expand All @@ -32,6 +33,7 @@ const FileTree: React.FC<Props> = ({
onPreview,
getInternalByPath,
getJsonByPath,
highlightText,
}) => (
<Box
sx={{
Expand Down Expand Up @@ -71,6 +73,7 @@ const FileTree: React.FC<Props> = ({
onPreview={onPreview}
getInternalByPath={getInternalByPath}
getJsonByPath={getJsonByPath}
highlightText={highlightText}
/> // pass the handlePreview(onPreview = handlePreview) function to FileTreeRow
))}
</Box>
Expand Down
60 changes: 34 additions & 26 deletions src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Box, Button, Collapse, Typography } from "@mui/material";
import { Tooltip, IconButton } from "@mui/material";
import { Colors } from "design/theme";
import React, { useState } from "react";
import { Color } from "three";

// show more / show less button for long string
const LeafString: React.FC<{ value: string }> = ({ value }) => {
Expand Down Expand Up @@ -80,11 +81,11 @@ const LeafString: React.FC<{ value: string }> = ({ value }) => {
type Props = {
node: TreeNode;
level: number;

// 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;
highlightText?: string;
};

// copy helper function
Expand Down Expand Up @@ -112,6 +113,7 @@ const FileTreeRow: React.FC<Props> = ({
onPreview,
getInternalByPath,
getJsonByPath,
highlightText,
}) => {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
Expand All @@ -120,6 +122,34 @@ const FileTreeRow: React.FC<Props> = ({
const internal = getInternalByPath(node.path);
const externalUrl = node.link?.url;

const rowRef = React.useRef<HTMLDivElement | null>(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 isExactHit =
!!highlightText &&
isSubjectFolder &&
node.name.toLowerCase() === highlightText.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]);

const rowHighlightSx = isExactHit
? { backgroundColor: `${Colors.yellow}`, borderRadius: 4 }
: {};

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)
Expand All @@ -136,13 +166,15 @@ const FileTreeRow: React.FC<Props> = ({
return (
<>
<Box
ref={rowRef}
sx={{
display: "flex",
alignItems: "flex-start",
gap: 1,
py: 0.5,
px: 1,
cursor: "pointer",
...rowHighlightSx,
"&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
}}
onClick={() => setOpen((o) => !o)}
Expand Down Expand Up @@ -252,6 +284,7 @@ const FileTreeRow: React.FC<Props> = ({
onPreview={onPreview}
getInternalByPath={getInternalByPath}
getJsonByPath={getJsonByPath}
highlightText={highlightText} // for subject highlight
/>
))}
</Collapse>
Expand Down Expand Up @@ -308,31 +341,6 @@ const FileTreeRow: React.FC<Props> = ({
: formatLeafValue(node.value)}
</Typography>
))}

{/* {!node.link && node.value !== undefined && (
<Typography
title={
node.name === "_ArrayZipData_"
? "[compressed data]"
: typeof node.value === "string"
? node.value
: JSON.stringify(node.value)
}
sx={{
fontFamily: "monospace",
fontSize: "0.85rem",
color: "text.secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
mt: 0.25,
}}
>
{node.name === "_ArrayZipData_"
? "[compressed data]"
: formatLeafValue(node.value)}
</Typography>
)} */}
</Box>
{/* ALWAYS show copy for files, even when no external/internal */}
<Box sx={{ alignSelf: "flex-start" }}>
Expand Down
64 changes: 62 additions & 2 deletions src/components/DatasetDetailPage/MetaDataPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
Tooltip,
} from "@mui/material";
import { Colors } from "design/theme";
import pako from "pako";
import React, { useMemo, useState } from "react";
import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels";

type Props = {
dbViewInfo: any;
Expand Down Expand Up @@ -63,6 +65,19 @@ const MetaDataPanel: React.FC<Props> = ({
// const [revIdx, setRevIdx] = useState(0);
// const selected = revs[revIdx];

// builds /search#query=<deflated-base64>
const buildSearchUrl = (query: Record<string, any>) => {
const deflated = pako.deflate(JSON.stringify(query));
const encoded = btoa(String.fromCharCode(...deflated));
return `${window.location.origin}/search#query=${encoded}`;
};

const openSearchForModality = (mod: string) => {
const normalized = modalityValueToEnumLabel[mod] || mod;
const url = buildSearchUrl({ modality: normalized });
window.open(url, "_blank", "noopener,noreferrer");
};

return (
<Box
sx={{
Expand Down Expand Up @@ -90,9 +105,54 @@ const MetaDataPanel: React.FC<Props> = ({
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Modalities
</Typography>
<Typography sx={{ color: "text.secondary" }}>

{(() => {
const mods = Array.isArray(dbViewInfo?.rows?.[0]?.value?.modality)
? [...new Set(dbViewInfo.rows[0].value.modality as string[])]
: [];

if (mods.length === 0) {
return (
<Typography sx={{ color: "text.secondary" }}>N/A</Typography>
);
}

return (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1, mt: 0.5 }}>
{mods.map((m) => (
<Chip
key={m}
label={m}
clickable
onClick={() => openSearchForModality(m)}
variant="outlined"
sx={{
"& .MuiChip-label": {
paddingX: "7px",
fontSize: "0.8rem",
},
height: "24px",
color: Colors.white,
border: `1px solid ${Colors.orange}`,
fontWeight: "bold",
transition: "all 0.2s ease",
backgroundColor: `${Colors.orange} !important`,
"&:hover": {
backgroundColor: `${Colors.darkOrange} !important`,
color: "white",
borderColor: Colors.darkOrange,
paddingX: "8px",
fontSize: "1rem",
},
}}
/>
))}
</Box>
);
})()}
{/* <Typography sx={{ color: "text.secondary" }}>
{dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"}
</Typography>
</Typography> */}
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Expand Down
15 changes: 15 additions & 0 deletions src/components/SearchPage/DatabaseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import {
Avatar,
} from "@mui/material";
import { Colors } from "design/theme";
import { useAppDispatch } from "hooks/useAppDispatch";
import { useAppSelector } from "hooks/useAppSelector";
import React from "react";
import { useEffect } from "react";
import { Link } from "react-router-dom";
import { fetchDbInfo } from "redux/neurojson/neurojson.action";
import { RootState } from "redux/store";
import RoutesEnum from "types/routes.enum";
import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels";

Expand All @@ -32,6 +37,14 @@ const DatabaseCard: React.FC<Props> = ({
keyword,
onChipClick,
}) => {
const dispatch = useAppDispatch();
const dbInfo = useAppSelector((state: RootState) => state.neurojson.dbInfo);
console.log("dbInfo", dbInfo);
useEffect(() => {
if (dbId) {
dispatch(fetchDbInfo(dbId.toLowerCase()));
}
}, [dbId, dispatch]);
const databaseLink = `${RoutesEnum.DATABASES}/${dbId}`;
// keyword hightlight functional component
const highlightKeyword = (text: string, keyword?: string) => {
Expand Down Expand Up @@ -182,6 +195,8 @@ const DatabaseCard: React.FC<Props> = ({
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
<Typography variant="body2" mt={1}>
<strong>Datasets:</strong> {datasets ?? "N/A"}
{/* <strong>Datasets:</strong>{" "}
{dbInfo?.doc_count != null ? dbInfo.doc_count - 1 : "N/A"} */}
</Typography>
</Stack>
</Stack>
Expand Down
19 changes: 13 additions & 6 deletions src/components/SearchPage/SubjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import RoutesEnum from "types/routes.enum";
interface SubjectCardProps {
dbname: string;
dsname: string;
age: string;
agemin: string;
subj: string;
parsedJson: {
key: string[];
Expand All @@ -26,14 +26,20 @@ interface SubjectCardProps {
const SubjectCard: React.FC<SubjectCardProps> = ({
dbname,
dsname,
age,
agemin,
subj,
parsedJson,
index,
onChipClick,
}) => {
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")}`;

// get the gender of subject
const genderCode = parsedJson?.key?.[1];
Expand All @@ -46,8 +52,8 @@ const SubjectCard: React.FC<SubjectCardProps> = ({

// cover age string to readable format
let ageDisplay = "N/A";
if (age) {
const ageNum = parseInt(age, 10) / 100;
if (agemin) {
const ageNum = parseInt(agemin, 10) / 100;
if (Number.isInteger(ageNum)) {
ageDisplay = `${ageNum} years`;
} else {
Expand Down Expand Up @@ -84,8 +90,9 @@ const SubjectCard: React.FC<SubjectCardProps> = ({
":hover": { textDecoration: "underline" },
}}
component={Link}
to={subjectLink}
target="_blank"
// to={subjectLink}
to={`${subjectLink}?focusSubj=${encodeURIComponent(canonicalSubj)}`}
// target="_blank"
>
<PersonOutlineIcon />
Subject: {subj} &nbsp;&nbsp;|&nbsp;&nbsp; Dataset: {dsname}
Expand Down
Loading