@openneuro/app 4.30.2 → 4.31.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 (58) hide show
  1. package/package.json +5 -5
  2. package/src/assets/activity-icon.png +0 -0
  3. package/src/assets/icon-archived.png +0 -0
  4. package/src/assets/icon-saved.png +0 -0
  5. package/src/assets/icon-unread.png +0 -0
  6. package/src/client.jsx +1 -1
  7. package/src/scripts/datalad/dataset/dataset-query-fragments.js +4 -14
  8. package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +4 -304
  9. package/src/scripts/dataset/components/ValidationBlock.tsx +13 -15
  10. package/src/scripts/dataset/components/__tests__/ValidationBlock.spec.tsx +2 -0
  11. package/src/scripts/dataset/draft-container.tsx +2 -1
  12. package/src/scripts/dataset/files/__tests__/__snapshots__/file-tree.spec.jsx.snap +1 -9
  13. package/src/scripts/dataset/fragments/__tests__/{dataset-alert-draft.spec.tsx → dataset-alert.spec.tsx} +33 -1
  14. package/src/scripts/dataset/fragments/{dataset-alert-draft.tsx → dataset-alert.tsx} +30 -18
  15. package/src/scripts/dataset/routes/delete-page.tsx +72 -39
  16. package/src/scripts/dataset/routes/snapshot.tsx +23 -17
  17. package/src/scripts/dataset/routes/tab-routes-draft.tsx +5 -2
  18. package/src/scripts/dataset/snapshot-container.tsx +11 -0
  19. package/src/scripts/search/__tests__/search-params-ctx.spec.tsx +3 -0
  20. package/src/scripts/search/initial-search-params.tsx +2 -0
  21. package/src/scripts/search/inputs/__tests__/nihselect.spec.tsx +36 -0
  22. package/src/scripts/search/inputs/index.ts +2 -0
  23. package/src/scripts/search/inputs/nih-select.tsx +63 -0
  24. package/src/scripts/search/search-container.tsx +20 -12
  25. package/src/scripts/search/search-params-ctx.tsx +2 -0
  26. package/src/scripts/search/search-routes.tsx +14 -6
  27. package/src/scripts/search/use-search-results.tsx +15 -0
  28. package/src/scripts/types/user-types.ts +72 -0
  29. package/src/scripts/uploader/upload-issues.tsx +2 -2
  30. package/src/scripts/users/__tests__/datasest-card.spec.tsx +201 -0
  31. package/src/scripts/users/__tests__/user-card.spec.tsx +30 -3
  32. package/src/scripts/users/__tests__/user-query.spec.tsx +6 -0
  33. package/src/scripts/users/__tests__/user-routes.spec.tsx +42 -18
  34. package/src/scripts/users/components/user-dataset-filters.tsx +157 -0
  35. package/src/scripts/users/dataset-card.tsx +121 -0
  36. package/src/scripts/users/fragments/query.js +42 -0
  37. package/src/scripts/users/scss/datasetcard.module.scss +153 -0
  38. package/src/scripts/users/scss/usernotifications.module.scss +159 -0
  39. package/src/scripts/users/user-account-view.tsx +1 -12
  40. package/src/scripts/users/user-card.tsx +1 -14
  41. package/src/scripts/users/user-container.tsx +1 -17
  42. package/src/scripts/users/user-datasets-view.tsx +58 -43
  43. package/src/scripts/users/user-notification-accordion.tsx +160 -0
  44. package/src/scripts/users/user-notification-list.tsx +27 -0
  45. package/src/scripts/users/user-notifications-tab-content.tsx +85 -0
  46. package/src/scripts/users/user-notifications-view.tsx +102 -4
  47. package/src/scripts/users/user-query.tsx +6 -14
  48. package/src/scripts/users/user-routes.tsx +18 -19
  49. package/src/scripts/utils/__tests__/user-datasets.spec.tsx +86 -0
  50. package/src/scripts/utils/gtag.js +3 -2
  51. package/src/scripts/utils/user-datasets.tsx +60 -0
  52. package/src/scripts/validation/__tests__/__snapshots__/validation-issues.spec.tsx.snap +1 -122
  53. package/src/scripts/validation/validation-results-query.ts +44 -0
  54. package/src/scripts/validation/validation-results.tsx +31 -7
  55. package/src/scripts/validation/validation.tsx +58 -49
  56. package/src/scripts/workers/schema.worker.ts +2 -7
  57. package/tsconfig.json +1 -2
  58. package/vite.config.js +1 -0
