Skip to content

Commit e4ad801

Browse files
authored
Merge pull request #100 from NeuroJSON/staging
Dataset Navigation and Search page enhancements
2 parents 7282894 + bd5c7cd commit e4ad801

File tree

9 files changed

+424
-134
lines changed

9 files changed

+424
-134
lines changed

src/components/DatasetDetailPage/FileTree/FileTree.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Props = {
1414
onPreview: (src: string | any, index: number, isInternal?: boolean) => void;
1515
getInternalByPath: (path: string) => { data: any; index: number } | undefined;
1616
getJsonByPath?: (path: string) => any;
17+
highlightText?: string;
1718
};
1819

1920
const formatSize = (n: number) => {
@@ -32,6 +33,7 @@ const FileTree: React.FC<Props> = ({
3233
onPreview,
3334
getInternalByPath,
3435
getJsonByPath,
36+
highlightText,
3537
}) => (
3638
<Box
3739
sx={{
@@ -71,6 +73,7 @@ const FileTree: React.FC<Props> = ({
7173
onPreview={onPreview}
7274
getInternalByPath={getInternalByPath}
7375
getJsonByPath={getJsonByPath}
76+
highlightText={highlightText}
7477
/> // pass the handlePreview(onPreview = handlePreview) function to FileTreeRow
7578
))}
7679
</Box>

src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Box, Button, Collapse, Typography } from "@mui/material";
1414
import { Tooltip, IconButton } from "@mui/material";
1515
import { Colors } from "design/theme";
1616
import React, { useState } from "react";
17+
import { Color } from "three";
1718

1819
// show more / show less button for long string
1920
const LeafString: React.FC<{ value: string }> = ({ value }) => {
@@ -80,11 +81,11 @@ const LeafString: React.FC<{ value: string }> = ({ value }) => {
8081
type Props = {
8182
node: TreeNode;
8283
level: number;
83-
8484
// src is either an external URL(string) or the internal object
8585
onPreview: (src: string | any, index: number, isInternal?: boolean) => void;
8686
getInternalByPath: (path: string) => { data: any; index: number } | undefined;
8787
getJsonByPath?: (path: string) => any;
88+
highlightText?: string;
8889
};
8990

9091
// copy helper function
@@ -112,6 +113,7 @@ const FileTreeRow: React.FC<Props> = ({
112113
onPreview,
113114
getInternalByPath,
114115
getJsonByPath,
116+
highlightText,
115117
}) => {
116118
const [open, setOpen] = useState(false);
117119
const [copied, setCopied] = useState(false);
@@ -120,6 +122,34 @@ const FileTreeRow: React.FC<Props> = ({
120122
const internal = getInternalByPath(node.path);
121123
const externalUrl = node.link?.url;
122124

125+
const rowRef = React.useRef<HTMLDivElement | null>(null);
126+
// Highlight only if this row is exactly the subject folder (e.g., "sub-04")
127+
const isSubjectFolder =
128+
node.kind === "folder" && /^sub-[A-Za-z0-9]+$/i.test(node.name);
129+
const isExactHit =
130+
!!highlightText &&
131+
isSubjectFolder &&
132+
node.name.toLowerCase() === highlightText.toLowerCase();
133+
134+
React.useEffect(() => {
135+
if (isExactHit && rowRef.current) {
136+
rowRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
137+
// subtle flash
138+
// rowRef.current.animate(
139+
// [
140+
// { backgroundColor: `${Colors.yellow}`, offset: 0 }, // turn yellow
141+
// { backgroundColor: `${Colors.yellow}`, offset: 0.85 }, // stay yellow 85% of time
142+
// { backgroundColor: "transparent", offset: 1 }, // then fade out
143+
// ],
144+
// { duration: 8000, easing: "ease", fill: "forwards" }
145+
// );
146+
}
147+
}, [isExactHit]);
148+
149+
const rowHighlightSx = isExactHit
150+
? { backgroundColor: `${Colors.yellow}`, borderRadius: 4 }
151+
: {};
152+
123153
const handleCopy = async (e: React.MouseEvent) => {
124154
e.stopPropagation(); // prevent expand/ collapse from firing when click the copy button
125155
const json = getJsonByPath?.(node.path); // call getJsonByPath(node.path)
@@ -136,13 +166,15 @@ const FileTreeRow: React.FC<Props> = ({
136166
return (
137167
<>
138168
<Box
169+
ref={rowRef}
139170
sx={{
140171
display: "flex",
141172
alignItems: "flex-start",
142173
gap: 1,
143174
py: 0.5,
144175
px: 1,
145176
cursor: "pointer",
177+
...rowHighlightSx,
146178
"&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
147179
}}
148180
onClick={() => setOpen((o) => !o)}
@@ -252,6 +284,7 @@ const FileTreeRow: React.FC<Props> = ({
252284
onPreview={onPreview}
253285
getInternalByPath={getInternalByPath}
254286
getJsonByPath={getJsonByPath}
287+
highlightText={highlightText} // for subject highlight
255288
/>
256289
))}
257290
</Collapse>
@@ -308,31 +341,6 @@ const FileTreeRow: React.FC<Props> = ({
308341
: formatLeafValue(node.value)}
309342
</Typography>
310343
))}
311-
312-
{/* {!node.link && node.value !== undefined && (
313-
<Typography
314-
title={
315-
node.name === "_ArrayZipData_"
316-
? "[compressed data]"
317-
: typeof node.value === "string"
318-
? node.value
319-
: JSON.stringify(node.value)
320-
}
321-
sx={{
322-
fontFamily: "monospace",
323-
fontSize: "0.85rem",
324-
color: "text.secondary",
325-
whiteSpace: "nowrap",
326-
overflow: "hidden",
327-
textOverflow: "ellipsis",
328-
mt: 0.25,
329-
}}
330-
>
331-
{node.name === "_ArrayZipData_"
332-
? "[compressed data]"
333-
: formatLeafValue(node.value)}
334-
</Typography>
335-
)} */}
336344
</Box>
337345
{/* ALWAYS show copy for files, even when no external/internal */}
338346
<Box sx={{ alignSelf: "flex-start" }}>

src/components/DatasetDetailPage/MetaDataPanel.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
Tooltip,
1212
} from "@mui/material";
1313
import { Colors } from "design/theme";
14+
import pako from "pako";
1415
import React, { useMemo, useState } from "react";
16+
import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels";
1517

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

68+
// builds /search#query=<deflated-base64>
69+
const buildSearchUrl = (query: Record<string, any>) => {
70+
const deflated = pako.deflate(JSON.stringify(query));
71+
const encoded = btoa(String.fromCharCode(...deflated));
72+
return `${window.location.origin}/search#query=${encoded}`;
73+
};
74+
75+
const openSearchForModality = (mod: string) => {
76+
const normalized = modalityValueToEnumLabel[mod] || mod;
77+
const url = buildSearchUrl({ modality: normalized });
78+
window.open(url, "_blank", "noopener,noreferrer");
79+
};
80+
6681
return (
6782
<Box
6883
sx={{
@@ -90,9 +105,54 @@ const MetaDataPanel: React.FC<Props> = ({
90105
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
91106
Modalities
92107
</Typography>
93-
<Typography sx={{ color: "text.secondary" }}>
108+
109+
{(() => {
110+
const mods = Array.isArray(dbViewInfo?.rows?.[0]?.value?.modality)
111+
? [...new Set(dbViewInfo.rows[0].value.modality as string[])]
112+
: [];
113+
114+
if (mods.length === 0) {
115+
return (
116+
<Typography sx={{ color: "text.secondary" }}>N/A</Typography>
117+
);
118+
}
119+
120+
return (
121+
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1, mt: 0.5 }}>
122+
{mods.map((m) => (
123+
<Chip
124+
key={m}
125+
label={m}
126+
clickable
127+
onClick={() => openSearchForModality(m)}
128+
variant="outlined"
129+
sx={{
130+
"& .MuiChip-label": {
131+
paddingX: "7px",
132+
fontSize: "0.8rem",
133+
},
134+
height: "24px",
135+
color: Colors.white,
136+
border: `1px solid ${Colors.orange}`,
137+
fontWeight: "bold",
138+
transition: "all 0.2s ease",
139+
backgroundColor: `${Colors.orange} !important`,
140+
"&:hover": {
141+
backgroundColor: `${Colors.darkOrange} !important`,
142+
color: "white",
143+
borderColor: Colors.darkOrange,
144+
paddingX: "8px",
145+
fontSize: "1rem",
146+
},
147+
}}
148+
/>
149+
))}
150+
</Box>
151+
);
152+
})()}
153+
{/* <Typography sx={{ color: "text.secondary" }}>
94154
{dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"}
95-
</Typography>
155+
</Typography> */}
96156
</Box>
97157
<Box>
98158
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>

src/components/SearchPage/DatabaseCard.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import {
88
Avatar,
99
} from "@mui/material";
1010
import { Colors } from "design/theme";
11+
import { useAppDispatch } from "hooks/useAppDispatch";
12+
import { useAppSelector } from "hooks/useAppSelector";
1113
import React from "react";
14+
import { useEffect } from "react";
1215
import { Link } from "react-router-dom";
16+
import { fetchDbInfo } from "redux/neurojson/neurojson.action";
17+
import { RootState } from "redux/store";
1318
import RoutesEnum from "types/routes.enum";
1419
import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels";
1520

@@ -32,6 +37,14 @@ const DatabaseCard: React.FC<Props> = ({
3237
keyword,
3338
onChipClick,
3439
}) => {
40+
const dispatch = useAppDispatch();
41+
const dbInfo = useAppSelector((state: RootState) => state.neurojson.dbInfo);
42+
console.log("dbInfo", dbInfo);
43+
useEffect(() => {
44+
if (dbId) {
45+
dispatch(fetchDbInfo(dbId.toLowerCase()));
46+
}
47+
}, [dbId, dispatch]);
3548
const databaseLink = `${RoutesEnum.DATABASES}/${dbId}`;
3649
// keyword hightlight functional component
3750
const highlightKeyword = (text: string, keyword?: string) => {
@@ -182,6 +195,8 @@ const DatabaseCard: React.FC<Props> = ({
182195
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
183196
<Typography variant="body2" mt={1}>
184197
<strong>Datasets:</strong> {datasets ?? "N/A"}
198+
{/* <strong>Datasets:</strong>{" "}
199+
{dbInfo?.doc_count != null ? dbInfo.doc_count - 1 : "N/A"} */}
185200
</Typography>
186201
</Stack>
187202
</Stack>

src/components/SearchPage/SubjectCard.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import RoutesEnum from "types/routes.enum";
88
interface SubjectCardProps {
99
dbname: string;
1010
dsname: string;
11-
age: string;
11+
agemin: string;
1212
subj: string;
1313
parsedJson: {
1414
key: string[];
@@ -26,14 +26,20 @@ interface SubjectCardProps {
2626
const SubjectCard: React.FC<SubjectCardProps> = ({
2727
dbname,
2828
dsname,
29-
age,
29+
agemin,
3030
subj,
3131
parsedJson,
3232
index,
3333
onChipClick,
3434
}) => {
3535
const { modalities, tasks, sessions, types } = parsedJson.value;
3636
const subjectLink = `${RoutesEnum.DATABASES}/${dbname}/${dsname}`;
37+
const canonicalSubj = /^sub-/i.test(subj)
38+
? subj
39+
: `sub-${String(subj)
40+
.replace(/^sub-/i, "")
41+
.replace(/^0+/, "")
42+
.padStart(2, "0")}`;
3743

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

4753
// cover age string to readable format
4854
let ageDisplay = "N/A";
49-
if (age) {
50-
const ageNum = parseInt(age, 10) / 100;
55+
if (agemin) {
56+
const ageNum = parseInt(agemin, 10) / 100;
5157
if (Number.isInteger(ageNum)) {
5258
ageDisplay = `${ageNum} years`;
5359
} else {
@@ -84,8 +90,9 @@ const SubjectCard: React.FC<SubjectCardProps> = ({
8490
":hover": { textDecoration: "underline" },
8591
}}
8692
component={Link}
87-
to={subjectLink}
88-
target="_blank"
93+
// to={subjectLink}
94+
to={`${subjectLink}?focusSubj=${encodeURIComponent(canonicalSubj)}`}
95+
// target="_blank"
8996
>
9097
<PersonOutlineIcon />
9198
Subject: {subj} &nbsp;&nbsp;|&nbsp;&nbsp; Dataset: {dsname}

0 commit comments

Comments
 (0)