@openneuro/app 4.31.2 → 4.33.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.
Files changed (26) hide show
  1. package/package.json +4 -4
  2. package/src/@types/custom.d.ts +6 -0
  3. package/src/scripts/common/content/image-attribution.tsx +72 -0
  4. package/src/scripts/datalad/dataset/dataset-query-fragments.js +6 -4
  5. package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +3 -13
  6. package/src/scripts/dataset/components/ValidationBlock.tsx +12 -5
  7. package/src/scripts/dataset/components/__tests__/ValidationBlock.spec.tsx +17 -4
  8. package/src/scripts/dataset/draft-container.tsx +1 -1
  9. package/src/scripts/dataset/snapshot-container.tsx +1 -1
  10. package/src/scripts/pages/front-page/aggregate-queries/aggregate-counts-container.tsx +8 -5
  11. package/src/scripts/pages/front-page/aggregate-queries/use-publicDatasets-count.ts +24 -2
  12. package/src/scripts/pages/image-attribution.tsx +32 -0
  13. package/src/scripts/routes.tsx +2 -0
  14. package/src/scripts/search/__tests__/search-container.spec.tsx +8 -0
  15. package/src/scripts/search/filters-block-container.tsx +24 -5
  16. package/src/scripts/search/initial-search-params.tsx +6 -0
  17. package/src/scripts/search/inputs/__tests__/{nihselect.spec.tsx → initiative-select.spec.tsx} +6 -4
  18. package/src/scripts/search/inputs/index.ts +2 -2
  19. package/src/scripts/search/inputs/{nih-select.tsx → initiative-select.tsx} +23 -11
  20. package/src/scripts/search/inputs/show-datasets-radios.tsx +24 -19
  21. package/src/scripts/search/search-container.tsx +34 -10
  22. package/src/scripts/search/search-routes.tsx +4 -0
  23. package/src/scripts/validation-legacy/validation-legacy-query.ts +26 -0
  24. package/src/scripts/validation-legacy/{validation-results.jsx → validation-results.tsx} +35 -36
  25. package/src/scripts/validation-legacy/validation-status.jsx +36 -22
  26. package/src/scripts/validation-legacy/validation.jsx +6 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.31.2",
3
+ "version": "4.33.0",
4
4
  "description": "React JS web frontend for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "public/client.js",
@@ -16,11 +16,11 @@
16
16
  "dependencies": {
17
17
  "@apollo/client": "3.11.8",
18
18
  "@artsy/fresnel": "^1.3.1",
19
- "@bids/validator": "npm:@jsr/bids__validator@^2.0.2",
19
+ "@bids/validator": "npm:@jsr/bids__validator@^2.0.3",
20
20
  "@emotion/react": "11.11.1",
21
21
  "@emotion/styled": "11.11.0",
22
22
  "@niivue/niivue": "0.45.1",
23
- "@openneuro/components": "^4.31.2",
23
+ "@openneuro/components": "^4.33.0",
24
24
  "@sentry/react": "^8.25.0",
25
25
  "@tanstack/react-table": "^8.9.3",
26
26
  "buffer": "^6.0.3",
@@ -74,5 +74,5 @@
74
74
  "publishConfig": {
75
75
  "access": "public"
76
76
  },
77
- "gitHead": "d2665a438f56c1b2400f33073a43f2e996c286d1"
77
+ "gitHead": "ea2d4e9d00382b967075fb8fb206dce4f2a480af"
78
78
  }
@@ -1,4 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ // Allow .jpg imports
4
+ declare module "*.jpg" {
5
+ const value: string
6
+ export = value
7
+ }
2
8
  // Allow .png imports