@@ -0,0 +1,27 @@
1
+ import React, { useState } from "react"
2
+ import styles from "./scss/usernotifications.module.scss"
3
+ import { NotificationAccordion } from "./user-notification-accordion"
4
+
5
+ // NotificationsList Component
6
+ export const NotificationsList = ({ notificationdata }) => {
7
+ const [notifications, setNotifications] = useState(notificationdata)
8
+
9
+ const handleUpdateNotification = (id, updates) => {
10
+ setNotifications((prevNotifications) =>
11
+ prevNotifications.map((notification) =>
12
+ notification.id === id ? { ...notification, ...updates } : notification
13
+ )
14
+ )
15
+ }
16
+ return (
17
+ <ul className={styles.notificationsList}>
18
+ {notifications.map((notification) => (
19
+ <NotificationAccordion
20
+ key={notification.id}
21
+ notification={notification}
22
+ onUpdate={handleUpdateNotification}
23
+ />
24
+ ))}
25
+ </ul>
26
+ )
27
+ }
@@ -0,0 +1,85 @@
1
+ import React from "react"
2
+ import { NotificationsList } from "./user-notification-list"
3
+
4
+ // Dummy notifications
5
+ const dummyNotifications = [
6
+ {
7
+ id: 1,
8
+ title: "New Comment on Your dataset",
9
+ content: "A user has commented on your dataset. View here",
10
+ status: "unread",
11
+ type: "general",
12
+ approval: "",
13
+ },
14
+ {
15
+ id: 2,
16
+ title: "Example No Approval State ",
17
+ content: "",
18
+ status: "unread",
19
+ type: "approval",
20
+ approval: "not provided",
21
+ },
22
+ {
23
+ id: 3,
24
+ title: "Example Denied State",
25
+ content: "",
26
+ status: "unread",
27
+ type: "approval",
28
+ approval: "denied",
29
+ },
30
+ {
31
+ id: 4,
32
+ title: "Example Approved State",
33
+ content: "",
34
+ status: "unread",
35
+ type: "approval",
36
+ approval: "approved",
37
+ },
38
+ {
39
+ id: 5,
40
+ title: "Saved Notification Example",
41
+ content: "This is an example of a saved notification.",
42
+ status: "saved",
43
+ type: "general",
44
+ approval: "",
45
+ },
46
+ {
47
+ id: 6,
48
+ title: "Archived Notification Example",
49
+ content: "This is an example of an archived notification.",
50
+ status: "archived",
51
+ type: "general",
52
+ approval: "",
53
+ },
54
+ ]
55
+
56
+ // Tab Components for Different Notifications
57
+ export const UnreadNotifications = () => (
58
+ <div className="tabContentUnread">
59
+ <NotificationsList
60
+ notificationdata={dummyNotifications.filter(
61
+ (notification) => notification.status === "unread",
62
+ )}
63
+ />
64
+ </div>
65
+ )
66
+
67
+ export const SavedNotifications = () => (
68
+ <div className="tabContentSaved">
69
+ <NotificationsList
70
+ notificationdata={dummyNotifications.filter(
71
+ (notification) => notification.status === "saved",
72
+ )}
73
+ />
74
+ </div>
75
+ )
76
+
77
+ export const ArchivedNotifications = () => (
78
+ <div className="tabContentArchived">
79
+ <NotificationsList
80
+ notificationdata={dummyNotifications.filter(
81
+ (notification) => notification.status === "archived",
82
+ )}
83
+ />
84
+ </div>
85
+ )
@@ -1,11 +1,109 @@
1
- import React from "react"
1
+ import React, { useEffect, useRef, useState } from "react"
2
+ import {
3
+ NavLink,
4
+ Outlet,
5
+ useLocation,
6
+ useNavigate,
7
+ useParams,
8
+ } from "react-router-dom"
9
+ import styles from "./scss/usernotifications.module.scss"
10
+ import iconUnread from "../../assets/icon-unread.png"
11
+ import iconSaved from "../../assets/icon-saved.png"
12
+ import iconArchived from "../../assets/icon-archived.png"
2
13
 
