@openneuro/app 4.36.2 → 4.37.0-alpha.1

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.
Files changed (74) hide show
  1. package/package.json +5 -3
  2. package/src/scripts/components/accordion/accordion.scss +1 -1
  3. package/src/scripts/components/activity-slider/ActivitySlider.tsx +4 -22
  4. package/src/scripts/components/activity-slider/slider.scss +1 -82
  5. package/src/scripts/components/button/button.scss +1 -1
  6. package/src/scripts/components/count-toggle/count-toggle.scss +1 -1
  7. package/src/scripts/components/dropdown/dropdown.scss +1 -1
  8. package/src/scripts/components/facets/facet.scss +5 -4
  9. package/src/scripts/components/footer/footer.scss +1 -1
  10. package/src/scripts/components/front-page/front-page.scss +1 -1
  11. package/src/scripts/components/header/header.scss +1 -1
  12. package/src/scripts/components/input/input.scss +1 -1
  13. package/src/scripts/components/input/term-search.scss +1 -1
  14. package/src/scripts/components/loading/loading.scss +1 -1
  15. package/src/scripts/components/modal/modal.scss +1 -1
  16. package/src/scripts/components/modality-cube/ModalityHexagon.tsx +29 -0
  17. package/src/scripts/components/modality-cube/modality-cube.scss +1 -1
  18. package/src/scripts/components/modality-cube/modality-hexagon.scss +93 -0
  19. package/src/scripts/components/page/page.scss +1 -1
  20. package/src/scripts/components/progress-bar/progress-bar.scss +1 -1
  21. package/src/scripts/components/radio/radio.scss +2 -2
  22. package/src/scripts/components/range/TwoHandleRange.scss +3 -3
  23. package/src/scripts/components/read-more/read-more.scss +1 -1
  24. package/src/scripts/components/scss/upload-modal.scss +1 -1
  25. package/src/scripts/components/tooltip/tooltip.scss +1 -1
  26. package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +132 -128
  27. package/src/scripts/dataset/__tests__/draft-container.spec.tsx +136 -0
  28. package/src/scripts/dataset/common/follow-toggles.tsx +1 -1
  29. package/src/scripts/dataset/components/DatasetHeader.tsx +13 -16
  30. package/src/scripts/dataset/components/DatasetToolButton.tsx +6 -7
  31. package/src/scripts/dataset/components/DatasetTools.tsx +6 -2
  32. package/src/scripts/dataset/draft-container.tsx +30 -24
  33. package/src/scripts/dataset/files/{file-display.jsx → file-display.tsx} +32 -19
  34. package/src/scripts/dataset/routes/tab-routes-draft.tsx +6 -1
  35. package/src/scripts/dataset/routes/tab-routes-snapshot.tsx +5 -1
  36. package/src/scripts/dataset/snapshot-container.tsx +37 -43
  37. package/src/scripts/scss/dataset/dataset-page.scss +44 -120
  38. package/src/scripts/scss/variables.scss +65 -13
  39. package/src/scripts/{components/search-page → search}/__tests__/NuerobagelSearch.spec.tsx +1 -1
  40. package/src/scripts/search/components/DatasetsRadioTabs.tsx +103 -0
  41. package/src/scripts/{components/search-page → search/components}/FilterListItem.tsx +1 -1
  42. package/src/scripts/{components/search-page → search/components}/FiltersBlock.tsx +5 -8
  43. package/src/scripts/search/components/MetaListItemList.tsx +31 -0
  44. package/src/scripts/{components/search-page → search/components}/SearchPage.tsx +15 -8
  45. package/src/scripts/search/components/SearchResultDetails.tsx +167 -0
  46. package/src/scripts/{components/search-page → search/components}/SearchResultItem.tsx +57 -173
  47. package/src/scripts/search/components/SearchResultsList.tsx +45 -0
  48. package/src/scripts/{components/search-page → search/components}/SearchSort.tsx +2 -2
  49. package/src/scripts/search/filters-block-container.tsx +1 -1
  50. package/src/scripts/search/inputs/index.ts +0 -4
  51. package/src/scripts/search/inputs/sliding-radio-group.tsx +127 -0
  52. package/src/scripts/search/inputs/sort-by-select.tsx +1 -1
  53. package/src/scripts/{components/search-page → search/scss}/filters-block.scss +1 -1
  54. package/src/scripts/{components/search-page → search/scss}/search-page.scss +123 -92
  55. package/src/scripts/search/scss/search-result-details.scss +70 -0
  56. package/src/scripts/{components/search-page → search/scss}/search-result.scss +29 -56
  57. package/src/scripts/{components/search-page → search/scss}/search-sort.scss +1 -1
  58. package/src/scripts/search/scss/sliding-radio-group.scss +115 -0
  59. package/src/scripts/search/search-container.tsx +90 -24
  60. package/src/scripts/search/use-search-results.tsx +3 -0
  61. package/src/scripts/users/github-auth-button.tsx +1 -1
  62. package/src/scripts/components/scss/_variables.scss +0 -245
  63. package/src/scripts/components/search-page/SearchResultsList.tsx +0 -29
  64. package/src/scripts/dataset/files/index.tsx +0 -6
  65. package/src/scripts/search/inputs/admin-allDatasets-toggle.tsx +0 -47
  66. package/src/scripts/search/inputs/show-datasets-radios.tsx +0 -74
  67. /package/src/scripts/{components/search-page → search/components}/CommunityHeader.tsx +0 -0
  68. /package/src/scripts/{components/search-page → search/components}/FacetBlockContainerExample.tsx +0 -0
  69. /package/src/scripts/{components/search-page → search/components}/FilterDateItem.tsx +0 -0
  70. /package/src/scripts/{components/search-page → search/components}/ModalityHeader.tsx +0 -0
  71. /package/src/scripts/{components/search-page → search/components}/NeurobagelSearch.tsx +0 -0
  72. /package/src/scripts/{components/search-page → search/components}/SearchSortContainerExample.tsx +0 -0
  73. /package/src/scripts/{components/search-page → search/components}/TermListItem.tsx +0 -0
  74. /package/src/scripts/{components/search-page → search/components}/neurobagel_logo.svg +0 -0
