@openneuro/app 4.45.4 → 4.47.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 +3 -2
- package/src/assets/external/neurodesk.png +0 -0
- package/src/client.jsx +8 -0
- package/src/scripts/authentication/google-redirect.tsx +26 -0
- package/src/scripts/authentication/profile.ts +5 -1
- package/src/scripts/common/partials/__tests__/freshdesk-widget.spec.tsx +82 -0
- package/src/scripts/common/partials/freshdesk-widget.jsx +11 -1
- package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +16 -0
- package/src/scripts/dataset/components/AnalyzeDropdown.tsx +16 -0
- package/src/scripts/dataset/components/__tests__/AnalyzeDropdown.spec.tsx +6 -0
- package/src/scripts/dataset/mutations/delete-dataset-form.jsx +1 -0
- package/src/scripts/errors/404page.tsx +8 -0
- package/src/scripts/errors/freshdesk-widget.jsx +11 -1
- package/src/scripts/index.tsx +4 -29
- package/src/scripts/queries/user.ts +9 -1
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +11 -11
- package/src/scripts/users/notifications/user-notifications-context.tsx +22 -7
- package/src/scripts/users/notifications/user-notifications-view.tsx +1 -18
- package/src/scripts/users/user-account-view.tsx +11 -11
- package/src/scripts/users/user-menu.tsx +12 -10
- package/vite.config.js +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/app",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.47.0-alpha.0",
|
|
4
4
|
"description": "React JS web frontend for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "public/client.js",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"universal-cookie": "^4.0.4"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
+
"@codecov/vite-plugin": "^1.9.1",
|
|
55
56
|
"@testing-library/dom": "^10.4.0",
|
|
56
57
|
"@testing-library/jest-dom": "6.4.2",
|
|
57
58
|
"@testing-library/react": "^14.2.1",
|
|
@@ -78,5 +79,5 @@
|
|
|
78
79
|
"publishConfig": {
|
|
79
80
|
"access": "public"
|
|
80
81
|
},
|
|
81
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "0cce162b7c6dd6f62e16b2c50e68c77fb1048a7e"
|
|
82
83
|
}
|
|
Binary file
|
package/src/client.jsx
CHANGED
|
@@ -14,6 +14,14 @@ import * as gtag from "./scripts/utils/gtag"
|
|
|
14
14
|
import { relayStylePagination } from "@apollo/client/utilities"
|
|
15
15
|
// TODO - This should be a global SCSS?
|
|
16
16
|
import "./scripts/components/page/page.scss"
|
|
17
|
+
import cookies from "./scripts/utils/cookies.js"
|
|
18
|
+
import { getProfile, guardExpired } from "./scripts/authentication/profile"
|
|
19
|
+
|
|
20
|
+
// Clear expired JWT before the app mounts to avoid stale auth state
|
|
21
|
+
const profile = getProfile(cookies.getAll())
|
|
22
|
+
if (profile && !guardExpired(profile)) {
|
|
23
|
+
cookies.remove("accessToken", { path: "/" })
|
|
24
|
+
}
|
|
17
25
|
|
|
18
26
|
gtag.initialize(config.analytics.trackingIds)
|
|
19
27
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { useLocation, useNavigate } from "react-router-dom"
|
|
3
|
+
import { useUser } from "../queries/user"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redirects Google-authenticated users to the ORCID linking page.
|
|
7
|
+
* Renders nothing — just runs the redirect effect when the user query resolves.
|
|
8
|
+
*/
|
|
9
|
+
export function GoogleOrcidRedirect() {
|
|
10
|
+
const navigate = useNavigate()
|
|
11
|
+
const location = useLocation()
|
|
12
|
+
const { user, loading, error } = useUser()
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (
|
|
16
|
+
!loading &&
|
|
17
|
+
!error &&
|
|
18
|
+
location.pathname !== "/orcid-link" &&
|
|
19
|
+
user?.provider === "google"
|
|
20
|
+
) {
|
|
21
|
+
navigate("/orcid-link")
|
|
22
|
+
}
|
|
23
|
+
}, [location.pathname, user, loading, error, navigate])
|
|
24
|
+
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
@@ -21,7 +21,11 @@ export const parseJwt = jwtDecode
|
|
|
21
21
|
export function getProfile(cookies): OpenNeuroTokenProfile {
|
|
22
22
|
const accessToken = cookies["accessToken"]
|
|
23
23
|
if (!accessToken) return null
|
|
24
|
-
|
|
24
|
+
try {
|
|
25
|
+
return parseJwt(accessToken) as OpenNeuroTokenProfile
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Return profile if token is not expired.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { vi, describe, it, expect } from "vitest"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import { render, screen } from "@testing-library/react"
|
|
4
|
+
import { MemoryRouter } from "react-router-dom"
|
|
5
|
+
import { MockedProvider } from "@apollo/client/testing"
|
|
6
|
+
|
|
7
|
+
// Mock the dependencies
|
|
8
|
+
vi.mock("react-cookie", () => ({
|
|
9
|
+
useCookies: () => [{}],
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock("../../authentication/profile", () => ({
|
|
13
|
+
getProfile: () => ({ email: "test@example.com" }),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
vi.mock("../../queries/user", () => ({
|
|
17
|
+
useUser: () => ({
|
|
18
|
+
user: { email: "test@example.com" },
|
|
19
|
+
}),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
import FreshdeskWidget from "../freshdesk-widget"
|
|
23
|
+
|
|
24
|
+
describe("FreshdeskWidget component", () => {
|
|
25
|
+
it("includes meta[referrer] with current page URL in iframe src", () => {
|
|
26
|
+
const testRoute = "/datasets/ds000001"
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
<MemoryRouter initialEntries={[testRoute]}>
|
|
30
|
+
<MockedProvider>
|
|
31
|
+
<FreshdeskWidget subject="Test" description="Test description" />
|
|
32
|
+
</MockedProvider>
|
|
33
|
+
</MemoryRouter>,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const iframe = screen.getByTitle("Feedback Form") as HTMLIFrameElement
|
|
37
|
+
|
|
38
|
+
// The iframe src should contain the current page URL as meta[referrer]
|
|
39
|
+
expect(iframe.src).toContain("meta[referrer]=")
|
|
40
|
+
// Verify it includes the test route pathname
|
|
41
|
+
expect(iframe.src).toContain("/datasets/ds000001")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("includes prepopulated subject and description fields", () => {
|
|
45
|
+
const testRoute = "/datasets/ds000001"
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<MemoryRouter initialEntries={[testRoute]}>
|
|
49
|
+
<MockedProvider>
|
|
50
|
+
<FreshdeskWidget
|
|
51
|
+
subject="Test Issue"
|
|
52
|
+
description="This is a test"
|
|
53
|
+
/>
|
|
54
|
+
</MockedProvider>
|
|
55
|
+
</MemoryRouter>,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const iframe = screen.getByTitle("Feedback Form") as HTMLIFrameElement
|
|
59
|
+
|
|
60
|
+
// Verify subject and description are in the iframe src
|
|
61
|
+
expect(iframe.src).toContain("helpdesk_ticket[subject]=Test%20Issue")
|
|
62
|
+
expect(iframe.src).toContain("helpdesk_ticket[description]=This%20is%20a%20test")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("encodes special characters in the referrer URL correctly", () => {
|
|
66
|
+
const testRoute = "/search?query=test"
|
|
67
|
+
|
|
68
|
+
render(
|
|
69
|
+
<MemoryRouter initialEntries={[testRoute]}>
|
|
70
|
+
<MockedProvider>
|
|
71
|
+
<FreshdeskWidget subject="Test" />
|
|
72
|
+
</MockedProvider>
|
|
73
|
+
</MemoryRouter>,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const iframe = screen.getByTitle("Feedback Form") as HTMLIFrameElement
|
|
77
|
+
|
|
78
|
+
// Should properly encode the URL with query parameters
|
|
79
|
+
expect(iframe.src).toContain("meta[referrer]=")
|
|
80
|
+
expect(iframe.src).toContain("/search")
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import PropTypes from "prop-types"
|
|
3
3
|
import { useCookies } from "react-cookie"
|
|
4
|
+
import { useLocation } from "react-router-dom"
|
|
4
5
|
import { getProfile } from "../../authentication/profile"
|
|
5
6
|
import { config } from "../../config"
|
|
6
7
|
import { useUser } from "../../queries/user"
|
|
@@ -10,13 +11,21 @@ const buildCustomQuery = (customText, prepopulatedFields) => {
|
|
|
10
11
|
...Object.entries(customText).map(([key, value]) => `${key}=${value}`),
|
|
11
12
|
...Object.entries(prepopulatedFields)
|
|
12
13
|
.filter(([, value]) => value)
|
|
13
|
-
.map(([key, value]) =>
|
|
14
|
+
.map(([key, value]) => {
|
|
15
|
+
// Handle meta_ prefixed fields separately (e.g., meta_referrer -> meta[referrer])
|
|
16
|
+
if (key.startsWith("meta_")) {
|
|
17
|
+
const fieldName = key.replace(/^meta_/, "")
|
|
18
|
+
return `meta[${fieldName}]=${value}`
|
|
19
|
+
}
|
|
20
|
+
return `helpdesk_ticket[${key}]=${value}`
|
|
21
|
+
}),
|
|
14
22
|
]
|
|
15
23
|
return customizerQueries.length ? `&${customizerQueries.join(";")}` : ""
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
function FreshdeskWidget({ subject, error, sentryId, description }) {
|
|
19
27
|
const [cookies] = useCookies()
|
|
28
|
+
const { pathname } = useLocation()
|
|
20
29
|
const profile = getProfile(cookies)
|
|
21
30
|
const sentry = sentryId && `Sentry ID: ${sentryId}`
|
|
22
31
|
const joinedDescription = [sentry, description, error]
|
|
@@ -38,6 +47,7 @@ function FreshdeskWidget({ subject, error, sentryId, description }) {
|
|
|
38
47
|
requester: profile && user?.email,
|
|
39
48
|
subject,
|
|
40
49
|
description: joinedDescription,
|
|
50
|
+
meta_referrer: pathname,
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
return (
|
|
@@ -348,6 +348,22 @@ exports[`SnapshotContainer component > renders successfully 1`] = `
|
|
|
348
348
|
<p>
|
|
349
349
|
CBRAIN is a web-based distributed computing platform for collaborative neuroimaging research.
|
|
350
350
|
</p>
|
|
351
|
+
<hr />
|
|
352
|
+
<img
|
|
353
|
+
height="16"
|
|
354
|
+
src="/packages/openneuro-app/src/assets/external/neurodesk.png"
|
|
355
|
+
width="16"
|
|
356
|
+
/>
|
|
357
|
+
|
|
358
|
+
<a
|
|
359
|
+
href="https://play-america.neurodesk.org/hub/user-redirect/lab/tree/data/openneuro/ds001032"
|
|
360
|
+
target="_blank"
|
|
361
|
+
>
|
|
362
|
+
View on Neurodesk
|
|
363
|
+
</a>
|
|
364
|
+
<p>
|
|
365
|
+
Neurodesk provides browser-based access to a neuroimaging analysis environment with this dataset mounted.
|
|
366
|
+
</p>
|
|
351
367
|
</div>
|
|
352
368
|
</div>
|
|
353
369
|
</div>
|
|
@@ -5,6 +5,7 @@ import styled from "@emotion/styled"
|
|
|
5
5
|
import BrainlifeIcon from "../../../assets/external/brainlife.png"
|
|
6
6
|
import NemarIcon from "../../../assets/external/nemar.png"
|
|
7
7
|
import CbrainIcon from "../../../assets/external/cbrain.png"
|
|
8
|
+
import NeurodeskIcon from "../../../assets/external/neurodesk.png"
|
|
8
9
|
|
|
9
10
|
export interface CloneDropdownProps {
|
|
10
11
|
datasetId: string
|
|
@@ -24,6 +25,8 @@ export const AnalyzeDropdown: React.FC<CloneDropdownProps> = (
|
|
|
24
25
|
`https://nemar.org/dataexplorer/detail?dataset_id=${datasetId}`
|
|
25
26
|
const cbrainUrl =
|
|
26
27
|
`https://portal.cbrain.mcgill.ca/openneuro/${datasetId}/versions/${snapshotVersion}`
|
|
28
|
+
const neurodeskUrl =
|
|
29
|
+
`https://play-america.neurodesk.org/hub/user-redirect/lab/tree/data/openneuro/${datasetId}`
|
|
27
30
|
return (
|
|
28
31
|
<AnalyzeDiv className="clone-dropdown">
|
|
29
32
|
<Dropdown
|
|
@@ -91,6 +94,19 @@ export const AnalyzeDropdown: React.FC<CloneDropdownProps> = (
|
|
|
91
94
|
CBRAIN is a web-based distributed computing platform for
|
|
92
95
|
collaborative neuroimaging research.
|
|
93
96
|
</p>
|
|
97
|
+
<hr />
|
|
98
|
+
<img
|
|
99
|
+
src={NeurodeskIcon}
|
|
100
|
+
height="16"
|
|
101
|
+
width="16"
|
|
102
|
+
/>{" "}
|
|
103
|
+
<a href={neurodeskUrl} target="_blank">
|
|
104
|
+
View on Neurodesk
|
|
105
|
+
</a>
|
|
106
|
+
<p>
|
|
107
|
+
Neurodesk provides browser-based access to a neuroimaging analysis
|
|
108
|
+
environment with this dataset mounted.
|
|
109
|
+
</p>
|
|
94
110
|
</div>
|
|
95
111
|
</Dropdown>
|
|
96
112
|
</AnalyzeDiv>
|
|
@@ -15,5 +15,11 @@ describe("AnalyzeDropdown component", () => {
|
|
|
15
15
|
expect(menu).toHaveClass("collapsed")
|
|
16
16
|
fireEvent.click(button)
|
|
17
17
|
expect(menu).toHaveClass("expanded")
|
|
18
|
+
expect(
|
|
19
|
+
screen.getByRole("link", { name: "View on Neurodesk" }),
|
|
20
|
+
).toHaveAttribute(
|
|
21
|
+
"href",
|
|
22
|
+
"https://play-america.neurodesk.org/hub/user-redirect/lab/tree/data/openneuro/ds000031",
|
|
23
|
+
)
|
|
18
24
|
})
|
|
19
25
|
})
|
|
@@ -36,6 +36,14 @@ const FourOFourPage: FC<FourOFourPageProps> = ({
|
|
|
36
36
|
<Container styleContext={theme} data-testid="404-page">
|
|
37
37
|
<h3>404: The page you are looking for does not exist.</h3>
|
|
38
38
|
{message && <p>{message}</p>}
|
|
39
|
+
{message.includes("data retention policy") && (
|
|
40
|
+
<p>
|
|
41
|
+
See our documentation for the{" "}
|
|
42
|
+
<a href="https://docs.openneuro.org/policy/data_retention.html">
|
|
43
|
+
full data retention policy
|
|
44
|
+
</a>.
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
39
47
|
<p>
|
|
40
48
|
Click <Link to={redirectRoute}>here</Link> to go to
|
|
41
49
|
{" " + redirectRouteName}.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import PropTypes from "prop-types"
|
|
3
3
|
import { useCookies } from "react-cookie"
|
|
4
|
+
import { useLocation } from "react-router-dom"
|
|
4
5
|
import { getProfile } from "../authentication/profile"
|
|
5
6
|
import { config } from "../config"
|
|
6
7
|
import { useUser } from "../queries/user"
|
|
@@ -10,13 +11,21 @@ const buildCustomQuery = (customText, prepopulatedFields) => {
|
|
|
10
11
|
...Object.entries(customText).map(([key, value]) => `${key}=${value}`),
|
|
11
12
|
...Object.entries(prepopulatedFields)
|
|
12
13
|
.filter(([, value]) => value)
|
|
13
|
-
.map(([key, value]) =>
|
|
14
|
+
.map(([key, value]) => {
|
|
15
|
+
// Handle meta_ prefixed fields separately (e.g., meta_referrer -> meta[referrer])
|
|
16
|
+
if (key.startsWith("meta_")) {
|
|
17
|
+
const fieldName = key.replace(/^meta_/, "")
|
|
18
|
+
return `meta[${fieldName}]=${value}`
|
|
19
|
+
}
|
|
20
|
+
return `helpdesk_ticket[${key}]=${value}`
|
|
21
|
+
}),
|
|
14
22
|
]
|
|
15
23
|
return customizerQueries.length ? `&${customizerQueries.join(";")}` : ""
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
function FreshdeskWidget({ subject, error, sentryId, description }) {
|
|
19
27
|
const [cookies] = useCookies()
|
|
28
|
+
const { pathname } = useLocation()
|
|
20
29
|
const profile = getProfile(cookies)
|
|
21
30
|
const sentry = sentryId && `Sentry ID: ${sentryId}`
|
|
22
31
|
const joinedDescription = [sentry, description, error]
|
|
@@ -39,6 +48,7 @@ function FreshdeskWidget({ subject, error, sentryId, description }) {
|
|
|
39
48
|
requester: profile && user?.email,
|
|
40
49
|
subject,
|
|
41
50
|
description: joinedDescription,
|
|
51
|
+
meta_referrer: pathname,
|
|
42
52
|
}
|
|
43
53
|
return (
|
|
44
54
|
<>
|
package/src/scripts/index.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { useLocation, useNavigate } from "react-router-dom"
|
|
1
|
+
import React from "react"
|
|
3
2
|
import Uploader from "./uploader/uploader"
|
|
4
3
|
import AppRoutes from "./routes"
|
|
5
4
|
import HeaderContainer from "./common/containers/header"
|
|
@@ -7,47 +6,23 @@ import FooterContainer from "./common/containers/footer"
|
|
|
7
6
|
import { SearchParamsProvider } from "./search/search-params-ctx"
|
|
8
7
|
import { UserModalOpenProvider } from "./utils/user-login-modal-ctx"
|
|
9
8
|
import { useAnalytics } from "./utils/analytics"
|
|
10
|
-
import { useUser } from "./queries/user"
|
|
11
9
|
import { NotificationsProvider } from "./users/notifications/user-notifications-context"
|
|
10
|
+
import { GoogleOrcidRedirect } from "./authentication/google-redirect"
|
|
12
11
|
import "../assets/email-header.png"
|
|
13
12
|
|
|
14
13
|
const Index: React.FC = () => {
|
|
15
14
|
useAnalytics()
|
|
16
15
|
|
|
17
|
-
const navigate = useNavigate()
|
|
18
|
-
const location = useLocation()
|
|
19
|
-
const { user, loading, error } = useUser()
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (
|
|
23
|
-
!loading &&
|
|
24
|
-
!error &&
|
|
25
|
-
location.pathname !== "/orcid-link" &&
|
|
26
|
-
user?.provider === "google"
|
|
27
|
-
) {
|
|
28
|
-
navigate("/orcid-link")
|
|
29
|
-
}
|
|
30
|
-
}, [location.pathname, user, loading, error, navigate])
|
|
31
|
-
|
|
32
|
-
if (loading || error) return null
|
|
33
|
-
|
|
34
|
-
const initialNotifications = user?.notifications?.map((n) => ({
|
|
35
|
-
id: n.id,
|
|
36
|
-
status: Array.isArray(n.notificationStatus)
|
|
37
|
-
? n.notificationStatus.map((ns) => ns.status.toLowerCase())[0] || "unread"
|
|
38
|
-
: n.notificationStatus?.status?.toLowerCase() || "unread",
|
|
39
|
-
...n,
|
|
40
|
-
})) || []
|
|
41
|
-
|
|
42
16
|
return (
|
|
43
17
|
<Uploader>
|
|
44
18
|
<SearchParamsProvider>
|
|
45
19
|
<UserModalOpenProvider>
|
|
46
|
-
<NotificationsProvider
|
|
20
|
+
<NotificationsProvider>
|
|
47
21
|
<div className="sticky-content">
|
|
48
22
|
<HeaderContainer />
|
|
49
23
|
<AppRoutes />
|
|
50
24
|
</div>
|
|
25
|
+
<GoogleOrcidRedirect />
|
|
51
26
|
</NotificationsProvider>
|
|
52
27
|
<div className="sticky-footer">
|
|
53
28
|
<FooterContainer />
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
1
2
|
import { gql, useQuery } from "@apollo/client"
|
|
2
3
|
import { useCookies } from "react-cookie"
|
|
3
4
|
import { getProfile } from "../authentication/profile"
|
|
@@ -211,7 +212,7 @@ export const ADVANCED_SEARCH_DATASETS_QUERY = gql`
|
|
|
211
212
|
|
|
212
213
|
// Reusable hook to fetch user data
|
|
213
214
|
export const useUser = (userId?: string) => {
|
|
214
|
-
const [cookies] = useCookies()
|
|
215
|
+
const [cookies, , removeCookie] = useCookies()
|
|
215
216
|
const profile = getProfile(cookies)
|
|
216
217
|
const profileSub = profile?.sub
|
|
217
218
|
|
|
@@ -230,6 +231,13 @@ export const useUser = (userId?: string) => {
|
|
|
230
231
|
Sentry.captureException(userError)
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
// Clear invalid session: cookie exists but query failed or returned no user
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!userLoading && profile && (userError || !userData?.user)) {
|
|
237
|
+
removeCookie("accessToken", { path: "/" })
|
|
238
|
+
}
|
|
239
|
+
}, [userLoading, profile, userError, userData?.user, removeCookie])
|
|
240
|
+
|
|
233
241
|
return {
|
|
234
242
|
user: userData?.user,
|
|
235
243
|
loading: userLoading,
|
|
@@ -33,7 +33,7 @@ const baseUser: User = {
|
|
|
33
33
|
const userMock = {
|
|
34
34
|
request: {
|
|
35
35
|
query: userQueries.GET_USER,
|
|
36
|
-
variables: {
|
|
36
|
+
variables: { userId: baseUser.id },
|
|
37
37
|
},
|
|
38
38
|
result: {
|
|
39
39
|
data: {
|
|
@@ -50,13 +50,13 @@ describe("<UserAccountView />", () => {
|
|
|
50
50
|
request: {
|
|
51
51
|
query: userQueries.UPDATE_USER,
|
|
52
52
|
variables: {
|
|
53
|
-
id: baseUser.
|
|
53
|
+
id: baseUser.id,
|
|
54
54
|
},
|
|
55
55
|
},
|
|
56
56
|
result: {
|
|
57
57
|
data: {
|
|
58
58
|
updateUser: {
|
|
59
|
-
id: baseUser.
|
|
59
|
+
id: baseUser.id,
|
|
60
60
|
location: "Marin, CA",
|
|
61
61
|
links: ["https://newlink.com"],
|
|
62
62
|
institution: "New University",
|
|
@@ -87,14 +87,14 @@ describe("<UserAccountView />", () => {
|
|
|
87
87
|
request: {
|
|
88
88
|
query: userQueries.UPDATE_USER,
|
|
89
89
|
variables: {
|
|
90
|
-
id: baseUser.
|
|
90
|
+
id: baseUser.id,
|
|
91
91
|
location: "Marin, CA",
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
94
|
result: {
|
|
95
95
|
data: {
|
|
96
96
|
updateUser: {
|
|
97
|
-
id: baseUser.
|
|
97
|
+
id: baseUser.id,
|
|
98
98
|
location: "Marin, CA",
|
|
99
99
|
links: ["https://newlink.com"],
|
|
100
100
|
institution: "New University",
|
|
@@ -128,14 +128,14 @@ describe("<UserAccountView />", () => {
|
|
|
128
128
|
request: {
|
|
129
129
|
query: userQueries.UPDATE_USER,
|
|
130
130
|
variables: {
|
|
131
|
-
id: baseUser.
|
|
131
|
+
id: baseUser.id,
|
|
132
132
|
institution: "New University",
|
|
133
133
|
},
|
|
134
134
|
},
|
|
135
135
|
result: {
|
|
136
136
|
data: {
|
|
137
137
|
updateUser: {
|
|
138
|
-
id: baseUser.
|
|
138
|
+
id: baseUser.id,
|
|
139
139
|
location: "Marin, CA",
|
|
140
140
|
links: ["https://newlink.com"],
|
|
141
141
|
institution: "New University",
|
|
@@ -168,14 +168,14 @@ describe("<UserAccountView />", () => {
|
|
|
168
168
|
request: {
|
|
169
169
|
query: userQueries.UPDATE_USER,
|
|
170
170
|
variables: {
|
|
171
|
-
id: baseUser.
|
|
171
|
+
id: baseUser.id,
|
|
172
172
|
links: ["https://newlink.com"],
|
|
173
173
|
},
|
|
174
174
|
},
|
|
175
175
|
result: {
|
|
176
176
|
data: {
|
|
177
177
|
updateUser: {
|
|
178
|
-
id: baseUser.
|
|
178
|
+
id: baseUser.id,
|
|
179
179
|
location: "Marin, CA",
|
|
180
180
|
links: ["https://newlink.com"],
|
|
181
181
|
institution: "New University",
|
|
@@ -208,14 +208,14 @@ describe("<UserAccountView />", () => {
|
|
|
208
208
|
request: {
|
|
209
209
|
query: userQueries.UPDATE_USER,
|
|
210
210
|
variables: {
|
|
211
|
-
id: baseUser.
|
|
211
|
+
id: baseUser.id,
|
|
212
212
|
links: ["https://newlink.com"],
|
|
213
213
|
},
|
|
214
214
|
},
|
|
215
215
|
result: {
|
|
216
216
|
data: {
|
|
217
217
|
updateUser: {
|
|
218
|
-
id: baseUser.
|
|
218
|
+
id: baseUser.id,
|
|
219
219
|
location: "Marin, CA",
|
|
220
220
|
links: ["https://newlink.com"],
|
|
221
221
|
institution: "New University",
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react"
|
|
2
8
|
import * as Sentry from "@sentry/react"
|
|
3
9
|
import { useMutation } from "@apollo/client"
|
|
4
10
|
import { UPDATE_NOTIFICATION_STATUS_MUTATION } from "../../queries/datasetEvents"
|
|
5
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
type MappedNotification,
|
|
13
|
+
mapRawEventToMappedNotification,
|
|
14
|
+
} from "../../types/event-types"
|
|
15
|
+
import { useUser } from "../../queries/user"
|
|
6
16
|
|
|
7
17
|
interface NotificationsContextValue {
|
|
8
18
|
notifications: MappedNotification[]
|
|
@@ -15,7 +25,7 @@ interface NotificationsContextValue {
|
|
|
15
25
|
|
|
16
26
|
interface NotificationsProviderProps {
|
|
17
27
|
children: React.ReactNode
|
|
18
|
-
|
|
28
|
+
userId?: string
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
const NotificationsContext = createContext<
|
|
@@ -24,11 +34,16 @@ const NotificationsContext = createContext<
|
|
|
24
34
|
|
|
25
35
|
export const NotificationsProvider: React.FC<NotificationsProviderProps> = ({
|
|
26
36
|
children,
|
|
27
|
-
|
|
37
|
+
userId,
|
|
28
38
|
}) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const { user } = useUser(userId)
|
|
40
|
+
const [notifications, setNotifications] = useState<MappedNotification[]>([])
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const mapped = user?.notifications?.map(mapRawEventToMappedNotification) ??
|
|
44
|
+
[]
|
|
45
|
+
setNotifications(mapped)
|
|
46
|
+
}, [user?.notifications])
|
|
32
47
|
const [updateEventStatus] = useMutation(UPDATE_NOTIFICATION_STATUS_MUTATION)
|
|
33
48
|
|
|
34
49
|
const handleUpdateNotification = useCallback(
|
|
@@ -10,10 +10,6 @@ import styles from "./scss/usernotifications.module.scss"
|
|
|
10
10
|
import iconUnread from "../../../assets/icon-unread.png"
|
|
11
11
|
import iconSaved from "../../../assets/icon-saved.png"
|
|
12
12
|
import iconArchived from "../../../assets/icon-archived.png"
|
|
13
|
-
import { useUser } from "../../queries/user"
|
|
14
|
-
import { Loading } from "../../components/loading/Loading"
|
|
15
|
-
import * as Sentry from "@sentry/react"
|
|
16
|
-
import { mapRawEventToMappedNotification } from "../../types/event-types"
|
|
17
13
|
import {
|
|
18
14
|
NotificationsProvider,
|
|
19
15
|
useNotifications,
|
|
@@ -27,8 +23,6 @@ export const UserNotificationsView: React.FC<UserNotificationsViewProps> = (
|
|
|
27
23
|
const { tab = "unread" } = useParams()
|
|
28
24
|
const navigate = useNavigate()
|
|
29
25
|
const location = useLocation()
|
|
30
|
-
const { user, loading, error } = useUser(orcidUser.id)
|
|
31
|
-
|
|
32
26
|
const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({
|
|
33
27
|
width: "0px",
|
|
34
28
|
transform: "translateX(0px)",
|
|
@@ -63,19 +57,8 @@ export const UserNotificationsView: React.FC<UserNotificationsViewProps> = (
|
|
|
63
57
|
}
|
|
64
58
|
}, [tab, orcidUser.orcid, navigate])
|
|
65
59
|
|
|
66
|
-
if (loading) return <Loading />
|
|
67
|
-
if (error) {
|
|
68
|
-
Sentry.captureException(error)
|
|
69
|
-
return (
|
|
70
|
-
<div>Error loading notifications: {error.message}. Please try again.</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const initialNotifications =
|
|
75
|
-
user?.notifications.map(mapRawEventToMappedNotification) || []
|
|
76
|
-
|
|
77
60
|
return (
|
|
78
|
-
<NotificationsProvider
|
|
61
|
+
<NotificationsProvider userId={orcidUser.id}>
|
|
79
62
|
<div data-testid="user-notifications-view">
|
|
80
63
|
<h3>Notifications for {orcidUser.name}</h3>
|
|
81
64
|
<div className={styles.tabContainer}>
|
|
@@ -14,12 +14,12 @@ import { pageTitle } from "../resources/strings.js"
|
|
|
14
14
|
export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
15
15
|
orcidUser,
|
|
16
16
|
}) => {
|
|
17
|
-
const [userLinks, setLinks] = useState<string[]>(orcidUser
|
|
17
|
+
const [userLinks, setLinks] = useState<string[]>(orcidUser.links ?? [])
|
|
18
18
|
const [userLocation, setLocation] = useState<string>(
|
|
19
|
-
orcidUser
|
|
19
|
+
orcidUser.location ?? "",
|
|
20
20
|
)
|
|
21
21
|
const [userInstitution, setInstitution] = useState<string>(
|
|
22
|
-
orcidUser
|
|
22
|
+
orcidUser.institution ?? "",
|
|
23
23
|
)
|
|
24
24
|
const [updateUser] = useMutation(UPDATE_USER)
|
|
25
25
|
|
|
@@ -28,13 +28,13 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
28
28
|
try {
|
|
29
29
|
await updateUser({
|
|
30
30
|
variables: {
|
|
31
|
-
id: orcidUser
|
|
31
|
+
id: orcidUser.id,
|
|
32
32
|
links: newLinks,
|
|
33
33
|
},
|
|
34
34
|
refetchQueries: [
|
|
35
35
|
{
|
|
36
36
|
query: GET_USER,
|
|
37
|
-
variables: {
|
|
37
|
+
variables: { userId: orcidUser.id },
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
})
|
|
@@ -49,13 +49,13 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
49
49
|
try {
|
|
50
50
|
await updateUser({
|
|
51
51
|
variables: {
|
|
52
|
-
id: orcidUser
|
|
52
|
+
id: orcidUser.id,
|
|
53
53
|
location: newLocation,
|
|
54
54
|
},
|
|
55
55
|
refetchQueries: [
|
|
56
56
|
{
|
|
57
57
|
query: GET_USER,
|
|
58
|
-
variables: {
|
|
58
|
+
variables: { userId: orcidUser.id },
|
|
59
59
|
},
|
|
60
60
|
],
|
|
61
61
|
})
|
|
@@ -70,13 +70,13 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
70
70
|
try {
|
|
71
71
|
await updateUser({
|
|
72
72
|
variables: {
|
|
73
|
-
id: orcidUser
|
|
73
|
+
id: orcidUser.id,
|
|
74
74
|
institution: newInstitution,
|
|
75
75
|
},
|
|
76
76
|
refetchQueries: [
|
|
77
77
|
{
|
|
78
78
|
query: GET_USER,
|
|
79
|
-
variables: {
|
|
79
|
+
variables: { userId: orcidUser.id },
|
|
80
80
|
},
|
|
81
81
|
],
|
|
82
82
|
})
|
|
@@ -107,7 +107,7 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
107
107
|
<span>ORCID:</span>
|
|
108
108
|
{orcidUser.orcid}
|
|
109
109
|
</li>
|
|
110
|
-
{orcidUser
|
|
110
|
+
{orcidUser.github &&
|
|
111
111
|
(
|
|
112
112
|
<li>
|
|
113
113
|
<span>GitHub:</span>
|
|
@@ -128,7 +128,7 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
128
128
|
data-testid="links-section"
|
|
129
129
|
/>
|
|
130
130
|
|
|
131
|
-
{orcidUser
|
|
131
|
+
{orcidUser.orcid !== undefined && (
|
|
132
132
|
<div className={styles.umbOrcidConsent}>
|
|
133
133
|
<div className={styles.umbOrcidHeading}>
|
|
134
134
|
<h4>ORCID Integration</h4>
|
|
@@ -10,15 +10,17 @@ export interface UserMenuProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const UserMenu: React.FC<UserMenuProps> = ({ signOutAndRedirect }) => {
|
|
13
|
-
const { user } = useUser()
|
|
13
|
+
const { user, loading } = useUser()
|
|
14
14
|
const { notifications } = useNotifications()
|
|
15
15
|
|
|
16
|
+
if (loading || !user) return null
|
|
17
|
+
|
|
16
18
|
const inboxCount =
|
|
17
19
|
notifications?.filter((n) => n.status === "unread").length || 0
|
|
18
20
|
|
|
19
21
|
return (
|
|
20
22
|
<span className="user-menu-wrap">
|
|
21
|
-
{user
|
|
23
|
+
{user.orcid && (
|
|
22
24
|
<span className="notifications-link">
|
|
23
25
|
<Link to={`/user/${user.orcid}/notifications/unread`}>
|
|
24
26
|
<i className="fa fa-inbox">
|
|
@@ -41,7 +43,7 @@ export const UserMenu: React.FC<UserMenuProps> = ({ signOutAndRedirect }) => {
|
|
|
41
43
|
|
|
42
44
|
<Dropdown
|
|
43
45
|
className="user-menu-dropdown"
|
|
44
|
-
label={user
|
|
46
|
+
label={user.avatar
|
|
45
47
|
? (
|
|
46
48
|
<img
|
|
47
49
|
className="user-menu-label avatar"
|
|
@@ -56,23 +58,23 @@ export const UserMenu: React.FC<UserMenuProps> = ({ signOutAndRedirect }) => {
|
|
|
56
58
|
<li className="dropdown-header">
|
|
57
59
|
<p>
|
|
58
60
|
<span>Hello</span> <br />
|
|
59
|
-
{user
|
|
60
|
-
{user
|
|
61
|
+
{user.name} <br />
|
|
62
|
+
{user.email}
|
|
61
63
|
</p>
|
|
62
64
|
<p>
|
|
63
|
-
<span>signed in via {user
|
|
65
|
+
<span>signed in via {user.provider}</span>
|
|
64
66
|
</p>
|
|
65
67
|
</li>
|
|
66
68
|
|
|
67
69
|
<li>
|
|
68
70
|
<Link
|
|
69
|
-
to={user
|
|
71
|
+
to={user.orcid ? `/user/${user.orcid}` : "/search?mydatasets"}
|
|
70
72
|
>
|
|
71
73
|
My Datasets
|
|
72
74
|
</Link>
|
|
73
75
|
</li>
|
|
74
76
|
|
|
75
|
-
{user
|
|
77
|
+
{user.orcid && (
|
|
76
78
|
<li>
|
|
77
79
|
<Link to={`/user/${user.orcid}/account`}>Account Info</Link>
|
|
78
80
|
</li>
|
|
@@ -82,13 +84,13 @@ export const UserMenu: React.FC<UserMenuProps> = ({ signOutAndRedirect }) => {
|
|
|
82
84
|
<Link to="/keygen">Obtain an API Key</Link>
|
|
83
85
|
</li>
|
|
84
86
|
|
|
85
|
-
{user
|
|
87
|
+
{user.provider !== "orcid" && (
|
|
86
88
|
<li className="user-menu-link">
|
|
87
89
|
<a href="/crn/auth/orcid?link=true">Link ORCID to my account</a>
|
|
88
90
|
</li>
|
|
89
91
|
)}
|
|
90
92
|
|
|
91
|
-
{user
|
|
93
|
+
{user.admin && (
|
|
92
94
|
<li className="user-menu-link">
|
|
93
95
|
<Link to="/admin">Admin</Link>
|
|
94
96
|
</li>
|
package/vite.config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineConfig } from "vite"
|
|
2
2
|
import { nodePolyfills } from "vite-plugin-node-polyfills"
|
|
3
|
+
import { codecovVitePlugin } from "@codecov/vite-plugin"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Vite plugin to hack a bug injected by the default assetImportMetaUrlPlugin
|
|
@@ -32,9 +33,7 @@ export default defineConfig({
|
|
|
32
33
|
build: {
|
|
33
34
|
sourcemap: true,
|
|
34
35
|
rollupOptions: {
|
|
35
|
-
external: [
|
|
36
|
-
"/crn/config.js",
|
|
37
|
-
],
|
|
36
|
+
external: ["/crn/config.js"],
|
|
38
37
|
},
|
|
39
38
|
},
|
|
40
39
|
optimizeDeps: {
|
|
@@ -62,5 +61,15 @@ export default defineConfig({
|
|
|
62
61
|
},
|
|
63
62
|
],
|
|
64
63
|
},
|
|
65
|
-
plugins: [
|
|
64
|
+
plugins: [
|
|
65
|
+
workaroundAssetImportMetaUrlPluginBug(),
|
|
66
|
+
nodePolyfills(),
|
|
67
|
+
/** @type {any} */ (
|
|
68
|
+
codecovVitePlugin({
|
|
69
|
+
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
|
|
70
|
+
bundleName: "@openneuro/app",
|
|
71
|
+
uploadToken: process.env.CODECOV_TOKEN,
|
|
72
|
+
})
|
|
73
|
+
),
|
|
74
|
+
],
|
|
66
75
|
})
|