3
14
  export const UserNotificationsView = ({ user }) => {
4
- // this is a placeholder for the user notification feature
15
+ const tabsRef = useRef<HTMLUListElement | null>(null)
16
+ const { tab = "unread" } = useParams()
17
+ const navigate = useNavigate()
18
+ const location = useLocation()
19
+
20
+ // Explicitly define the type of indicatorStyle
21
+ const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({
22
+ width: "0px",
23
+ transform: "translateX(0px)",
24
+ position: "absolute",
25
+ bottom: "0px",
26
+ height: "2px",
27
+ backgroundColor: "#000",
28
+ transition: "transform 0.3s ease, width 0.3s ease",
29
+ })
30
+
31
+ // Update the indicator position based on active tab whenever location changes
32
+ useEffect(() => {
33
+ const activeLink = tabsRef.current?.querySelector(`.${styles.active}`)
34
+ if (activeLink) {
35
+ const li = activeLink.parentElement as HTMLElement
36
+ if (li) {
37
+ setIndicatorStyle({
38
+ width: `${li.offsetWidth}px`,
39
+ transform: `translateX(${li.offsetLeft}px)`,
40
+ position: "absolute",
41
+ bottom: "0px",
42
+ height: "2px",
43
+ backgroundColor: "#000",
44
+ transition: "transform 0.3s ease, width 0.3s ease",
45
+ })
46
+ }
47
+ }
48
+ }, [location])
49
+
50
+ // Redirect to default tab if no tab is specified
51
+ useEffect(() => {
52
+ if (!["unread", "saved", "archived"].includes(tab)) {
53
+ navigate(`/user/${user.orcid}/notifications/unread`, { replace: true })
54
+ }
55
+ }, [tab, user.orcid, navigate])
56
+
5
57
  return (
6
58
  <div data-testid="user-notifications-view">
7
- <h3>UserNotificationsPAge for {user.name}</h3>
8
- <p>This should show user info</p>
59
+ <h3>Notifications for {user.name}</h3>
60
+ <div className={styles.tabContainer}>
61
+ <ul className={styles.tabs} ref={tabsRef}>
62
+ <li>
63
+ <NavLink
64
+ to={`/user/${user.orcid}/notifications/unread`}
65
+ className={({ isActive }) =>
66
+ isActive
67
+ ? `${styles.active} ${styles.tabUnread}`
68
+ : styles.tabUnread}
69
+ >
70
+ <img className={styles.tabicon} src={iconUnread} alt="" /> Unread
71
+ <span className={styles.count}>121</span>
72
+ </NavLink>
73
+ </li>
74
+ <li>
75
+ <NavLink
76
+ to={`/user/${user.orcid}/notifications/saved`}
77
+ className={({ isActive }) =>
78
+ isActive
79
+ ? `${styles.active} ${styles.tabSaved}`
80
+ : styles.tabSaved}
81
+ >
82
+ <img className={styles.tabicon} src={iconSaved} alt="" /> Saved
83
+ <span className={styles.count}>121</span>
84
+ </NavLink>
85
+ </li>
86
+ <li>
87
+ <NavLink
88
+ to={`/user/${user.orcid}/notifications/archived`}
89
+ className={({ isActive }) =>
90
+ isActive
91
+ ? `${styles.active} ${styles.tabArchived}`
92
+ : styles.tabArchived}
93
+ >
94
+ <img className={styles.tabicon} src={iconArchived} alt="" />{" "}
95
+ Archived
96
+ <span className={styles.count}>121</span>
97
+ </NavLink>
98
+ </li>
99
+ </ul>
100
+
101
+ {/* This is the indicator that will follow the active tab */}
102
+ <span style={indicatorStyle}></span>
103
+ </div>
104
+ <div className={styles.tabContent}>
105
+ <Outlet />
106
+ </div>
9
107
  </div>
10
108
  )
11
109
  }
@@ -35,18 +35,6 @@ mutation updateUser($id: ID!, $location: String, $links: [String], $institution:
35
35
  }
