@openneuro/app 4.36.2 → 4.37.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/scripts/components/accordion/accordion.scss +1 -1
- package/src/scripts/components/activity-slider/ActivitySlider.tsx +4 -22
- package/src/scripts/components/activity-slider/slider.scss +1 -82
- package/src/scripts/components/button/button.scss +1 -1
- package/src/scripts/components/count-toggle/count-toggle.scss +1 -1
- package/src/scripts/components/dropdown/dropdown.scss +1 -1
- package/src/scripts/components/facets/facet.scss +5 -4
- package/src/scripts/components/footer/footer.scss +1 -1
- package/src/scripts/components/front-page/front-page.scss +1 -1
- package/src/scripts/components/header/header.scss +1 -1
- package/src/scripts/components/input/input.scss +1 -1
- package/src/scripts/components/input/term-search.scss +1 -1
- package/src/scripts/components/loading/loading.scss +1 -1
- package/src/scripts/components/modal/modal.scss +1 -1
- package/src/scripts/components/modality-cube/ModalityHexagon.tsx +29 -0
- package/src/scripts/components/modality-cube/modality-cube.scss +1 -1
- package/src/scripts/components/modality-cube/modality-hexagon.scss +93 -0
- package/src/scripts/components/page/page.scss +1 -1
- package/src/scripts/components/progress-bar/progress-bar.scss +1 -1
- package/src/scripts/components/radio/radio.scss +2 -2
- package/src/scripts/components/range/TwoHandleRange.scss +3 -3
- package/src/scripts/components/read-more/read-more.scss +1 -1
- package/src/scripts/components/scss/upload-modal.scss +1 -1
- package/src/scripts/components/tooltip/tooltip.scss +1 -1
- package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +132 -128
- package/src/scripts/dataset/__tests__/draft-container.spec.tsx +136 -0
- package/src/scripts/dataset/common/follow-toggles.tsx +1 -1
- package/src/scripts/dataset/components/DatasetHeader.tsx +13 -16
- package/src/scripts/dataset/components/DatasetToolButton.tsx +6 -7
- package/src/scripts/dataset/components/DatasetTools.tsx +6 -2
- package/src/scripts/dataset/draft-container.tsx +30 -24
- package/src/scripts/dataset/files/{file-display.jsx → file-display.tsx} +32 -19
- package/src/scripts/dataset/routes/tab-routes-draft.tsx +6 -1
- package/src/scripts/dataset/routes/tab-routes-snapshot.tsx +5 -1
- package/src/scripts/dataset/snapshot-container.tsx +37 -43
- package/src/scripts/scss/dataset/dataset-page.scss +44 -120
- package/src/scripts/scss/variables.scss +65 -13
- package/src/scripts/{components/search-page → search}/__tests__/NuerobagelSearch.spec.tsx +1 -1
- package/src/scripts/search/components/DatasetsRadioTabs.tsx +103 -0
- package/src/scripts/{components/search-page → search/components}/FilterListItem.tsx +1 -1
- package/src/scripts/{components/search-page → search/components}/FiltersBlock.tsx +5 -8
- package/src/scripts/search/components/MetaListItemList.tsx +31 -0
- package/src/scripts/{components/search-page → search/components}/SearchPage.tsx +15 -8
- package/src/scripts/search/components/SearchResultDetails.tsx +167 -0
- package/src/scripts/{components/search-page → search/components}/SearchResultItem.tsx +57 -173
- package/src/scripts/search/components/SearchResultsList.tsx +45 -0
- package/src/scripts/{components/search-page → search/components}/SearchSort.tsx +2 -2
- package/src/scripts/search/filters-block-container.tsx +1 -1
- package/src/scripts/search/inputs/index.ts +0 -4
- package/src/scripts/search/inputs/sliding-radio-group.tsx +127 -0
- package/src/scripts/search/inputs/sort-by-select.tsx +1 -1
- package/src/scripts/{components/search-page → search/scss}/filters-block.scss +1 -1
- package/src/scripts/{components/search-page → search/scss}/search-page.scss +123 -92
- package/src/scripts/search/scss/search-result-details.scss +70 -0
- package/src/scripts/{components/search-page → search/scss}/search-result.scss +29 -56
- package/src/scripts/{components/search-page → search/scss}/search-sort.scss +1 -1
- package/src/scripts/search/scss/sliding-radio-group.scss +115 -0
- package/src/scripts/search/search-container.tsx +90 -24
- package/src/scripts/search/use-search-results.tsx +3 -0
- package/src/scripts/users/github-auth-button.tsx +1 -1
- package/src/scripts/components/scss/_variables.scss +0 -245
- package/src/scripts/components/search-page/SearchResultsList.tsx +0 -29
- package/src/scripts/dataset/files/index.tsx +0 -6
- package/src/scripts/search/inputs/admin-allDatasets-toggle.tsx +0 -47
- package/src/scripts/search/inputs/show-datasets-radios.tsx +0 -74
- /package/src/scripts/{components/search-page → search/components}/CommunityHeader.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/FacetBlockContainerExample.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/FilterDateItem.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/ModalityHeader.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/NeurobagelSearch.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/SearchSortContainerExample.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/TermListItem.tsx +0 -0
- /package/src/scripts/{components/search-page → search/components}/neurobagel_logo.svg +0 -0
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import
|
|
2
|
+
import getYear from "date-fns/getYear"
|
|
3
3
|
import parseISO from "date-fns/parseISO"
|
|
4
|
-
import formatDistanceToNow from "date-fns/formatDistanceToNow"
|
|
5
4
|
import { Link } from "react-router-dom"
|
|
6
5
|
import { Tooltip } from "../../components/tooltip/Tooltip"
|
|
7
6
|
import { Icon } from "../../components/icon/Icon"
|
|
8
7
|
import { useCookies } from "react-cookie"
|
|
9
8
|
import { getProfile } from "../../authentication/profile"
|
|
10
9
|
import { useUser } from "../../queries/user"
|
|
11
|
-
import "
|
|
10
|
+
import "../scss/search-result.scss"
|
|
12
11
|
import activityPulseIcon from "../../../assets/activity-icon.png"
|
|
13
|
-
import { ModalityLabel } from "../../components/formatting/modality-label"
|
|
14
12
|
import { hasEditPermissions } from "../../authentication/profile"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @param {*} dateObject
|
|
18
|
-
*/
|
|
13
|
+
import { ModalityHexagon } from "../../components/modality-cube/ModalityHexagon"
|
|
14
|
+
|
|
19
15
|
export const formatDate = (dateObject) =>
|
|
20
16
|
new Date(dateObject).toISOString().split("T")[0]
|
|
21
17
|
|
|
@@ -50,6 +46,7 @@ export interface SearchResultItemProps {
|
|
|
50
46
|
latestSnapshot: {
|
|
51
47
|
id: string
|
|
52
48
|
size: number
|
|
49
|
+
readme: string
|
|
53
50
|
summary: {
|
|
54
51
|
pet: {
|
|
55
52
|
BodyPart: string
|
|
@@ -58,6 +55,7 @@ export interface SearchResultItemProps {
|
|
|
58
55
|
TracerName: string[]
|
|
59
56
|
TracerRadionuclide: string
|
|
60
57
|
}
|
|
58
|
+
primaryModality: string
|
|
61
59
|
modalities: string[]
|
|
62
60
|
sessions: []
|
|
63
61
|
subjects: string[]
|
|
@@ -84,7 +82,9 @@ export interface SearchResultItemProps {
|
|
|
84
82
|
warnings: number
|
|
85
83
|
}
|
|
86
84
|
description: {
|
|
85
|
+
Authors: string[]
|
|
87
86
|
Name: string
|
|
87
|
+
DatasetDOI: string
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
analytics: {
|
|
@@ -112,11 +112,15 @@ export interface SearchResultItemProps {
|
|
|
112
112
|
]
|
|
113
113
|
}
|
|
114
114
|
datasetTypeSelected?: string
|
|
115
|
+
onClick: (itemId: string, event: React.MouseEvent<HTMLButtonElement>) => void
|
|
116
|
+
isExpanded: boolean
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
export const SearchResultItem = ({
|
|
118
120
|
node,
|
|
119
121
|
datasetTypeSelected,
|
|
122
|
+
onClick,
|
|
123
|
+
isExpanded,
|
|
120
124
|
}: SearchResultItemProps) => {
|
|
121
125
|
const { user } = useUser()
|
|
122
126
|
const [cookies] = useCookies()
|
|
@@ -125,96 +129,10 @@ export const SearchResultItem = ({
|
|
|
125
129
|
|
|
126
130
|
const isAdmin = user?.admin
|
|
127
131
|
const hasEdit = hasEditPermissions(node.permissions, profileSub) || isAdmin
|
|
128
|
-
|
|
129
|
-
const heading = node.latestSnapshot.description?.Name
|
|
130
|
-
? node.latestSnapshot.description?.Name
|
|
131
|
-
: node.id
|
|
132
|
-
const summary = node.latestSnapshot?.summary
|
|
133
132
|
const datasetId = node.id
|
|
134
|
-
const numSessions = summary?.sessions.length > 0 ? summary.sessions.length : 1
|
|
135
|
-
const numSubjects = summary?.subjects.length > 0 ? summary.subjects.length : 1
|
|
136
|
-
const accessionNumber = (
|
|
137
|
-
<span className="result-summary-meta">
|
|
138
|
-
<strong>Openneuro Accession Number:</strong>
|
|
139
|
-
<Link to={"/datasets/" + datasetId}>{node.id}</Link>
|
|
140
|
-
</span>
|
|
141
|
-
)
|
|
142
|
-
const sessions = (
|
|
143
|
-
<span className="result-summary-meta">
|
|
144
|
-
<strong>Sessions:</strong>
|
|
145
|
-
<span>{numSessions.toLocaleString()}</span>
|
|
146
|
-
</span>
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
const ages = (value) => {
|
|
150
|
-
if (value) {
|
|
151
|
-
const ages = value.filter((x) => x)
|
|
152
|
-
if (ages.length === 0) return "N/A"
|
|
153
|
-
else if (ages.length === 1) return ages[0]
|
|
154
|
-
else return `${Math.min(...ages)} - ${Math.max(...ages)}`
|
|
155
|
-
} else return "N/A"
|
|
156
|
-
}
|
|
157
133
|
|
|
158
|
-
const
|
|
159
|
-
<span className="result-summary-meta">
|
|
160
|
-
<strong>
|
|
161
|
-
{node?.metadata?.ages?.length === 1
|
|
162
|
-
? "Participant's Age"
|
|
163
|
-
: "Participants' Ages"}
|
|
164
|
-
:{" "}
|
|
165
|
-
</strong>
|
|
166
|
-
<span>
|
|
167
|
-
{ages(summary?.subjectMetadata?.map((subject) => subject.age))}
|
|
168
|
-
</span>
|
|
169
|
-
</span>
|
|
170
|
-
)
|
|
171
|
-
const subjects = (
|
|
172
|
-
<span className="result-summary-meta">
|
|
173
|
-
<strong>Participants:</strong>
|
|
174
|
-
<span>{numSubjects.toLocaleString()}</span>
|
|
175
|
-
</span>
|
|
176
|
-
)
|
|
177
|
-
const size = (
|
|
178
|
-
<span className="result-summary-meta">
|
|
179
|
-
<strong>Size:</strong>
|
|
180
|
-
<span>{bytes(node?.latestSnapshot?.size) || "unknown"}</span>
|
|
181
|
-
</span>
|
|
182
|
-
)
|
|
183
|
-
const files = (
|
|
184
|
-
<span className="result-summary-meta">
|
|
185
|
-
<strong>Files:</strong>
|
|
186
|
-
<span>{summary?.totalFiles.toLocaleString()}</span>
|
|
187
|
-
</span>
|
|
188
|
-
)
|
|
134
|
+
const heading = node.latestSnapshot.description?.Name?.trim() || datasetId
|
|
189
135
|
|
|
190
|
-
const dateAdded = formatDate(node.created)
|
|
191
|
-
const dateAddedDifference = formatDistanceToNow(parseISO(node.created))
|
|
192
|
-
let lastUpdatedDate
|
|
193
|
-
if (node.snapshots.length) {
|
|
194
|
-
const dateUpdated = formatDate(
|
|
195
|
-
node.snapshots[node.snapshots.length - 1].created,
|
|
196
|
-
)
|
|
197
|
-
const dateUpdatedDifference = formatDistanceToNow(
|
|
198
|
-
parseISO(node.snapshots[node.snapshots.length - 1].created),
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
lastUpdatedDate = (
|
|
202
|
-
<>
|
|
203
|
-
<span className="updated-divider">|</span>
|
|
204
|
-
<div className="updated-date">
|
|
205
|
-
<span>Updated:</span>
|
|
206
|
-
{dateUpdated} - {dateUpdatedDifference} ago
|
|
207
|
-
</div>
|
|
208
|
-
</>
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const uploader = (
|
|
213
|
-
<div className="uploader">
|
|
214
|
-
<span>Uploaded by:</span>
|
|
215
|
-
{node.uploader?.name} on {dateAdded} - {dateAddedDifference} ago
|
|
216
|
-
</div>
|
|
217
|
-
)
|
|
218
136
|
const downloads = node.analytics.downloads
|
|
219
137
|
? node.analytics.downloads.toLocaleString() + " Downloads \n"
|
|
220
138
|
: ""
|
|
@@ -292,33 +210,12 @@ export const SearchResultItem = ({
|
|
|
292
210
|
</Tooltip>
|
|
293
211
|
)
|
|
294
212
|
|
|
295
|
-
const _list = (type, items) => {
|
|
296
|
-
if (items && items.length > 0) {
|
|
297
|
-
return (
|
|
298
|
-
<>
|
|
299
|
-
<strong>{type}:</strong>
|
|
300
|
-
<div>
|
|
301
|
-
{items.map((item, index) => (
|
|
302
|
-
<span className="list-item" key={index}>
|
|
303
|
-
{item}
|
|
304
|
-
</span>
|
|
305
|
-
))}
|
|
306
|
-
</div>
|
|
307
|
-
</>
|
|
308
|
-
)
|
|
309
|
-
} else {
|
|
310
|
-
return null
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
213
|
let invalid = false
|
|
315
|
-
// Legacy issues still flagged
|
|
316
214
|
if (node.latestSnapshot.issues) {
|
|
317
|
-
invalid = node.latestSnapshot.issues.some(
|
|
318
|
-
issue.severity === "error"
|
|
215
|
+
invalid = node.latestSnapshot.issues.some(
|
|
216
|
+
(issue) => issue.severity === "error",
|
|
319
217
|
)
|
|
320
218
|
} else {
|
|
321
|
-
// Test if there's any schema validator errors
|
|
322
219
|
invalid = node.latestSnapshot.validation?.errors > 0
|
|
323
220
|
}
|
|
324
221
|
const shared = !node.public && node.uploader?.id !== profileSub
|
|
@@ -346,74 +243,61 @@ export const SearchResultItem = ({
|
|
|
346
243
|
</div>
|
|
347
244
|
)
|
|
348
245
|
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
)),
|
|
357
|
-
)}
|
|
358
|
-
</div>
|
|
359
|
-
)
|
|
360
|
-
: null
|
|
361
|
-
const taskList = summary?.tasks.length
|
|
362
|
-
? <div className="task-list">{_list(<>Tasks</>, summary?.tasks)}</div>
|
|
363
|
-
: null
|
|
364
|
-
|
|
365
|
-
const tracers = summary?.pet?.TracerName?.length
|
|
366
|
-
? (
|
|
367
|
-
<div className="tracers-list">
|
|
368
|
-
{_list(
|
|
369
|
-
<>
|
|
370
|
-
{summary?.pet?.TracerName.length === 1
|
|
371
|
-
? "Radiotracer"
|
|
372
|
-
: "Radiotracers"}
|
|
373
|
-
</>,
|
|
374
|
-
summary?.pet?.TracerName,
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
377
|
-
)
|
|
378
|
-
: null
|
|
246
|
+
const year = getYear(parseISO(node.created))
|
|
247
|
+
const authors = node.latestSnapshot.description?.Authors
|
|
248
|
+
? node.latestSnapshot.description.Authors.join(" and ")
|
|
249
|
+
: "NO AUTHORS FOUND"
|
|
250
|
+
const datasetCite =
|
|
251
|
+
`${authors} (${year}). ${node.latestSnapshot.description.Name}. OpenNeuro. [Dataset] doi: ${node.latestSnapshot.description.DatasetDOI}`
|
|
252
|
+
const trimlength = 450
|
|
379
253
|
|
|
380
254
|
return (
|
|
381
255
|
<>
|
|
382
|
-
<div
|
|
256
|
+
<div
|
|
257
|
+
className={`grid grid-nogutter search-result ${
|
|
258
|
+
isExpanded ? "expanded" : ""
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
383
261
|
<div className="col col-9">
|
|
384
|
-
<
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
262
|
+
<div className="col col-12">
|
|
263
|
+
<h3>
|
|
264
|
+
<Link to={"/datasets/" + datasetId}>{heading}</Link>
|
|
265
|
+
</h3>
|
|
266
|
+
<p>
|
|
267
|
+
{node.latestSnapshot?.readme
|
|
268
|
+
? (node.latestSnapshot.readme.length > trimlength
|
|
269
|
+
? `${node.latestSnapshot.readme.substring(0, trimlength)}...`
|
|
270
|
+
: node.latestSnapshot.readme)
|
|
271
|
+
: ""}
|
|
272
|
+
</p>
|
|
273
|
+
<cite>{datasetCite}</cite>
|
|
390
274
|
</div>
|
|
391
275
|
</div>
|
|
392
276
|
|
|
393
|
-
<div className="col col-3
|
|
277
|
+
<div className="col col-3 grid">
|
|
278
|
+
<div className="col col-12 result-icon-wrap">
|
|
279
|
+
{datasetOwenerIcons}
|
|
280
|
+
{activityIcon}
|
|
281
|
+
<ModalityHexagon
|
|
282
|
+
primaryModality={node.latestSnapshot.summary?.primaryModality}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
394
285
|
{MyDatasetsPage && (
|
|
395
|
-
<div className="dataset-permissions-tag">
|
|
286
|
+
<div className="col col-12 dataset-permissions-tag text-right">
|
|
396
287
|
<small>Access: {datasetPerms}</small>
|
|
397
288
|
</div>
|
|
398
289
|
)}
|
|
399
|
-
<div className="result-
|
|
400
|
-
|
|
401
|
-
|
|
290
|
+
<div className="col col-12 result-actions">
|
|
291
|
+
<button
|
|
292
|
+
className={`on-button on-button--small ${
|
|
293
|
+
isExpanded && "expanded"
|
|
294
|
+
}`}
|
|
295
|
+
onClick={(e) => onClick(node.id, e)}
|
|
296
|
+
>
|
|
297
|
+
{isExpanded ? "Hide Details" : "Show Details"}
|
|
298
|
+
</button>
|
|
402
299
|
</div>
|
|
403
300
|
</div>
|
|
404
|
-
<div className="col col-12 result-meta-body">
|
|
405
|
-
{modalityList}
|
|
406
|
-
{taskList}
|
|
407
|
-
{tracers}
|
|
408
|
-
</div>
|
|
409
|
-
<div className="result-meta-footer">
|
|
410
|
-
{accessionNumber}
|
|
411
|
-
{sessions}
|
|
412
|
-
{subjects}
|
|
413
|
-
{agesRange}
|
|
414
|
-
{size}
|
|
415
|
-
{files}
|
|
416
|
-
</div>
|
|
417
301
|
</div>
|
|
418
302
|
</>
|
|
419
303
|
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { SearchResultItem } from "./SearchResultItem"
|
|
3
|
+
import "../scss/search-page.scss"
|
|
4
|
+
import type { SearchResultItemProps } from "./SearchResultItem"
|
|
5
|
+
|
|
6
|
+
export interface SearchResultsListProps {
|
|
7
|
+
items: { node: SearchResultItemProps["node"] }[]
|
|
8
|
+
datasetTypeSelected: string
|
|
9
|
+
clickedItemData: SearchResultItemProps["node"] | null
|
|
10
|
+
handleItemClick: (
|
|
11
|
+
itemId: string,
|
|
12
|
+
event: React.MouseEvent<HTMLButtonElement>,
|
|
13
|
+
) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SearchResultsList = ({
|
|
17
|
+
items,
|
|
18
|
+
datasetTypeSelected,
|
|
19
|
+
clickedItemData,
|
|
20
|
+
handleItemClick,
|
|
21
|
+
}: SearchResultsListProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<div
|
|
25
|
+
className={`search-results col col-12 ${clickedItemData ? "open" : ""}`}
|
|
26
|
+
>
|
|
27
|
+
{items.map((data) => {
|
|
28
|
+
if (data) {
|
|
29
|
+
const isActive = clickedItemData?.id === data.node.id
|
|
30
|
+
return (
|
|
31
|
+
<SearchResultItem
|
|
32
|
+
node={data.node}
|
|
33
|
+
key={data.node.id}
|
|
34
|
+
datasetTypeSelected={datasetTypeSelected}
|
|
35
|
+
onClick={handleItemClick}
|
|
36
|
+
isExpanded={isActive}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SearchParamsCtx,
|
|
14
14
|
useCheckIfParamsAreSelected,
|
|
15
15
|
} from "./search-params-ctx"
|
|
16
|
-
import { FiltersBlock } from "
|
|
16
|
+
import { FiltersBlock } from "./components/FiltersBlock"
|
|
17
17
|
import initialSearchParams from "./initial-search-params"
|
|
18
18
|
|
|
19
19
|
interface FiltersBlockContainerProps {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import KeywordInput from "./keyword-input"
|
|
2
|
-
import AllDatasetsToggle from "./admin-allDatasets-toggle"
|
|
3
2
|
import ModalitySelect from "./modality-select"
|
|
4
3
|
import InitiativeSelect from "./initiative-select"
|
|
5
|
-
import ShowDatasetRadios from "./show-datasets-radios"
|
|
6
4
|
import AgeRangeInput from "./age-range-input"
|
|
7
5
|
import SubjectCountRangeInput from "./subject-count-range-input"
|
|
8
6
|
import DatasetTypeSelect from "./dataset-type-select"
|
|
@@ -23,7 +21,6 @@ import SortBySelect from "./sort-by-select"
|
|
|
23
21
|
|
|
24
22
|
export {
|
|
25
23
|
AgeRangeInput,
|
|
26
|
-
AllDatasetsToggle,
|
|
27
24
|
AuthorInput,
|
|
28
25
|
BodyPartsInput,
|
|
29
26
|
DatasetTypeSelect,
|
|
@@ -36,7 +33,6 @@ export {
|
|
|
36
33
|
ScannerManufacturersModelNames,
|
|
37
34
|
SectionSelect,
|
|
38
35
|
SexRadios,
|
|
39
|
-
ShowDatasetRadios,
|
|
40
36
|
SortBySelect,
|
|
41
37
|
SpeciesSelect,
|
|
42
38
|
StudyDomainInput,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react"
|
|
2
|
+
import type { FC } from "react"
|
|
3
|
+
import "../scss/sliding-radio-group.scss"
|
|
4
|
+
|
|
5
|
+
interface RadioItem {
|
|
6
|
+
value: string
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SlidingRadioGroupProps {
|
|
11
|
+
items: (string | RadioItem)[]
|
|
12
|
+
selected: string | undefined
|
|
13
|
+
setSelected: (value: string) => void
|
|
14
|
+
groupName: string
|
|
15
|
+
className?: string
|
|
16
|
+
initialSelectedValueOverride?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SlidingRadioGroup: FC<SlidingRadioGroupProps> = ({
|
|
20
|
+
items,
|
|
21
|
+
selected,
|
|
22
|
+
setSelected,
|
|
23
|
+
groupName,
|
|
24
|
+
className = "",
|
|
25
|
+
initialSelectedValueOverride,
|
|
26
|
+
}) => {
|
|
27
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
const radioLabelRefs = useRef<{ [key: string]: HTMLLabelElement }>({})
|
|
29
|
+
const [sliderStyles, setSliderStyles] = useState({
|
|
30
|
+
left: 0,
|
|
31
|
+
width: 0,
|
|
32
|
+
height: 0,
|
|
33
|
+
top: 0,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const calculateSliderPosition = useCallback((targetValue: string) => {
|
|
37
|
+
if (!containerRef.current || !radioLabelRefs.current[targetValue]) {
|
|
38
|
+
return { left: 0, width: 0, height: 0, top: 0 }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const targetLabel = radioLabelRefs.current[targetValue]
|
|
42
|
+
const containerRect = containerRef.current.getBoundingClientRect()
|
|
43
|
+
const labelRect = targetLabel.getBoundingClientRect()
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
left: labelRect.left - containerRect.left,
|
|
47
|
+
width: labelRect.width,
|
|
48
|
+
height: labelRect.height,
|
|
49
|
+
top: labelRect.top - containerRect.top,
|
|
50
|
+
}
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const updateSlider = () => {
|
|
55
|
+
const valueToCalculate = initialSelectedValueOverride || selected
|
|
56
|
+
|
|
57
|
+
if (valueToCalculate) {
|
|
58
|
+
const newStyles = calculateSliderPosition(valueToCalculate)
|
|
59
|
+
setSliderStyles(newStyles)
|
|
60
|
+
} else {
|
|
61
|
+
// If nothing is selected, hide the slider
|
|
62
|
+
setSliderStyles({ left: 0, width: 0, height: 0, top: 0 })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const timeoutId = setTimeout(updateSlider, 50)
|
|
67
|
+
window.addEventListener("resize", updateSlider)
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
clearTimeout(timeoutId)
|
|
71
|
+
window.removeEventListener("resize", updateSlider)
|
|
72
|
+
}
|
|
73
|
+
}, [selected, calculateSliderPosition, initialSelectedValueOverride])
|
|
74
|
+
|
|
75
|
+
const isSliderVisible = sliderStyles.width > 0 && sliderStyles.height > 0
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={`sliding-radio-group-root ${className} btn-group-wrapper facet-radio show-dataset-radios-container`}
|
|
80
|
+
ref={containerRef}
|
|
81
|
+
>
|
|
82
|
+
{/* The sliding highlight element */}
|
|
83
|
+
<div
|
|
84
|
+
className="sliding-highlight"
|
|
85
|
+
style={{
|
|
86
|
+
left: sliderStyles.left,
|
|
87
|
+
width: sliderStyles.width,
|
|
88
|
+
height: sliderStyles.height - 2,
|
|
89
|
+
top: sliderStyles.top,
|
|
90
|
+
opacity: isSliderVisible ? 1 : 0,
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<div className="show-dataset-radios-group" role="radiogroup">
|
|
95
|
+
{items.map((item) => {
|
|
96
|
+
const value = typeof item === "object" ? item.value : item
|
|
97
|
+
const label = typeof item === "object" ? item.label : item
|
|
98
|
+
const isChecked = selected === value
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="dataset-filter-radio" key={value}>
|
|
102
|
+
<input
|
|
103
|
+
type="radio"
|
|
104
|
+
id={`${groupName}-${value.replace(/\s/g, "")}`}
|
|
105
|
+
name={groupName}
|
|
106
|
+
value={value}
|
|
107
|
+
checked={isChecked}
|
|
108
|
+
onChange={() => setSelected(value)}
|
|
109
|
+
/>
|
|
110
|
+
<label
|
|
111
|
+
htmlFor={`${groupName}-${value.replace(/\s/g, "")}`}
|
|
112
|
+
ref={(el) => {
|
|
113
|
+
if (el) radioLabelRefs.current[value] = el
|
|
114
|
+
}}
|
|
115
|
+
className={isChecked ? "is-active" : ""}
|
|
116
|
+
>
|
|
117
|
+
{label}
|
|
118
|
+
</label>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default SlidingRadioGroup
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useContext } from "react"
|
|
2
2
|
import type { FC } from "react"
|
|
3
3
|
import { SearchParamsCtx } from "../search-params-ctx"
|
|
4
|
-
import { SearchSort } from "
|
|
4
|
+
import { SearchSort } from "../components/SearchSort"
|
|
5
5
|
|
|
6
6
|
interface SortBySelectProps {
|
|
7
7
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|