@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.
- package/package.json +5 -5
- package/src/assets/activity-icon.png +0 -0
- package/src/assets/icon-archived.png +0 -0
- package/src/assets/icon-saved.png +0 -0
- package/src/assets/icon-unread.png +0 -0
- package/src/client.jsx +1 -1
- package/src/scripts/datalad/dataset/dataset-query-fragments.js +4 -14
- package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +4 -304
- package/src/scripts/dataset/components/ValidationBlock.tsx +13 -15
- package/src/scripts/dataset/components/__tests__/ValidationBlock.spec.tsx +2 -0
- package/src/scripts/dataset/draft-container.tsx +2 -1
- package/src/scripts/dataset/files/__tests__/__snapshots__/file-tree.spec.jsx.snap +1 -9
- package/src/scripts/dataset/fragments/__tests__/{dataset-alert-draft.spec.tsx → dataset-alert.spec.tsx} +33 -1
- package/src/scripts/dataset/fragments/{dataset-alert-draft.tsx → dataset-alert.tsx} +30 -18
- package/src/scripts/dataset/routes/delete-page.tsx +72 -39
- package/src/scripts/dataset/routes/snapshot.tsx +23 -17
- package/src/scripts/dataset/routes/tab-routes-draft.tsx +5 -2
- package/src/scripts/dataset/snapshot-container.tsx +11 -0
- package/src/scripts/search/__tests__/search-params-ctx.spec.tsx +3 -0
- package/src/scripts/search/initial-search-params.tsx +2 -0
- package/src/scripts/search/inputs/__tests__/nihselect.spec.tsx +36 -0
- package/src/scripts/search/inputs/index.ts +2 -0
- package/src/scripts/search/inputs/nih-select.tsx +63 -0
- package/src/scripts/search/search-container.tsx +20 -12
- package/src/scripts/search/search-params-ctx.tsx +2 -0
- package/src/scripts/search/search-routes.tsx +14 -6
- package/src/scripts/search/use-search-results.tsx +15 -0
- package/src/scripts/types/user-types.ts +72 -0
- package/src/scripts/uploader/upload-issues.tsx +2 -2
- package/src/scripts/users/__tests__/datasest-card.spec.tsx +201 -0
- package/src/scripts/users/__tests__/user-card.spec.tsx +30 -3
- package/src/scripts/users/__tests__/user-query.spec.tsx +6 -0
- package/src/scripts/users/__tests__/user-routes.spec.tsx +42 -18
- package/src/scripts/users/components/user-dataset-filters.tsx +157 -0
- package/src/scripts/users/dataset-card.tsx +121 -0
- package/src/scripts/users/fragments/query.js +42 -0
- package/src/scripts/users/scss/datasetcard.module.scss +153 -0
- package/src/scripts/users/scss/usernotifications.module.scss +159 -0
- package/src/scripts/users/user-account-view.tsx +1 -12
- package/src/scripts/users/user-card.tsx +1 -14
- package/src/scripts/users/user-container.tsx +1 -17
- package/src/scripts/users/user-datasets-view.tsx +58 -43
- package/src/scripts/users/user-notification-accordion.tsx +160 -0
- package/src/scripts/users/user-notification-list.tsx +27 -0
- package/src/scripts/users/user-notifications-tab-content.tsx +85 -0
- package/src/scripts/users/user-notifications-view.tsx +102 -4
- package/src/scripts/users/user-query.tsx +6 -14
- package/src/scripts/users/user-routes.tsx +18 -19
- package/src/scripts/utils/__tests__/user-datasets.spec.tsx +86 -0
- package/src/scripts/utils/gtag.js +3 -2
- package/src/scripts/utils/user-datasets.tsx +60 -0
- package/src/scripts/validation/__tests__/__snapshots__/validation-issues.spec.tsx.snap +1 -122
- package/src/scripts/validation/validation-results-query.ts +44 -0
- package/src/scripts/validation/validation-results.tsx +31 -7
- package/src/scripts/validation/validation.tsx +58 -49
- package/src/scripts/workers/schema.worker.ts +2 -7
- package/tsconfig.json +1 -2
- 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
|
-
|
|
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>
|
|
8
|
-
<
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
5
|
-
|
|
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
|
+
}
|