@openneuro/app 4.21.3 → 4.22.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.21.3",
3
+ "version": "4.22.0-alpha.0",
4
4
  "description": "React JS web frontend for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "public/client.js",
@@ -20,18 +20,19 @@
20
20
  "@emotion/react": "11.11.1",
21
21
  "@emotion/styled": "11.11.0",
22
22
  "@niivue/niivue": "0.34.0",
23
- "@openneuro/client": "^4.21.3",
24
- "@openneuro/components": "^4.21.3",
23
+ "@openneuro/client": "^4.22.0-alpha.0",
24
+ "@openneuro/components": "^4.22.0-alpha.0",
25
25
  "@tanstack/react-table": "^8.9.3",
26
26
  "bids-validator": "1.13.0",
27
27
  "bytes": "^3.0.0",
28
28
  "comlink": "^4.0.5",
29
29
  "date-fns": "^2.16.1",
30
+ "dompurify": "^3.0.8",
30
31
  "draft-js": "^0.11.7",
31
32
  "email-validator": "^2.0.4",
32
33
  "graphql": "16.8.1",
33
34
  "jwt-decode": "^2.2.0",
34
- "markdown-to-jsx": "^7.1.2",
35
+ "markdown-to-jsx": "^7.4.0",
35
36
  "pluralize": "8.0.0",
36
37
  "prop-types": "^15.6.0",
37
38
  "react": "^17.0.1",
@@ -49,6 +50,7 @@
49
50
  "devDependencies": {
50
51
  "@testing-library/jest-dom": "6.1.3",
51
52
  "@testing-library/react": "^11.1.0",
53
+ "@types/dompurify": "^3",
52
54
  "@types/jsdom": "^16",
53
55
  "@types/node": "18.11.9",
54
56
  "@types/react": "^17.0.8",
@@ -64,11 +66,11 @@
64
66
  "sass": "^1.32.8",
65
67
  "stream-browserify": "^3.0.0",
66
68
  "typescript": "5.1.6",
67
- "vite": "4.4.12",
69
+ "vite": "4.5.2",
68
70
  "vitest": "0.34.4"
69
71
  },
70
72
  "publishConfig": {
71
73
  "access": "public"
72
74
  },
73
- "gitHead": "a1bc17ea73966bc27be4a4bed41729cbdee20beb"
75
+ "gitHead": "9931842bb7afcac659af2a7b5a5d045aae1f4776"
74
76
  }