@@ -0,0 +1,103 @@
1
+ import React, { useCallback, useContext } from "react"
2
+ import type { FC } from "react"
3
+ import { SearchParamsCtx } from "../search-params-ctx"
4
+ import { useCookies } from "react-cookie"
5
+ import { getUnexpiredProfile } from "../../authentication/profile"
6
+ import { useUser } from "../../queries/user"
7
+ import SlidingRadioGroup from "../inputs/sliding-radio-group"
8
+
9
+ export const DatasetsRadioTabs: FC = () => {
10
+ const [cookies] = useCookies()
11
+ const loggedOut = !getUnexpiredProfile(cookies)
12
+ const { user } = useUser()
13
+ const isAdmin = user?.admin
14
+ const { searchParams, setSearchParams } = useContext(SearchParamsCtx)
15
+ const {
16
+ datasetType_available,
17
+ datasetType_selected,
18
+ datasetStatus_available,
19
+ datasetStatus_selected,
20
+ searchAllDatasets,
21
+ } = searchParams
22
+
23
+ const updatedDatasetTypeAvailable = [...datasetType_available]
24
+ if (isAdmin) {
25
+ const adminDatasetValue = "Admin: All Datasets"
26
+ const adminDatasetLabel = "Admin" // Define the new label
27
+
28
+ // Check if an item with the 'Admin: All Datasets' value already exists
29
+ const alreadyHasAdminDataset = updatedDatasetTypeAvailable.some((item) =>
30
+ (typeof item === "object" ? item.value : item) === adminDatasetValue
31
+ )
32
+
33
+ if (!alreadyHasAdminDataset) {
34
+ // Push it as an object with both value and the desired label
35
+ updatedDatasetTypeAvailable.push({
36
+ value: adminDatasetValue,
37
+ label: adminDatasetLabel,
38
+ })
39
+ }
40
+ }
41
+
42
+ const setShowSelected = useCallback(
43
+ (newDatasetTypeSelected: string) => {
44
+ setSearchParams((prevState) => {
45
+ const newSearchAllDatasets =
46
+ newDatasetTypeSelected === "Admin: All Datasets"
47
+
48
+ return {
49
+ ...prevState,
50
+ datasetType_selected: newDatasetTypeSelected,
51
+ searchAllDatasets: newSearchAllDatasets,
52
+
53
+ datasetStatus_selected: newSearchAllDatasets
54
+ ? undefined
55
+ : newDatasetTypeSelected === "My Datasets"
56
+ ? prevState.datasetStatus_selected
57
+ : undefined,
58
+ }
59
+ })
60
+ },
61
+ [setSearchParams],
62
+ )
63
+
64
+ const setShowMyUploadsSelected = useCallback(
65
+ (newDatasetStatusSelected: string) => {
66
+ setSearchParams((prevState) => ({
67
+ ...prevState,
68
+ datasetStatus_selected: newDatasetStatusSelected,
69
+ }))
70
+ },
71
+ [setSearchParams],
72
+ )
73
+
74
+ if (loggedOut) {
75
+ return null
76
+ }
77
+
78
+ return (
79
+ <>
80
+ <SlidingRadioGroup
81
+ items={updatedDatasetTypeAvailable}
82
+ selected={datasetType_selected}
83
+ setSelected={setShowSelected}
84
+ groupName="show-datasets"
85
+ className={datasetType_selected === "Admin: All Datasets"
86
+ ? "AdminAllDatasets"
87
+ : datasetType_selected.replace(/\s/g, "")}
88
+ initialSelectedValueOverride={searchAllDatasets
89
+ ? "Admin: All Datasets"
90
+ : undefined}
91
+ />
92
+
93
+ {datasetType_selected === "My Datasets" && !searchAllDatasets && (
94
+ <SlidingRadioGroup
95
+ items={datasetStatus_available}
96
+ selected={datasetStatus_selected}
97
+ setSelected={setShowMyUploadsSelected}
98
+ groupName="dataset-status"
99
+ />
100
+ )}
101
+ </>
102
+ )
103
+ }
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import { ModalityLabel } from "../formatting/modality-label"
2
+ import { ModalityLabel } from "../../components/formatting/modality-label"
3
3
 