3
9
  declare module "*.png" {
4
10
  const value: string
@@ -0,0 +1,72 @@
1
+ import React from "react"
2
+ import styled from "@emotion/styled"
3
+ import { Link } from "react-router-dom"
4
+
5
+ import nirs from "../../../../../openneuro-components/src/assets/nirs.jpg"
6
+ import nih from "../../../../../openneuro-components/src/assets/nih_cube.jpg"
7
+
8
+ /** Image attribution content. */
9
+ export function Attribution(): React.ReactElement {
10
+ const AtributionBlock = styled.div`
11
+ display: flex;
12
+ align-items: flex-start;
13
+ margin-bottom: 20px;
14
+ img {
15
+ width: 30%;
16
+ max-width: 200px;
17
+ margin-right: 20px;
18
+ height: auto;
19
+ }
20
+ label {
21
+ font-weight: bold;
22
+ }
23
+ p:first-of-type{ margin-top:0}
24
+ `
25
+
26
+ return (
27
+ <>
28
+ <AtributionBlock>
29
+ <img src={nirs} alt="" />
30
+ <p>
31
+ <div>
32
+ <label>Used with permission:</label>{" "}
33
+ <a href="https://mne.tools/mne-nirs/stable/index.html">
34
+ MNE-NIRS
35
+ </a>
36
+ </div>
37
+ <div>
38
+ <label>Locations used: {" "}</label>
39
+ <Link to="/">
40
+ OpenNeuro
41
+ </Link>
42
+ {", "}
43
+ <Link to="/search/modality/nirs?query=%7B%22modality_selected%22%3A%22NIRS%22%7D">
44
+ NIRS Modality Search
45
+ </Link>
46
+ </div>
47
+ </p>
48
+ </AtributionBlock>
49
+ <AtributionBlock>
50
+ <img src={nih} alt="" />
51
+ <p>
52
+ <div>
53
+ <label>Used with permission:</label>{" "}
54
+ <a href="https://braininitiative.nih.gov/">
55
+ NIH BRAIN Initiative<sup>&reg;</sup>
56
+ </a>
57
+ </div>
58
+ <div>
59
+ <label>Locations used: {" "}</label>
60
+ <Link to="/">
61
+ OpenNeuro
62
+ </Link>
63
+ {", "}
64
+ <Link to="/search/nih?query=%7B%22brain_initiative%22%3A%22true%22%7D">
65
+ NIH BRAIN Initiative Portal
66
+ </Link>
67
+ </div>
68
+ </p>
69
+ </AtributionBlock>
70
+ </>
71
+ )
72
+ }
@@ -131,8 +131,9 @@ export const DATASET_ISSUES = gql`
131
131
  id
132
132
  draft {
133
133
  id
134
- issues {
135
- ${ISSUE_FIELDS}
134
+ issuesStatus {
135
+ errors
136
+ warnings
136
137
  }
137
138
  validation {
138
139
  errors
@@ -145,8 +146,9 @@ export const DATASET_ISSUES = gql`
145
146
  export const SNAPSHOT_ISSUES = gql`
146
147
  fragment SnapshotIssues on Snapshot {
147
148
  id
148
- issues {
149
- ${ISSUE_FIELDS}
149
+ issuesStatus {
150
+ errors
151
+ warnings
150
152
  }
151
153
  validation {
152
154
  errors
@@ -159,23 +159,13 @@ exports[`SnapshotContainer component > renders successfully 1`] = `
159
159
  role="switch"
160
160
  >
161
161
  <div>
162
- <h3
163
- class="metaheader"
164
- >
165
- BIDS Validation
166
- </h3>
167
- <span
168
- class="label text-warning pull-right"
169
- >
170
- 1 Warning
171
- </span>
172
162
  <span
173
- class="dataset-status ds-success"
163
+ class="dataset-status ds-warning ds-validation-pending"
174
164
  >
175
165
  <i
176
- class="fa fa-check-circle"
166
+ class="fa fa-circle-o-notch fa-spin"
177
167
  />
178
- Valid
168
+ Validation Pending
179
169
  </span>
180
170
  </div>
181
171
  </div>
@@ -20,7 +20,10 @@ interface ValidationFragment {
20
20
  export interface ValidationBlockProps {
21
21
  datasetId: string
22
22
  version?: string
23
- issues?: object
23
+ issuesStatus?: {
24
+ errors: number
25
+ warnings: number
26
+ }
24
27
  validation?: ValidationFragment
25
28
  }
26
29
 
@@ -32,18 +35,22 @@ export interface ValidationBlockProps {
32
35
  export const ValidationBlock: React.FC<ValidationBlockProps> = ({
33
36
  datasetId,
34
37
  version,
35
- issues,
38
+ issuesStatus,
36
39
  validation,
37
40
  }) => {
38
- if (issues) {
41
+ if (issuesStatus) {
39
42
  return (
40
43
  <div className="validation-accordion">
41
- <LegacyValidation datasetId={datasetId} issues={issues} />
44
+ <LegacyValidation
45
+ datasetId={datasetId}
46
+ version={version}
47
+ issuesStatus={issuesStatus}
48
+ />
42
49
  </div>
43
50
  )
44
51
  } else {
45
52
  // If data exists, populate this. Otherwise we show pending.
46
- if (validation?.warnings + validation?.errors > 0) {
53
+ if (validation) {
47
54
  return (
48
55
  <div className="validation-accordion">
49
56
  <Validation
@@ -7,10 +7,13 @@ vi.mock("../../../config.ts")
7
7
 
8
8
  describe("ValidationBlock component", () => {
9
9
  it("renders legacy validation if issues prop is present", () => {
10
- const issues = [
11
- {},
12
- ]
13
- render(<ValidationBlock datasetId="ds000031" issues={issues} />)
10
+ render(
11
+ <ValidationBlock
12
+ datasetId="ds000031"
13
+ issuesStatus={{ warnings: 3, errors: 0 }}
14
+ version="1.0.0"
15
+ />,
16
+ )
14
17
  expect(screen.getByText("BIDS Validation")).toBeInTheDocument()
15
18
  })
16
19
  it("renders schema validation if validation prop is present", () => {
@@ -41,4 +44,14 @@ describe("ValidationBlock component", () => {
41
44
  render(<ValidationBlock datasetId="ds000031" />)
42
45
  expect(screen.getByText("Validation Pending")).toBeInTheDocument()
43
46
  })
47
+ it("renders validation if `validation` is present but errors and warnings are zero", () => {
48
+ render(
49
+ <ValidationBlock
50
+ datasetId="ds000031"
51
+ validation={{ errors: 0, warnings: 0, issues: [], codeMessages: [] }}
52
+ />,
53
+ )
54
+ expect(screen.getByText("Valid")).toBeInTheDocument()
55
+ expect(screen.queryByText("Validation Pending")).not.toBeInTheDocument()
56
+ })
44
57
  })
@@ -136,7 +136,7 @@ const DraftContainer: React.FC<DraftContainerProps> = ({ dataset }) => {
136
136
  <ValidationBlock
137
137
  datasetId={dataset.id}
138
138
  version={dataset.draft.head}
139
- issues={dataset.draft.issues}
139
+ issuesStatus={dataset.draft.issuesStatus}
140
140
  validation={dataset.draft.validation}
141
141
  />
142
142
  <CloneDropdown
@@ -136,7 +136,7 @@ export const SnapshotContainer: React.FC<SnapshotContainerProps> = ({
136
136
  <ValidationBlock
137
137
  datasetId={datasetId}
138
138
  version={snapshot.tag}
139
- issues={snapshot.issues}
139
+ issuesStatus={snapshot.issuesStatus}
140
140
  validation={snapshot.validation}
141
141
  />
142
142
  <AnalyzeDropdown
@@ -29,13 +29,16 @@ const AggregateCountsContainer: React.FC<AggregateCountsContainerProps> = ({
29
29
  else {
30
30
  return (
31
31
  <>
32
- <AggregateCount
33
- type="participants"
34
- count={participantData.participantCount}
35
- />
32
+ {participantData.participantCount > 0 && (
33
+ <AggregateCount
34
+ type="participants"
35
+ count={participantData.participantCount}
36
+ />
37
+ )}
36
38
  <AggregateCount
37
39
  type="publicDataset"
38
- count={publicDatasetsData.datasets?.pageInfo?.count || 0}
40
+ count={publicDatasetsData.datasets?.pageInfo?.count ||
41
+ publicDatasetsData.advancedSearch?.pageInfo.count || 0}
39
42
  />
40
43
  </>
41
44
  )
@@ -10,9 +10,31 @@ const PUBLIC_DATASETS_COUNT = gql`
10
10
  }
11
11
  `
12
12
 
13
+ const BRAIN_INITIATIVE_COUNT = gql`
14
+ query AdvancedSearch($query: JSON!, $datasetType: String!) {
15
+ advancedSearch(query: $query, datasetType: $datasetType) {
16
+ pageInfo {
17
+ count
18
+ }
19
+ }
20
+ }
21
+ `
22
+
13
23
  const usePublicDatasetsCount = (modality?: string) => {
14
- return useQuery(PUBLIC_DATASETS_COUNT, {
15
- variables: { modality },
24
+ const isNIH = modality === "NIH"
25
+
26
+ const query = isNIH ? BRAIN_INITIATIVE_COUNT : PUBLIC_DATASETS_COUNT
27
+ const variables = isNIH
28
+ ? {
29
+ query: {
30
+ bool: { filter: [{ match: { brainInitiative: { query: "true" } } }] },
31
+ },
32
+ datasetType: "public",
33
+ }
34
+ : { modality }
35
+
36
+ return useQuery(query, {
37
+ variables,
16
38
  errorPolicy: "all",
17
39
  })
18
40
  }
@@ -0,0 +1,32 @@
1
+ import React from "react"
2
+ import type { ReactElement } from "react"
3
+ import { Attribution } from "../common/content/image-attribution"
4
+ import Helmet from "react-helmet"
5
+ import { frontPage } from "./front-page/front-page-content"
6
+ import styled from "@emotion/styled"
7
+
8
+ const ImageAttributionStyle = styled.div`
9
+ background: white;
10
+
11
+ .container {
12
+ max-width: 60em;
13
+ }
14
+ `
15
+
16
+ export function ImageAttribution(): ReactElement {
17
+ return (
18
+ <ImageAttributionStyle>
19
+ <Helmet>
20
+ <title>Image Attribution - {frontPage.pageTitle}</title>
21
+ <meta
22
+ name="description"
23
+ content={`Image Attribution of the ${frontPage.pageTitle} data archive`}
24
+ />
25
+ </Helmet>
26
+ <div className="container">
27
+ <h2>OpenNeuro Image Attribution</h2>
28
+ <Attribution />
29
+ </div>
30
+ </ImageAttributionStyle>
31
+ )
32
+ }
@@ -17,6 +17,7 @@ import FourOFourPage from "./errors/404page"
17
17
  import { ImportDataset } from "./pages/import-dataset"
18
18
  import { DatasetMetadata } from "./pages/metadata/dataset-metadata"
19
19
  import { TermsPage } from "./pages/terms"
20
+ import { ImageAttribution } from "./pages/image-attribution"
20
21
  import { UserQuery } from "./users/user-query"
21
22
  import LoggedIn from "../scripts/authentication/logged-in"
22
23
  import LoggedOut from "../scripts/authentication/logged-out"
@@ -34,6 +35,7 @@ const AppRoutes: React.VoidFunctionComponent = () => (
34
35
  <Route path="/pet" element={<PETRedirect />} />
35
36
  <Route path="/cite" element={<Citation />} />
36
37
  <Route path="/terms" element={<TermsPage />} />
38
+ <Route path="/image-attribution" element={<ImageAttribution />} />
37
39
  <Route path="/import" element={<ImportDataset />} />
38
40
  <Route path="/metadata" element={<DatasetMetadata />} />
39
41
  <Route path="/public" element={<Navigate to="/search" replace />} />
@@ -8,6 +8,8 @@ describe("SearchContainer component", () => {
8
8
  const setContext = vi.fn()
9
9
  setDefaultSearch(
10
10
  "MRI",
11
+ "",
12
+ false,
11
13
  context,
12
14
  setContext,
13
15
  new URLSearchParams("/search"),
@@ -15,6 +17,8 @@ describe("SearchContainer component", () => {
15
17
  expect(setContext).not.toHaveBeenCalled()
16
18
  setDefaultSearch(
17
19
  "PET",
20
+ "",
21
+ false,
18
22
  context,
19
23
  setContext,
20
24
  new URLSearchParams("/search"),
@@ -31,6 +35,8 @@ describe("SearchContainer component", () => {
31
35
  })
32
36
  setDefaultSearch(
33
37
  "MRI",
38
+ "",
39
+ false,
34
40
  context,
35
41
  setContext,
36
42
  new URLSearchParams("mydatasets"),
@@ -47,6 +53,8 @@ describe("SearchContainer component", () => {
47
53
  })
48
54
  setDefaultSearch(
49
55
  "MRI",
56
+ "",
57
+ false,
50
58
  context,
51
59
  setContext,
52
60
  new URLSearchParams("bookmarks"),
@@ -1,4 +1,5 @@
1
1
  import React, { useContext } from "react"
2
+ import * as Sentry from "@sentry/react"
2
3
  import type { FC } from "react"
3
4
  import { useNavigate, useParams } from "react-router-dom"
4
5
  import {
@@ -15,6 +16,8 @@ interface FiltersBlockContainerProps {
15
16
  loading: boolean
16
17
  }
17
18
 
19
+ type FilterValue = string | number | boolean | string[] | number[]
20
+
18
21
  const FiltersBlockContainer: FC<FiltersBlockContainerProps> = ({
19
22
  numTotalResults,
20
23
  loading,
@@ -28,10 +31,25 @@ const FiltersBlockContainer: FC<FiltersBlockContainerProps> = ({
28
31
  const { path } = useParams()
29
32
  const globalSearchPath = "/search"
30
33
 
31
- const removeFilter = (isModality: boolean) => (param, value): void => {
32
- removeFilterItem(setSearchParams)(param, value)
33
- if (isModality) navigate(globalSearchPath)
34
- }
34
+ const removeFilter =
35
+ (isModality?: boolean) => (param: string, value: FilterValue): void => {
36
+ if (isModality && param === "modality_selected") {
37
+ removeFilterItem(setSearchParams)(param, value)
38
+ const queryParams = new URLSearchParams(location.search)
39
+ const query = queryParams.get("query")
40
+ if (query) {
41
+ try {
42
+ navigate(`${globalSearchPath}?${queryParams.toString()}`, {
43
+ replace: true,
44
+ })
45
+ } catch (error) {
46
+ Sentry.captureException(error)
47
+ }
48
+ }
49
+ } else {
50
+ removeFilterItem(setSearchParams)(param, value)
51
+ }
52
+ }
35
53
 
36
54
  const removeAllFilters = (): void => {
37
55
  // reset params to default values
@@ -39,8 +57,9 @@ const FiltersBlockContainer: FC<FiltersBlockContainerProps> = ({
39
57
  ...prevState,
40
58
  ...getSelectParams(initialSearchParams),
41
59
  }))
42
- if (path !== globalSearchPath) navigate(globalSearchPath)
60
+ if (path !== globalSearchPath) navigate(globalSearchPath, { replace: true })
43
61
  }
62
+
44
63
  return (
45
64
  <FiltersBlock
46
65
  noFilters={noFilters}
@@ -92,6 +92,12 @@ export const modality_available: ModalityOption[] = [
92
92
  portalPath: "/search/modality/meg",
93
93
  count: null,
94
94
  },
95
+ {
96
+ label: "NIRS",
97
+ value: "NIRS",
98
+ portalPath: "/search/modality/nirs",
99
+ count: null,
100
+ },
95
101
  ]
96
102
 
97
103
  export const flattenedModalities = modality_available.reduce(
@@ -2,7 +2,7 @@ import React from "react"
2
2
  import { fireEvent, render, screen, waitFor } from "@testing-library/react"
3
3
  import { MemoryRouter, Route, Routes } from "react-router-dom"
4
4
  import { SearchParamsCtx } from "../../search-params-ctx"
5
- import NIHSelect from "../nih-select"
5
+ import InitiativeSelect from "../initiative-select"
6
6
 
7
7
  const customRender = (
8
8
  ui,
@@ -24,13 +24,15 @@ const customRender = (
24
24
 
25
25
  describe("NIHSelect Component", () => {
26
26
  it("updates search params when NIH is selected and navigates to the NIH page", async () => {
27
- customRender(<NIHSelect label="NIH Funding" />)
27
+ customRender(
28
+ <InitiativeSelect label="Initiative" />,
29
+ )
28
30
 
29
- const nihOption = screen.getByText("NIH Funding")
31
+ const nihOption = screen.getByText("Initiative")
30
32
  fireEvent.click(nihOption)
31
33
 
32
34
  await waitFor(() => {
33
- expect(screen.getByText("NIH Brain Initiative")).toBeInTheDocument()
35
+ expect(screen.getByText("Initiative")).toBeInTheDocument()
34
36
  })
35
37
  })
36
38
  })
@@ -1,7 +1,7 @@
1
1
  import KeywordInput from "./keyword-input"
2
2
  import AllDatasetsToggle from "./admin-allDatasets-toggle"
3
3
  import ModalitySelect from "./modality-select"
4
- import NIHSelect from "./nih-select"
4
+ import InitiativeSelect from "./initiative-select"
5
5
  import ShowDatasetRadios from "./show-datasets-radios"
6
6
  import AgeRangeInput from "./age-range-input"
7
7
  import SubjectCountRangeInput from "./subject-count-range-input"
@@ -29,9 +29,9 @@ export {
29
29
  DatasetTypeSelect,
30
30
  DateRadios,
31
31
  DiagnosisSelect,
32
+ InitiativeSelect,
32
33
  KeywordInput,
33
34
  ModalitySelect,
34
- NIHSelect,
35
35
  ScannerManufacturers,
36
36
  ScannerManufacturersModelNames,
37
37
  SectionSelect,
@@ -2,13 +2,14 @@ import React, { useContext, useEffect } from "react"
2
2
  import type { FC } from "react"
3
3
  import { useLocation, useNavigate } from "react-router-dom"
4
4
  import { SearchParamsCtx } from "../search-params-ctx"
5
+ import { AccordionTab, AccordionWrap } from "@openneuro/components/accordion"
5
6
  import { SingleSelect } from "@openneuro/components/facets"
6
7
 
7
- interface NIHSelectProps {
8
- label?: string
8
+ interface InitiativeSelectProps {
9
+ label: string
9
10
  }
10
11
 
11
- const NIHSelect: FC<NIHSelectProps> = ({ label }) => {
12
+ const InitiativeSelect: FC<InitiativeSelectProps> = ({ label }) => {
12
13
  const { searchParams, setSearchParams } = useContext(SearchParamsCtx)
13
14
  const navigate = useNavigate()
14
15
  const location = useLocation()
@@ -50,14 +51,25 @@ const NIHSelect: FC<NIHSelectProps> = ({ label }) => {
50
51
  }, [brain_initiative, location.search, navigate])
51
52
 
52
53
  return (
53
- <SingleSelect
54
- className="nih-facet facet-open"
55
- label={label}
56
- selected={brain_initiative === "true" ? "NIH" : ""}
57
- setSelected={setBrainInitiative}
58
- items={["NIH"]}
59
- />
54
+ <>
55
+ <AccordionWrap className="modality-facet facet-accordion">
56
+ <AccordionTab
57
+ accordionStyle="plain"
58
+ label={label}
59
+ startOpen={false}
60
+ dropdown={false}
61
+ >
62
+ <SingleSelect
63
+ className="nih-facet facet-open"
64
+ selected={brain_initiative === "true" ? "NIH" : ""}
65
+ setSelected={setBrainInitiative}
66
+ items={["NIH"]}
67
+ label="NIH BRAIN Initiative"
68
+ />
69
+ </AccordionTab>
70
+ </AccordionWrap>
71
+ </>
60
72
  )
61
73
  }
62
74
 
63
- export default NIHSelect
75
+ export default InitiativeSelect
@@ -19,16 +19,23 @@ const ShowDatasetsRadios: FC = () => {
19
19
  datasetStatus_available,
20
20
  datasetStatus_selected,
21
21
  } = searchParams
22
- const setShowSelected = (datasetType_selected) =>
22
+
23
+ const setShowSelected = (datasetType_selected) => {
23
24
  setSearchParams((prevState) => ({
24
25
  ...prevState,
25
26
  datasetType_selected,
27
+ datasetStatus_selected: datasetType_selected === "My Datasets"
28
+ ? prevState.datasetStatus_selected
29
+ : undefined,
26
30
  }))
27
- const setShowMyUploadsSelected = (datasetStatus_selected) =>
31
+ }
32
+
33
+ const setShowMyUploadsSelected = (datasetStatus_selected) => {
28
34
  setSearchParams((prevState) => ({
29
35
  ...prevState,
30
36
  datasetStatus_selected,
31
37
  }))
38
+ }
32
39
 
33
40
  return loggedOut ? null : (
34
41
  <>
@@ -44,23 +51,21 @@ const ShowDatasetsRadios: FC = () => {
44
51
  setSelected={setShowSelected}
45
52
  />
46
53
  </div>
47
- {datasetType_selected == "My Datasets"
48
- ? (
49
- <AccordionWrap className="facet-accordion">
50
- <AccordionTab
51
- accordionStyle="plain"
52
- label="My Datasets Status"
53
- startOpen={true}
54
- >
55
- <FacetSelect
56
- selected={datasetStatus_selected}
57
- setSelected={setShowMyUploadsSelected}
58
- items={datasetStatus_available}
59
- />
60
- </AccordionTab>
61
- </AccordionWrap>
62
- )
63
- : null}
54
+ {datasetType_selected === "My Datasets" && (
55
+ <AccordionWrap className="facet-accordion">
56
+ <AccordionTab
57
+ accordionStyle="plain"
58
+ label="My Datasets Status"
59
+ startOpen={true}
60
+ >
61
+ <FacetSelect
62
+ selected={datasetStatus_selected}
63
+ setSelected={setShowMyUploadsSelected}
64
+ items={datasetStatus_available}
65
+ />
66
+ </AccordionTab>
67
+ </AccordionWrap>
68
+ )}
64
69
  </>
65
70
  )
66
71
  }
@@ -20,9 +20,9 @@ import {
20
20
  DatasetTypeSelect,
21
21
  DateRadios,
22
22
  DiagnosisSelect,
23
+ InitiativeSelect,
23
24
  KeywordInput,
24
25
  ModalitySelect,
25
- NIHSelect,
26
26
  ScannerManufacturers,
27
27
  ScannerManufacturersModelNames,
28
28
  SectionSelect,
@@ -55,6 +55,8 @@ export interface SearchContainerProps {
55
55
  */
56
56
  export const setDefaultSearch = (
57
57
  modality: string,
58
+ grant: string,
59
+ is_grant_portal: boolean,
58
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
61
  searchParams: Record<string, any>,
60
62
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -84,20 +86,31 @@ export const setDefaultSearch = (
84
86
  EEG: ["EEG"],
85
87
  iEEG: ["iEEG"],
86
88
  MEG: ["MEG"],
89
+ NIRS: ["NIRS"],
87
90
  NIH: ["NIH"],
88
91
  }
92
+
89
93
  if (
90
94
  modality &&
91
- !modalitiesWithSecondaries[modality].includes(
95
+ !modalitiesWithSecondaries[modality]?.includes(
92
96
  searchParams.modality_selected,
93
97
  )
94
98
  ) {
95
- setSearchParams(
96
- (prevState: SearchParams): SearchParams => ({
97
- ...prevState,
98
- modality_selected: modality,
99
- }),
100
- )
99
+ setSearchParams((prevState) => ({
100
+ ...prevState,
101
+ modality_selected: modality,
102
+ }))
103
+ }
104
+
105
+ // Check for grant-related conditions
106
+ if (
107
+ is_grant_portal && grant === "nih" &&
108
+ searchParams.brain_initiative !== "true"
109
+ ) {
110
+ setSearchParams((prevState) => ({
111
+ ...prevState,
112
+ brain_initiative: "true",
113
+ }))
101
114
  }
102
115
  }
103
116
 
@@ -109,15 +122,26 @@ const SearchContainer: FC<SearchContainerProps> = ({ portalContent }) => {
109
122
  const { searchParams, setSearchParams } = useContext(SearchParamsCtx)
110
123
  const modality = portalContent?.modality || null
111
124
  const selected_grant = portalContent?.portalName || null
125
+ const is_grant_portal = portalContent?.portal || false
126
+ const grant = portalContent?.grant || null
112
127
 
113
128
  useEffect(() => {
114
129
  setDefaultSearch(
115
130
  modality,
131
+ grant,
132
+ is_grant_portal,
116
133
  searchParams,
117
134
  setSearchParams,
118
135
  new URLSearchParams(location.search),
119
136
  )
120
- }, [modality, searchParams, setSearchParams, location.search])
137
+ }, [
138
+ modality,
139
+ grant,
140
+ is_grant_portal,
141
+ searchParams,
142
+ setSearchParams,
143
+ location.search,
144
+ ])
121
145
 
122
146
  const { loading, data, fetchMore, variables } = useSearchResults()
123
147
  const loadMore = loading ? () => {} : () => {
@@ -166,7 +190,6 @@ const SearchContainer: FC<SearchContainerProps> = ({ portalContent }) => {
166
190
  )}
167
191
  renderSearchFacets={() => (
168
192
  <>
169
- <NIHSelect label={"Search NIH Brain Initiative Datasets"} />
170
193
  <NeurobagelSearch />
171
194
  <KeywordInput />
172
195
  <AdminUser>
@@ -176,6 +199,7 @@ const SearchContainer: FC<SearchContainerProps> = ({ portalContent }) => {
176
199
  {!portalContent
177
200
  ? <ModalitySelect portalStyles={true} label="Modalities" />
178
201
  : <ModalitySelect portalStyles={false} label="Choose Modality" />}
202
+ <InitiativeSelect label="Initiatives" />
179
203
  <DatasetTypeSelect />
180
204
  <AgeRangeInput />
181
205
  <SubjectCountRangeInput />
@@ -30,6 +30,10 @@ const SearchRoutes: FC = () => (
30
30
  path="/modality/pet"
31
31
  element={<SearchContainer portalContent={portalContent.pet} />}
32
32
  />
33
+ <Route
34
+ path="/modality/nirs"
35
+ element={<SearchContainer portalContent={portalContent.nirs} />}
36
+ />
33
37
  <Route
34
38
  path="/nih"
35
39
  element={<SearchContainer portalContent={portalGrantContent.nih} />}
@@ -0,0 +1,26 @@
1
+ import { gql, useQuery } from "@apollo/client"
2
+ import { ISSUE_FIELDS } from "../datalad/dataset/dataset-query-fragments"
3
+
4
+ export const LEGACY_VALIDATION_DATA_QUERY = gql`
5
+ query ($datasetId: ID!, $version: String!) {
6
+ snapshot(datasetId: $datasetId, tag: $version) {
7
+ issues {
8
+ ${ISSUE_FIELDS}
9
+ }
10
+ }
11
+ }
12
+ `
13
+
14
+ export const useLegacyValidationResults = (
15
+ datasetId: string,
16
+ version: string,
17
+ ) => {
18
+ const { loading, error, data } = useQuery(LEGACY_VALIDATION_DATA_QUERY, {
19
+ variables: { datasetId, version },
20
+ })
21
+ if (!loading && data) {
22
+ return { loading, error, issues: data.snapshot.issues }
23
+ } else {
24
+ return { loading, error, issues: [] }
25
+ }
26
+ }
@@ -1,29 +1,51 @@
1
1
  // dependencies -----------------------------------------------------------
2
2
 
3
3
  import React from "react"
4
- import PropTypes from "prop-types"
5
4
  import pluralize from "pluralize"
6
5
  import { AccordionTab, AccordionWrap } from "@openneuro/components/accordion"
7
-
8
6
  import Issues from "./validation-results.issues.jsx"
7
+ import { useLegacyValidationResults } from "./validation-legacy-query.js"
8
+ import { Loading } from "@openneuro/components/loading"
9
9
 
10
- // component setup --------------------------------------------------------
10
+ function countFiles(issues) {
11
+ let numFiles = 0
12
+ for (const issue of issues) {
13
+ numFiles += Array.from(issue.files).length
14
+ if (issue.additionalFileCount) {
15
+ numFiles += issue.additionalFileCount
16
+ }
17
+ }
18
+ return numFiles
19
+ }
11
20
 
12
- class ValidationResults extends React.Component {
13
- // life cycle events ------------------------------------------------------
21
+ interface ValidationResultsProp {
22
+ datasetId: string
23
+ version: string
24
+ }
14
25
 
15
- render() {
16
- const errors = this.props.errors
17
- const warnings = this.props.warnings
26
+ export function ValidationResults(
27
+ { datasetId, version }: ValidationResultsProp,
28
+ ) {
29
+ const { loading, issues, error } = useLegacyValidationResults(
30
+ datasetId,
31
+ version,
32
+ )
18
33
 
19
- if (errors === "Invalid") {
20
- return false
21
- }
34
+ if (loading || error) {
35
+ return (
36
+ <>
37
+ <Loading />
38
+ <span className="message">Loading validation results...</span>
39
+ </>
40
+ )
41
+ } else {
42
+ const warnings = issues.filter((issue) => issue.severity === "warning")
43
+ const errors = issues.filter((issue) => issue.severity === "error")
22
44
 
23
45
  // errors
24
46
  let errorsWrap
25
47
  if (errors.length > 0) {
26
- const fileCount = this._countFiles(errors)
48
+ const fileCount = countFiles(errors)
27
49
  const errorHeader = (
28
50
  <span>
29
51
  view {errors.length} {pluralize("error", errors.length)} in{" "}
@@ -45,7 +67,7 @@ class ValidationResults extends React.Component {
45
67
  //warnings
46
68
  let warningWrap
47
69
  if (warnings && warnings.length > 0) {
48
- const fileCount = this._countFiles(warnings)
70
+ const fileCount = countFiles(warnings)
49
71
  const warningHeader = (
50
72
  <span>
51
73
  view {warnings.length} {pluralize("warning", warnings.length)} in{" "}
@@ -72,29 +94,6 @@ class ValidationResults extends React.Component {
72
94
  </AccordionWrap>
73
95
  )
74
96
  }
75
-
76
- // custom methods ---------------------------------------------------------
77
-
78
- _countFiles(issues) {
79
- let numFiles = 0
80
- for (const issue of issues) {
81
- numFiles += Array.from(issue.files).length
82
- if (issue.additionalFileCount) {
83
- numFiles += issue.additionalFileCount
84
- }
85
- }
86
- return numFiles
87
- }
88
- }
89
-
90
- ValidationResults.Props = {
91
- errors: [],
92
- warnings: [],
93
- }
94
-
95
- ValidationResults.propTypes = {
96
- errors: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
97
- warnings: PropTypes.array,
98
97
  }
99
98
 
100
99
  export default ValidationResults
@@ -2,7 +2,7 @@ import React from "react"
2
2
  import PropTypes from "prop-types"
3
3
  import pluralize from "pluralize"
4
4
  import ValidationPanel from "./validation-panel.jsx"
5
- import Results from "./validation-results.jsx"
5
+ import Results from "./validation-results"
6
6
 
7
7
  /**
8
8
  * These can't be React components due to legacy react-bootstrap
@@ -52,54 +52,68 @@ const Valid = () => (
52
52
  </div>
53
53
  )
54
54
 
55
- const Warnings = ({ errors, warnings }) => (
56
- <ValidationPanel heading={warningHeader(warnings.length)}>
55
+ const Warnings = ({ datasetId, version, issuesStatus }) => (
56
+ <ValidationPanel heading={warningHeader(issuesStatus.warnings)}>
57
57
  <div>
58
58
  <span className="message error fade-in">
59
59
  We found{" "}
60
60
  <strong>
61
- {warnings.length + " " + pluralize("Warning", warnings.length)}
61
+ {issuesStatus.warnings + " " +
62
+ pluralize("Warning", issuesStatus.warnings)}
62
63
  </strong>{" "}
63
64
  in your dataset. You are not required to fix warnings, but doing so will
64
65
  make your dataset more BIDS compliant.
65
66
  </span>
66
67
  </div>
67
68
  <br />
68
- <Results errors={errors} warnings={warnings} />
69
+ <Results datasetId={datasetId} version={version} />
69
70
  </ValidationPanel>
70
71
  )
71
72
 
72
73
  Warnings.propTypes = {
73
- errors: PropTypes.array,
74
- warnings: PropTypes.array,
74
+ issuesStatus: PropTypes.object,
75
+ datasetId: PropTypes.string,
76
+ version: PropTypes.string,
75
77
  }
76
78
 
77
- const Errors = ({ errors, warnings }) => (
78
- <ValidationPanel heading={errorHeader(errors.length)}>
79
+ const Errors = ({ datasetId, version, issuesStatus }) => (
80
+ <ValidationPanel heading={errorHeader(issuesStatus.errors)}>
79
81
  <span className="message error fade-in">
80
82
  Your dataset is no longer valid. You must fix the{" "}
81
- <strong>{errors.length + " " + pluralize("Error", errors.length)}</strong>
82
- {" "}
83
+ <strong>
84
+ {issuesStatus.errors + " " + pluralize("Error", issuesStatus.errors)}
85
+ </strong>{" "}
83
86
  to use all of the site features.
84
87
  </span>
85
88
  <br />
86
- <Results errors={errors} warnings={warnings} />
89
+ <Results datasetId={datasetId} version={version} />
87
90
  </ValidationPanel>
88
91
  )
89
92
 
90
93
  Errors.propTypes = {
91
- errors: PropTypes.array,
92
- warnings: PropTypes.array,
94
+ issuesStatus: PropTypes.object,
95
+ datasetId: PropTypes.string,
96
+ version: PropTypes.string,
93
97
  }
94
98
 
95
- const ValidationStatus = ({ issues }) => {
96
- if (issues) {
97
- const warnings = issues.filter((issue) => issue.severity === "warning")
98
- const errors = issues.filter((issue) => issue.severity === "error")
99
- if (errors.length) {
100
- return <Errors errors={errors} warnings={warnings} />
101
- } else if (warnings.length) {
102
- return <Warnings errors={errors} warnings={warnings} />
99
+ const ValidationStatus = ({ issuesStatus, datasetId, version }) => {
100
+ if (issuesStatus) {
101
+ if (issuesStatus.errors > 0) {
102
+ return (
103
+ <Errors
104
+ issuesStatus={issuesStatus}
105
+ datasetId={datasetId}
106
+ version={version}
107
+ />
108
+ )
109
+ } else if (issuesStatus.warnings > 0) {
110
+ return (
111
+ <Warnings
112
+ issuesStatus={issuesStatus}
113
+ datasetId={datasetId}
114
+ version={version}
115
+ />
116
+ )
103
117
  } else {
104
118
  return <Valid />
105
119
  }
@@ -3,10 +3,14 @@ import PropTypes from "prop-types"
3
3
  import ValidationStatus from "./validation-status.jsx"
4
4
  import ErrorBoundary from "../errors/errorBoundary.jsx"
5
5
 
6
- const Validation = ({ issues }) => (
6
+ const Validation = ({ issuesStatus, datasetId, version }) => (
7
7
  <>
8
8
  <ErrorBoundary subject={"error in dataset validation component"}>
9
- <ValidationStatus issues={issues} />
9
+ <ValidationStatus
10
+ issuesStatus={issuesStatus}
11
+ datasetId={datasetId}
12
+ version={version}
13
+ />
10
14
  </ErrorBoundary>
11
15
  </>
12
16
  )