36
36
  `
37
37
 
38
- export interface User {
39
- id: string
40
- name: string
41
- location: string
42
- github?: string
43
- institution: string
44
- email: string
45
- avatar: string
46
- orcid: string
47
- links: string[]
48
- }
49
-
50
38
  export const UserQuery: React.FC = () => {
51
39
  const { orcid } = useParams()
52
40
  const isOrcidValid = orcid && isValidOrcid(orcid)
@@ -57,6 +45,7 @@ export const UserQuery: React.FC = () => {
57
45
 
58
46
  const [cookies] = useCookies()
59
47
  const profile = getProfile(cookies)
48
+ const isAdminUser = isAdmin()
60
49
 
61
50
  if (!isOrcidValid) {
62
51
  return <FourOFourPage />
@@ -68,9 +57,12 @@ export const UserQuery: React.FC = () => {
68
57
  return <FourOFourPage />
69
58
  }
70
59
 
71
- // is admin or profile matches id from the user data being returned
72
- const hasEdit = isAdmin || data.user.id !== profile.sub ? true : false
60
+ if (!profile || !profile.sub) {
61
+ return <FourOFourPage />
62
+ }
73
63
 
64
+ // is admin or profile matches id from the user data being returned
65
+ const hasEdit = isAdminUser || (data.user.id === profile?.sub) ? true : false
74
66
  // Render user data with UserRoutes
75
67
  return <UserRoutes user={data.user} hasEdit={hasEdit} />
76
68
  }
@@ -6,23 +6,13 @@ import { UserNotificationsView } from "./user-notifications-view"
6
6
  import { UserDatasetsView } from "./user-datasets-view"
7
7
  import FourOFourPage from "../errors/404page"
8
8
  import FourOThreePage from "../errors/403page"
9
+ import {
10
+ ArchivedNotifications,
11
+ SavedNotifications,
12
+ UnreadNotifications,
13
+ } from "./user-notifications-tab-content"
9
14
 
10
- export interface User {
11
- id: string
12
- name: string
13
- location: string
14
- github?: string
15
- institution: string
16
- email: string
17
- avatar: string
18
- orcid: string
19
- links: string[]
20
- }
21
-
22
- interface UserRoutesProps {
23
- user: User
24
- hasEdit: boolean
25
- }
15
+ import type { UserRoutesProps } from "../types/user-types"
26
16
 
27
17
  export const UserRoutes: React.FC<UserRoutesProps> = ({ user, hasEdit }) => {
28
18
  return (
@@ -32,7 +22,10 @@ export const UserRoutes: React.FC<UserRoutesProps> = ({ user, hasEdit }) => {
32
22
  path="*"
33
23
  element={<UserAccountContainer user={user} hasEdit={hasEdit} />}
34
24
  >
35
- <Route path="" element={<UserDatasetsView user={user} />} />
25
+ <Route
26
+ path=""
27
+ element={<UserDatasetsView user={user} hasEdit={hasEdit} />}
28
+ />
36
29
  <Route
37
30
  path="account"
38
31
  element={hasEdit
@@ -40,11 +33,17 @@ export const UserRoutes: React.FC<UserRoutesProps> = ({ user, hasEdit }) => {
40
33
  : <FourOThreePage />}
41
34
  />
42
35
  <Route
43
- path="notifications"
36
+ path="notifications/*"
44
37
  element={hasEdit
45
38
  ? <UserNotificationsView user={user} />
46
39
  : <FourOThreePage />}
47
- />
40
+ >
41
+ <Route index element={<UnreadNotifications />} />
42
+ <Route path="unread" element={<UnreadNotifications />} />
43
+ <Route path="saved" element={<SavedNotifications />} />
44
+ <Route path="archived" element={<ArchivedNotifications />} />
45
+ <Route path="*" element={<FourOFourPage />} />
46
+ </Route>
48
47
  <Route path="*" element={<FourOFourPage />} />
49
48
  </Route>
50
49
  </Routes>
@@ -0,0 +1,86 @@
1
+ import { filterAndSortDatasets } from "../user-datasets"
2
+ import type { Dataset } from "../../types/user-types"
3
+
4
+ describe("filterAndSortDatasets", () => {
5
+ const datasets: Dataset[] = [
6
+ {
7
+ id: "1",
8
+ name: "Dataset Bel",
9
+ created: "2025-01-21T12:00:00Z",
10
+ latestSnapshot: {
11
+ id: "1.0.0",
12
+ size: 1024,
13
+ issues: [{ severity: "low" }],
14
+ created: "2025-01-22T12:00:00Z",
15
+ },
16
+ public: true,
17
+ analytics: { downloads: 0, views: 10 },
18
+ },
19
+ {
20
+ id: "2",
21
+ name: "Dataset Ael",
22
+ created: "2025-01-22T12:00:00Z",
23
+ latestSnapshot: {
24
+ id: "2.0.0",
25
+ size: 2048,
26
+ issues: [{ severity: "high" }],
27
+ created: "2025-01-23T12:00:00Z",
28
+ },
29
+ public: false,
30
+ analytics: { downloads: 5, views: 20 },
31
+ },
32
+ {
33
+ id: "3",
34
+ name: "Dataset Cel",
35
+ created: "2025-01-20T12:00:00Z",
36
+ latestSnapshot: {
37
+ id: "3.0.0",
38
+ size: 4096,
39
+ issues: [{ severity: "medium" }],
40
+ created: "2025-01-20T12:00:00Z",
41
+ },
42
+ public: true,
43
+ analytics: { downloads: 15, views: 30 },
44
+ },
45
+ ]
46
+
47
+ it("filters and sorts datasets by name ascending", () => {
48
+ const result = filterAndSortDatasets(datasets, {
49
+ searchQuery: "",
50
+ publicFilter: "all",
51
+ sortOrder: "name-asc",
52
+ })
53
+ expect(result.map((d) => d.name)).toEqual([
54
+ "Dataset Ael",
55
+ "Dataset Bel",
56
+ "Dataset Cel",
57
+ ])
58
+ })
59
+
60
+ it("filters datasets by public visibility and sorts by name descending", () => {
61
+ const result = filterAndSortDatasets(datasets, {
62
+ searchQuery: "",
63
+ publicFilter: "public",
64
+ sortOrder: "name-desc",
65
+ })
66
+ expect(result.map((d) => d.name)).toEqual(["Dataset Cel", "Dataset Bel"])
67
+ })
68
+
69
+ it("filters datasets by search query and sorts by newest date", () => {
70
+ const result = filterAndSortDatasets(datasets, {
71
+ searchQuery: "Ael",
72
+ publicFilter: "all",
73
+ sortOrder: "date-newest",
74
+ })
75
+ expect(result.map((d) => d.name)).toEqual(["Dataset Ael"])
76
+ })
77
+
78
+ it("returns datasets unchanged for unknown sort order", () => {
79
+ const result = filterAndSortDatasets(datasets, {
80
+ searchQuery: "",
81
+ publicFilter: "all",
82
+ sortOrder: "unknown-order",
83
+ })
84
+ expect(result).toEqual(datasets)
85
+ })
86
+ })
@@ -1,8 +1,9 @@
1
1
  /*global globalThis*/
2
2
  globalThis.dataLayer = globalThis.dataLayer || []
3
3
 
4
- function gtag(...args) {
5
- globalThis.dataLayer.push(args)
4
+ function gtag() {
5
+ // eslint-disable-next-line prefer-rest-params
6
+ globalThis.dataLayer.push(arguments)
6
7
  }
7
8
 
8
9
  gtag("js", new Date())
@@ -0,0 +1,60 @@
1
+ import type { Dataset } from "../types/user-types"
2
+ export interface FilterOptions {
3
+ searchQuery: string
4
+ publicFilter: string
5
+ sortOrder: string
6
+ }
7
+
8
+ export function filterAndSortDatasets(
9
+ datasets: Dataset[],
10
+ { searchQuery, publicFilter, sortOrder }: FilterOptions,
11
+ ): Dataset[] {
12
+ const filteredDatasets = datasets.filter((dataset) => {
13
+ const matchesSearch = searchQuery === "" ||
14
+ (dataset.name &&
15
+ dataset.name.toLowerCase().includes(searchQuery.toLowerCase())) ||
16
+ (dataset.id &&
17
+ dataset.id.toLowerCase().includes(searchQuery.toLowerCase()))
18
+
19
+ const matchesPublicFilter = publicFilter === "all" ||
20
+ (publicFilter === "public" && dataset.public) ||
21
+ (publicFilter === "private" && !dataset.public)
22
+
23
+ return matchesSearch && matchesPublicFilter
24
+ })
25
+
26
+ const sortedDatasets = filteredDatasets.sort((a, b) => {
27
+ let comparisonResult = 0
28
+
29
+ // Move declarations outside the switch block
30
+ let aUpdated: Date | string
31
+ let bUpdated: Date | string
32
+
33
+ switch (sortOrder) {
34
+ case "name-asc":
35
+ comparisonResult = (a.name || "").localeCompare(b.name || "")
36
+ break
37
+ case "name-desc":
38
+ comparisonResult = (b.name || "").localeCompare(a.name || "")
39
+ break
40
+ case "date-newest":
41
+ comparisonResult =
42
+ new Date(b.latestSnapshot?.created || b.created).getTime() -
43
+ new Date(a.latestSnapshot?.created || a.created).getTime()
44
+ break
45
+ case "date-updated":
46
+ aUpdated = a.latestSnapshot?.created || a.created
47
+ bUpdated = b.latestSnapshot?.created || b.created
48
+ comparisonResult = new Date(bUpdated).getTime() -
49
+ new Date(aUpdated).getTime()
50
+ break
51
+ default:
52
+ comparisonResult = 0
53
+ break
54
+ }
55
+
56
+ return comparisonResult
57
+ })
58
+
59
+ return sortedDatasets
60
+ }
@@ -67,128 +67,7 @@ exports[`Issues component > renders multiple issues 1`] = `
67
67
  </div>