@@ -31,11 +31,18 @@ export const HeaderContainer: FC = () => {
31
31
  const [newKeyword, setNewKeyword, newKeywordRef] = useState("")
32
32
 
33
33
  const handleSubmit = () => {
34
- const query = JSON.stringify({
34
+ const newQuery = {
35
35
  keywords: newKeywordRef.current ? [newKeywordRef.current] : [],
36
- })
36
+ }
37
+ const query = JSON.stringify(newQuery)
37
38
  setNewKeyword("")
38
- navigate(`/search?query=${query}`)
39
+ if (
40
+ newQuery?.keywords?.length && newQuery.keywords[0].match(/^ds[0-9]{6,6}$/)
41
+ ) {
42
+ navigate(`/datasets/${newQuery.keywords[0]}`)
43
+ } else {
44
+ navigate(`/search?query=${query}`)
45
+ }
39
46
  }
40
47
 
41
48
  const toggleLoginModal = (): void => {
@@ -156,6 +156,7 @@ exports[`SnapshotContainer component > renders successfully 1`] = `
156
156
  >
157
157
  <div
158
158
  class="accordion-title "
159
+ role="switch"
159
160
  >
160
161
  <div>
161
162
  <h3
@@ -204,6 +205,7 @@ exports[`SnapshotContainer component > renders successfully 1`] = `
204
205
  >
205
206
  <div
206
207
  class="accordion-title "
208
+ role="switch"
207
209
  >
208
210
  <span>
209
211
  view 1 warning in 1 file
@@ -223,6 +225,7 @@ exports[`SnapshotContainer component > renders successfully 1`] = `
223
225
  >
224
226
  <div
225
227
  class="accordion-title "
228
+ role="switch"
226
229
  >
227
230
  <span
228
231
  class="file-header"
@@ -556,6 +559,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
556
559
  >
557
560
  <div
558
561
  class="accordion-title "
562
+ role="switch"
559
563
  >
560
564
  <span
561
565
  aria-label="DS003-downsampled (only T1)"
@@ -675,6 +679,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
675
679
  >
676
680
  <div
677
681
  class="accordion-title "
682
+ role="switch"
678
683
  >
679
684
  <span
680
685
  aria-label="sub-01"
@@ -708,6 +713,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
708
713
  >
709
714
  <div
710
715
  class="accordion-title "
716
+ role="switch"
711
717
  >
712
718
  <span
713
719
  aria-label="sub-02"
@@ -766,6 +772,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
766
772
  >
767
773
  <div
768
774
  class="accordion-title open"
775
+ role="switch"
769
776
  >
770
777
  <span
771
778
  aria-label="DS003-downsampled (only T1)"
@@ -885,6 +892,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
885
892
  >
886
893
  <div
887
894
  class="accordion-title "
895
+ role="switch"
888
896
  >
889
897
  <span
890
898
  aria-label="sub-01"
@@ -918,6 +926,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
918
926
  >
919
927
  <div
920
928
  class="accordion-title "
929
+ role="switch"
921
930
  >
922
931
  <span
923
932
  aria-label="sub-02"
@@ -1279,6 +1288,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
1279
1288
  >
1280
1289
  Funding
1281
1290
  </h2>
1291
+ <ul />
1282
1292
  </div>
1283
1293
  <div
1284
1294
  class="dataset-meta-block dmb-list"
@@ -1288,6 +1298,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
1288
1298
  >
1289
1299
  References and Links
1290
1300
  </h2>
1301
+ <ul />
1291
1302
  </div>
1292
1303
  <div
1293
1304
  class="dataset-meta-block dmb-list"
@@ -1297,6 +1308,7 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
1297
1308
  >
1298
1309
  Ethics Approvals
1299
1310
  </h2>
1311
+ <ul />
1300
1312
  </div>
1301
1313
  </div>
1302
1314
  </div>
@@ -1,4 +1,4 @@
1
- import Markdown from "markdown-to-jsx"
1
+ import { Markdown } from "../../utils/markdown"
2
2
  import React from "react"
3
3
 
4
4
  export interface MetaDataBlockProps {
@@ -1,4 +1,5 @@
1
1
  import React from "react"
2
+ import { Markdown } from "../../utils/markdown"
2
3
 
3
4
  export interface MetaDataListBlockProps {
4
5
  heading: string
@@ -17,7 +18,15 @@ export const MetaDataListBlock = ({
17
18
  return (
18
19
  <div className={"dataset-meta-block " + className}>
19
20
  <h2 className="dmb-heading">{heading}</h2>
20
- {fieldContent}
21
+ <ul>
22
+ {Array.isArray(fieldContent)
23
+ ? fieldContent.map((item, index) => (
24
+ <li key={index}>
25
+ <Markdown>{item}</Markdown>
26
+ </li>
27
+ ))
28
+ : item}
29
+ </ul>
21
30
  </div>
22
31
  )
23
32
  }
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import Markdown from "markdown-to-jsx"
2
+ import { Markdown } from "../utils/markdown"
3
3
  import Helmet from "react-helmet"
4
4
  import { Navigate, useLocation } from "react-router-dom"
5
5
  import pluralize from "pluralize"
@@ -7,6 +7,7 @@ exports[`FileTree component > renders with default props 1`] = `
7
7
  >
8
8
  <div
9
9
  class="accordion-title "
10
+ role="switch"
10
11
  >
11
12
  <span
12
13
  aria-label=""
@@ -2,7 +2,7 @@ import React, { useState } from "react"
2
2
  import UpdateDescription from "../mutations/description.jsx"
3
3
  import { CancelButton } from "./cancel-button"
4
4
  import { EditButton } from "./edit-button"
5
- import Markdown from "markdown-to-jsx"
5
+ import { Markdown } from "../../utils/markdown"
6
6
 
7
7
  import EditList from "./edit-list.jsx"
8
8
 
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import Markdown from "markdown-to-jsx"
2
+ import { Markdown } from "../../utils/markdown"
3
3
  import { ReadMore } from "@openneuro/components/read-more"
4
4
  import { MetaDataBlock } from "../components/MetaDataBlock"
5
5
  import Files from "../files/files"
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import Markdown from "markdown-to-jsx"
2
+ import { Markdown } from "../../utils/markdown"
3
3
  import { ReadMore } from "@openneuro/components/read-more"
4
4
  import { MetaDataBlock } from "../components/MetaDataBlock"
5
5
  import Files from "../files/files"
@@ -17,6 +17,7 @@ import { DatasetGitAccess } from "./components/DatasetGitAccess"
17
17
  import { DatasetHeader } from "./components/DatasetHeader"
18
18
  import { DatasetTools } from "./components/DatasetTools"
19
19
  import { MetaDataBlock } from "./components/MetaDataBlock"
20
+ import { MetaDataListBlock } from "./components/MetaDataListBlock"
20
21
  import { ModalitiesMetaDataBlock } from "./components/ModalitiesMetaDataBlock"
21
22
  import { ValidationBlock } from "./components/ValidationBlock"
22
23
  import { VersionList } from "./components/VersionList"
@@ -316,19 +317,19 @@ export const SnapshotContainer: React.FC<SnapshotContainerProps> = ({
316
317
  heading="How to Acknowledge"
317
318
  item={description.HowToAcknowledge}
318
319
  />
319
- <MetaDataBlock
320
+ <MetaDataListBlock
320
321
  heading="Funding"
321
322
  item={description.Funding}
322
323
  className="dmb-list"
323
324
  />
324
325
 
325
- <MetaDataBlock
326
+ <MetaDataListBlock
326
327
  heading="References and Links"
327
328
  item={description.ReferencesAndLinks}
328
329
  className="dmb-list"
329
330
  />
330
331
 
331
- <MetaDataBlock
332
+ <MetaDataListBlock
332
333
  heading="Ethics Approvals"
333
334
  item={description.EthicsApprovals}
334
335
  className="dmb-list"
@@ -10,6 +10,7 @@ import {
10
10
  } from "../authentication/profile"
11
11
  import { Button } from "@openneuro/components/button"
12
12
  import { Loading } from "@openneuro/components/loading"
13
+ import { NeurobagelSearch } from "@openneuro/components/search-page"
13
14
  import {
14
15
  AgeRangeInput,
15
16
  AllDatasetsToggle,
@@ -156,6 +157,7 @@ const SearchContainer: FC<SearchContainerProps> = ({ portalContent }) => {
156
157
  )}
157
158
  renderSearchFacets={() => (
158
159
  <>
160
+ <NeurobagelSearch />
159
161
  <KeywordInput />
160
162
  <AdminUser>
161
163
  <AllDatasetsToggle />
@@ -157,7 +157,7 @@ export const useSearchResults = () => {
157
157
  boolQuery.addClause(
158
158
  "must",
159
159
  simpleQueryString(sqsJoinWithAND(keywords), [
160
- "id^6",
160
+ "id^20",
161
161
  "latestSnapshot.readme",
162
162
  "latestSnapshot.description.Name^6",
163
163
  "latestSnapshot.description.Authors^3",
@@ -0,0 +1,57 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Test <Markdown> component > allows a href with certain protocols 1`] = `
4
+ <DocumentFragment>
5
+ <a
6
+ href="https://example.com"
7
+ >
8
+ Example link that should work.
9
+ </a>
10
+ </DocumentFragment>
11
+ `;
12
+
13
+ exports[`Test <Markdown> component > does not allow href with unknown protocols 1`] = `
14
+ <DocumentFragment>
15
+ <a>
16
+ Example link that should not work.
17
+ </a>
18
+ </DocumentFragment>
19
+ `;
20
+
21
+ exports[`Test <Markdown> component > filters close-break tags 1`] = `
22
+ <DocumentFragment>
23
+ <span>
24
+ <br />
25
+ sample text
26
+ <br />
27
+ </span>
28
+ </DocumentFragment>
29
+ `;
30
+
31
+ exports[`Test <Markdown> component > filters out disallowed tags 1`] = `
32
+ <DocumentFragment>
33
+ <ul>
34
+ <li>
35
+ Markdown document
36
+ </li>
37
+ </ul>
38
+ </DocumentFragment>
39
+ `;
40
+
41
+ exports[`Test <Markdown> component > safely handles broken HTML tags 1`] = `
42
+ <DocumentFragment>
43
+ <ul>
44
+ <li>
45
+ Markdown document
46
+
47
+ <br />
48
+ <br />
49
+ <ul>
50
+ <li>
51
+ test content
52
+ </li>
53
+ </ul>
54
+ </li>
55
+ </ul>
56
+ </DocumentFragment>
57
+ `;
@@ -0,0 +1,35 @@
1
+ import React from "react"
2
+ import { render } from "@testing-library/react"
3
+ import { Markdown } from "../markdown"
4
+
5
+ describe("Test <Markdown> component", () => {
6
+ it("safely handles broken HTML tags", () => {
7
+ const brokenTagInput = "* Markdown document\n<br><br>\n * test content"
8
+ const { asFragment } = render(<Markdown>{brokenTagInput}</Markdown>)
9
+ expect(asFragment()).toMatchSnapshot()
10
+ })
11
+ it("filters out disallowed tags", () => {
12
+ const badTags =
13
+ '* Markdown document\n<script type="text/javascript">alert("this should not happen")</script>\n'
14
+ const { asFragment } = render(<Markdown>{badTags}</Markdown>)
15
+ expect(asFragment()).toMatchSnapshot()
16
+ })
17
+ it("allows a href with certain protocols", () => {
18
+ const hrefExample =
19
+ '<a href="https://example.com">Example link that should work.</a>'
20
+ const { asFragment } = render(<Markdown>{hrefExample}</Markdown>)
21
+ expect(asFragment()).toMatchSnapshot()
22
+ })
23
+ it("does not allow href with unknown protocols", () => {
24
+ const hrefExample =
25
+ '<a href="about:memory">Example link that should not work.</a>'
26
+ const { asFragment } = render(<Markdown>{hrefExample}</Markdown>)
27
+ expect(asFragment()).toMatchSnapshot()
28
+ })
29
+ it("filters close-break tags", () => {
30
+ const hrefExample =
31
+ '<br>sample text</br>'
32
+ const { asFragment } = render(<Markdown>{hrefExample}</Markdown>)
33
+ expect(asFragment()).toMatchSnapshot()
34
+ })
35
+ })
@@ -0,0 +1,167 @@
1
+ import React from "react"
2
+ import MarkdownToJsx from "markdown-to-jsx"
3
+ import DOMPurify from "dompurify"
4
+
5
+ interface MarkdownProps {
6
+ children: string
7
+ }
8
+
9
+ // Closely aligned with GitHub Markdown
10
+ // See https://github.com/gjtorikian/html-pipeline/blob/7e562219f9814777b73b48f32aece874452c0c5e/lib/html_pipeline/sanitization_filter.rb
11
+
12
+ const ALLOWED_TAGS = [
13
+ "h1",
14
+ "h2",
15
+ "h3",
16
+ "h4",
17
+ "h5",
18
+ "h6",
19
+ "br",
20
+ "b",
21
+ "i",
22
+ "strong",
23
+ "em",
24
+ "a",
25
+ "pre",
26
+ "code",
27
+ "img",
28
+ "tt",
29
+ "div",
30
+ "ins",
31
+ "del",
32
+ "sup",
33
+ "sub",
34
+ "p",
35
+ "picture",
36
+ "ol",
37
+ "ul",
38
+ "table",
39
+ "thead",
40
+ "tbody",
41
+ "tfoot",
42
+ "blockquote",
43
+ "dl",
44
+ "dt",
45
+ "dd",
46
+ "kbd",
47
+ "q",
48
+ "samp",
49
+ "var",
50
+ "hr",
51
+ "ruby",
52
+ "rt",
53
+ "rp",
54
+ "li",
55
+ "tr",
56
+ "td",
57
+ "th",
58
+ "s",
59
+ "strike",
60
+ "summary",
61
+ "details",
62
+ "caption",
63
+ "figure",
64
+ "figcaption",
65
+ "abbr",
66
+ "bdo",
67
+ "cite",
68
+ "dfn",
69
+ "mark",
70
+ "small",
71
+ "source",
72
+ "span",
73
+ "time",
74
+ "wbr",
75
+ ]
76
+
77
+ const ALLOWED_ATTR = [
78
+ "abbr",
79
+ "accept",
80
+ "accept-charset",
81
+ "accesskey",
82
+ "action",
83
+ "align",
84
+ "alt",
85
+ "aria-describedby",
86
+ "aria-hidden",
87
+ "aria-label",
88
+ "aria-labelledby",
89
+ "axis",
90
+ "border",
91
+ "char",
92
+ "charoff",
93
+ "charset",
94
+ "checked",
95
+ "clear",
96
+ "cols",
97
+ "colspan",
98
+ "compact",
99
+ "coords",
100
+ "datetime",
101
+ "dir",
102
+ "disabled",
103
+ "enctype",
104
+ "for",
105
+ "frame",
106
+ "headers",
107
+ "height",
108
+ "hreflang",
109
+ "hspace",
110
+ "id",
111
+ "ismap",
112
+ "label",
113
+ "lang",
114
+ "maxlength",
115
+ "media",
116
+ "method",
117
+ "multiple",
118
+ "name",
119
+ "nohref",
120
+ "noshade",
121
+ "nowrap",
122
+ "open",
123
+ "progress",
124
+ "prompt",
125
+ "readonly",
126
+ "rel",
127
+ "rev",
128
+ "role",
129
+ "rows",
130
+ "rowspan",
131
+ "rules",
132
+ "scope",
133
+ "selected",
134
+ "shape",
135
+ "size",
136
+ "span",
137
+ "start",
138
+ "summary",
139
+ "tabindex",
140
+ "title",
141
+ "type",
142
+ "usemap",
143
+ "valign",
144
+ "value",
145
+ "width",
146
+ "itemprop",
147
+ "href",
148
+ "cite",
149
+ "src",
150
+ "longdesc",
151
+ ]
152
+
153
+ /**
154
+ * Sanitize disallowed HTML tags and attributes and convert from Markdown to JSX
155
+ */
156
+ export function Markdown({ children }: MarkdownProps) {
157
+ const sanitizedMarkdown = DOMPurify.sanitize(children, {
158
+ ALLOWED_TAGS,
159
+ ALLOWED_ATTR,
160
+ ALLOW_ARIA_ATTR: false,
161
+ })
162
+ return (
163
+ <>
164
+ <MarkdownToJsx>{sanitizedMarkdown}</MarkdownToJsx>
165
+ </>
166
+ )
167
+ }