4
4
  type TextList = string[]
5
5
  type Text = string
@@ -3,7 +3,8 @@ import { Button } from "../../components/button/Button"
3
3
  import { FilterListItem } from "./FilterListItem"
4
4
  import { TermListItem } from "./TermListItem"
5
5
  import type { FacetSelectValueType } from "../../components/facets/FacetSelect"
6
- import "./filters-block.scss"
6
+ import "../scss/filters-block.scss"
7
+ import { modalityShortMapping } from "../../components/formatting/modality-label"
7
8
 
8
9
  export interface FiltersBlockProps {
9
10
  keywords: string[]
@@ -69,17 +70,13 @@ export const FiltersBlock = ({
69
70
  const subjectCountRangeIsNull =
70
71
  JSON.stringify(subjectCountRange) === JSON.stringify([null, null])
71
72
 
73
+ const labelText = modalityShortMapping(modality_selected)
74
+
72
75
  return (
73
76
  <div className="filters-block">
74
77
  <h2>
75
78
  {noFilters
76
- ? (
77
- <b>
78
- Showing all available {modality_selected ? modality_selected : ""}
79
- {" "}
80
- datasets
81
- </b>
82
- )
79
+ ? <b>Showing all available {labelText ? labelText : ""} datasets</b>
83
80
  : (
84
81
  <>
85
82
  {loading
@@ -0,0 +1,31 @@
1
+ import React from "react"
2
+ import type { FC, ReactNode } from "react"
3
+
4
+ interface MetaListItemListProps {
5
+ typeLabel: ReactNode
6
+ items: (string | ReactNode)[]
7
+ }
8
+
9
+ /**
10
+ * A reusable component for rendering a list of meta items in the search results details.
11
+ */
12
+ export const MetaListItemList: FC<MetaListItemListProps> = (
13
+ { typeLabel, items },
14
+ ) => {
15
+ if (!items || items.length === 0) {
16
+ return null
17
+ }
18
+
19
+ return (
20
+ <div className="result-summary-meta">
21
+ <label>{typeLabel}:</label>
22
+ <div>
23
+ {items.map((item, index) => (
24
+ <span className="list-item" key={index}>
25
+ {item}
26
+ </span>
27
+ ))}
28
+ </div>
29
+ </div>
30
+ )
31
+ }
@@ -1,7 +1,7 @@
1
1
  import React from "react"
2
2
  import { ModalityHeader } from "./ModalityHeader"
3
3
  import { CommunityHeader } from "./CommunityHeader"
4
- import "./search-page.scss"
4
+ import "../scss/search-page.scss"
5
5
 
6
6
  export interface PortalContent {
7
7
  className?: string
@@ -15,6 +15,7 @@ export interface PortalContent {
15
15
  }
16
16
 
17
17
  export interface SearchPageProps {
18
+ hasDetailsOpen?: boolean
18
19
  portalContent?: PortalContent
19
20
  renderSearchFacets: () => React.ReactNode
20
21
  renderSearchResultsList: () => React.ReactNode
@@ -23,15 +24,18 @@ export interface SearchPageProps {
23
24
  renderSearchHeader: () => React.ReactNode
24
25
  renderLoading: () => React.ReactNode
25
26
  renderAggregateCounts: () => React.ReactNode
27
+ renderItemDetails: () => React.ReactNode
26
28
  }
27
29
 
28
30
  export const SearchPage = ({
31
+ hasDetailsOpen,
29
32
  portalContent,
30
33
  renderSearchFacets,
31
34
  renderSearchResultsList,
32
35
  renderSortBy,
33
36
  renderFilterBlock,
34
37
  renderSearchHeader,
38
+ renderItemDetails,
35
39
  renderLoading,
36
40
  renderAggregateCounts,
37
41
  }: SearchPageProps) => {
@@ -69,18 +73,14 @@ export const SearchPage = ({
69
73
  </>
70
74
  )
71
75
  : null}
72
- <div className="container">
76
+ <div className="container-full">
73
77
  <div className="grid grid-nogutter">
74
- <div className="col col-12 search-heading">
75
- <h1>{renderSearchHeader()}</h1>
76
- </div>
77
-
78
78
  <div className="col col-12 search-wrapper">
79
79
  <button
80
80
  className="show-filters-btn"
81
81
  onClick={() => setOpen(!isOpen)}
82
82
  >
83
- Show Filters
83
+ Show Additional Filters
84
84
  </button>
85
85
  <div
86
86
  className={isOpen
@@ -95,16 +95,23 @@ export const SearchPage = ({
95
95
  </button>
96
96
  {renderSearchFacets()}
97
97
  </div>
98
- <div className="search-content">
98
+ <div
99
+ className={`search-content ${
100
+ hasDetailsOpen ? " details-opened" : ""
101
+ }`}
102
+ >
103
+ <div className="search-heading">{renderSearchHeader()}</div>
99
104
  {renderLoading()}
100
105
  <div className="grid grid-nogutter">
101
106
  <div className="col col-12">{renderFilterBlock()}</div>
102
107
  <div className="col col-12">
103
108
  <div className="grid grid-nogutter">{renderSortBy()}</div>
104
109
  </div>
110
+
105
111
  {renderSearchResultsList()}
106
112
  </div>
107
113
  </div>
114
+ {renderItemDetails()}
108
115
  </div>
109
116
  </div>
110
117
  </div>
@@ -0,0 +1,167 @@
1
+ import React, { useEffect, useRef } from "react"
2
+ import type { FC, ReactNode } from "react"
3
+ import bytes from "bytes"
4
+ import parseISO from "date-fns/parseISO"
5
+ import formatDistanceToNow from "date-fns/formatDistanceToNow"
6
+ import { Link } from "react-router-dom"
7
+ import type { SearchResultItemProps } from "./SearchResultItem"
8
+ import { ModalityLabel } from "../../components/formatting/modality-label"
9
+ import { MetaListItemList } from "./MetaListItemList"
10
+ import "../scss/search-result-details.scss"
11
+
12
+ interface SearchResultDetailsProps {
13
+ itemData: SearchResultItemProps["node"] | null
14
+ onClose: () => void
15
+ }
16
+
17
+ export const SearchResultDetails: FC<SearchResultDetailsProps> = (
18
+ { itemData, onClose },
19
+ ) => {
20
+ const closeButtonRef = useRef<HTMLButtonElement>(null)
21
+
22
+ useEffect(() => {
23
+ if (itemData && closeButtonRef.current) {
24
+ closeButtonRef.current.focus()
25
+ }
26
+ }, [itemData])
27
+
28
+ if (!itemData) {
29
+ return null
30
+ }
31
+
32
+ const formatDate = (dateString: string): string => {
33
+ if (!dateString) return "N/A"
34
+ const date = new Date(dateString)
35
+ return date.toISOString().split("T")[0]
36
+ }
37
+
38
+ const summary = itemData.latestSnapshot?.summary
39
+ const numSessions = summary?.sessions?.length > 0
40
+ ? summary.sessions.length
41
+ : 1
42
+ const numSubjects = summary?.subjects?.length > 0
43
+ ? summary.subjects.length
44
+ : 1
45
+
46
+ // Header for more details
47
+ const moreDetailsHeader = (
48
+ <h4>
49
+ <Link to={"/datasets/" + itemData?.id}>
50
+ {itemData.latestSnapshot?.description?.Name || itemData.id}
51
+ </Link>
52
+ </h4>
53
+ )
54
+
55
+ // Lists
56
+ const modalityList = summary?.modalities?.length
57
+ ? (
58
+ <div className="modality-list">
59
+ <MetaListItemList
60
+ typeLabel={
61
+ <>{summary?.modalities.length === 1 ? "Modality" : "Modalities"}</>
62
+ }
63
+ items={summary?.modalities.map((modality) => (
64
+ <ModalityLabel key={modality} modality={modality} />
65
+ ))}
66
+ />
67
+ </div>
68
+ )
69
+ : null
70
+
71
+ const taskList = summary?.tasks?.length
72
+ ? (
73
+ <div className="task-list">
74
+ <MetaListItemList typeLabel={<>Tasks</>} items={summary?.tasks} />
75
+ </div>
76
+ )
77
+ : null
78
+
79
+ const tracers = summary?.pet?.TracerName?.length
80
+ ? (
81
+ <div className="tracers-list">
82
+ <MetaListItemList
83
+ typeLabel={
84
+ <>
85
+ {summary?.pet?.TracerName.length === 1
86
+ ? "Radiotracer"
87
+ : "Radiotracers"}
88
+ </>
89
+ }
90
+ items={summary?.pet?.TracerName}
91
+ />
92
+ </div>
93
+ )
94
+ : null
95
+
96
+ // function for consistent meta item rendering
97
+ const renderMetaItem = (
98
+ label: string | ReactNode,
99
+ content: ReactNode,
100
+ ): JSX.Element => (
101
+ <div className="result-summary-meta">
102
+ <label>{label}:&nbsp;</label>
103
+ {content}
104
+ </div>
105
+ )
106
+
107
+ const sessions = renderMetaItem("Sessions", numSessions.toLocaleString())
108
+ const subjects = renderMetaItem("Participants", numSubjects.toLocaleString())
109
+ const size = renderMetaItem(
110
+ "Size",
111
+ bytes(itemData?.latestSnapshot?.size) || "unknown",
112
+ )
113
+ const files = renderMetaItem("Files", summary?.totalFiles?.toLocaleString())
114
+ const lastUpdatedDisplay = renderMetaItem(
115
+ "Last Updated",
116
+ <div>
117
+ {formatDate(
118
+ itemData.snapshots?.[itemData.snapshots.length - 1]?.created ||
119
+ itemData.created,
120
+ )}
121
+ </div>,
122
+ )
123
+ const accessionNumberDisplay = renderMetaItem(
124
+ "Openneuro Accession Number",
125
+ <Link to={"/datasets/" + itemData?.id}>
126
+ {itemData?.id}
127
+ </Link>,
128
+ )
129
+ const authors = renderMetaItem(
130
+ "Authors",
131
+ <div>{itemData.latestSnapshot?.description?.Authors}</div>,
132
+ )
133
+ const uploaderDisplay = renderMetaItem(
134
+ "Uploader by",
135
+ <div>
136
+ {itemData.uploader?.name} on {formatDate(itemData?.created)} -{" "}
137
+ {formatDistanceToNow(parseISO(itemData?.created))} ago
138
+ </div>,
139
+ )
140
+
141
+ return (
142
+ <div className="search-details">
143
+ <div className="search-details-scroll">
144
+ <button
145
+ className="close-details-button"
146
+ onClick={onClose}
147
+ aria-label="Close details"
148
+ ref={closeButtonRef}
149
+ >
150
+ &times;
151
+ </button>
152
+ {moreDetailsHeader}
153
+ {authors}
154
+ {modalityList}
155
+ {taskList}
156
+ {accessionNumberDisplay}
157
+ {tracers}
158
+ {sessions}
159
+ {subjects}
160
+ {size}
161
+ {files}
162
+ {uploaderDisplay}
163
+ {lastUpdatedDisplay}
164
+ </div>
165
+ </div>
166
+ )
167
+ }