68
68
  <div
69
69
  class="accordion-item collapsed "
70
- >
71
- <div
72
- class="accordion-content"
73
- >
74
- <div
75
- class="em-body"
76
- >
77
- <div
78
- class="e-meta"
79
- >
80
- <label>
81
- /dataset_description.json
82
- </label>
83
- </div>
84
- <div
85
- class="e-meta"
86
- >
87
- <label>
88
- Rule:
89
- </label>
90
- rules.dataset_metadata.dataset_description
91
- </div>
92
- <div
93
- class="e-meta"
94
- >
95
- <label>
96
- Messages:
97
- </label>
98
- <p>
99
- A JSON file is missing a key listed as recommended.
100
- </p>
101
- </div>
102
- </div>
103
- <div
104
- class="em-body"
105
- >
106
- <div
107
- class="e-meta"
108
- >
109
- <label>
110
- /dataset_description.json
111
- </label>
112
- </div>
113
- <div
114
- class="e-meta"
115
- >
116
- <label>
117
- Rule:
118
- </label>
119
- rules.dataset_metadata.dataset_description
120
- </div>
121
- <div
122
- class="e-meta"
123
- >
124
- <label>
125
- Messages:
126
- </label>
127
- <p>
128
- A JSON file is missing a key listed as recommended.
129
- </p>
130
- </div>
131
- </div>
132
- <div
133
- class="em-body"
134
- >
135
- <div
136
- class="e-meta"
137
- >
138
- <label>
139
- /dataset_description.json
140
- </label>
141
- </div>
142
- <div
143
- class="e-meta"
144
- >
145
- <label>
146
- Rule:
147
- </label>
148
- rules.dataset_metadata.dataset_description
149
- </div>
150
- <div
151
- class="e-meta"
152
- >
153
- <label>
154
- Messages:
155
- </label>
156
- <p>
157
- A JSON file is missing a key listed as recommended.
158
- </p>
159
- </div>
160
- </div>
161
- <div
162
- class="em-body"
163
- >
164
- <div
165
- class="e-meta"
166
- >
167
- <label>
168
- /dataset_description.json
169
- </label>
170
- </div>
171
- <div
172
- class="e-meta"
173
- >
174
- <label>
175
- Rule:
176
- </label>
177
- rules.dataset_metadata.dataset_description
178
- </div>
179
- <div
180
- class="e-meta"
181
- >
182
- <label>
183
- Messages:
184
- </label>
185
- <p>
186
- A JSON file is missing a key listed as recommended.
187
- </p>
188
- </div>
189
- </div>
190
- </div>
191
- </div>
70
+ />
192
71
  </article>
193
72
  </div>
194
73
  </DocumentFragment>
@@ -0,0 +1,44 @@
1
+ import { DatasetIssues } from "@bids/validator/issues"
2
+ import { gql, useQuery } from "@apollo/client"
3
+ import { VALIDATION_FIELDS } from "../datalad/dataset/dataset-query-fragments"
4
+
5
+ export const VALIDATION_DATA_QUERY = gql`
6
+ query ($datasetId: ID!, $version: String!) {
7
+ snapshot(datasetId: $datasetId, tag: $version) {
8
+ validation {
9
+ id
10
+ datasetId
11
+ warnings
12
+ errors
13
+ issues {
14
+ ${VALIDATION_FIELDS}
15
+ }
16
+ codeMessages {
17
+ code
18
+ message
19
+ }
20
+ }
21
+ }
22
+ }
23
+ `
24
+
25
+ export const useValidationResults = (datasetId: string, version: string) => {
26
+ const { loading, error, data } = useQuery(VALIDATION_DATA_QUERY, {
27
+ variables: { datasetId, version },
28
+ })
29
+ const issues = new DatasetIssues()
30
+ // After loading, construct the dataset issues object for our hook
31
+ if (!loading && !error) {
32
+ const validation = data.snapshot.validation
33
+ // Reconstruct DatasetIssues from JSON
34
+ issues.issues = validation.issues
35
+ issues.codeMessages = validation.codeMessages.reduce(
36
+ (acc, curr) => {
37
+ acc.set(curr.code, curr.message)
38
+ return acc
39
+ },
40
+ new Map<string, string>(),
41
+ )
42
+ }
43
+ return { loading, error, issues }
44
+ }