@openneuro/app 5.0.0-alpha.0 → 5.1.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 +2 -2
- package/src/scripts/dataset/__tests__/dataset-canonical-url.spec.ts +20 -0
- package/src/scripts/dataset/dataset-canonical-url.ts +14 -0
- package/src/scripts/dataset/dataset-query.jsx +8 -0
- package/src/scripts/dataset/download/download-native.js +28 -45
- package/src/scripts/dataset/download/download-query.js +13 -17
- package/src/scripts/queries/user.ts +9 -1
- package/src/scripts/search/components/SearchResultItem.tsx +1 -0
- package/src/scripts/types/event-types.ts +1 -0
- package/src/scripts/types/user-types.ts +1 -0
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +5 -5
- package/src/scripts/users/__tests__/user-routes.spec.tsx +0 -5
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +4 -4
- package/src/scripts/users/components/editable-content.tsx +3 -1
- package/src/scripts/users/user-account-view.tsx +6 -2
- package/src/scripts/users/user-menu.tsx +10 -1
- package/src/scripts/users/user-query.tsx +2 -7
- package/src/scripts/users/user-routes.tsx +3 -3
- package/src/scripts/users/user-tabs.tsx +13 -12
- package/src/scripts/utils/user-datasets.tsx +1 -0
- package/src/scripts/workers/schema.worker.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/app",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "React JS web frontend for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "public/client.js",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "e3444f63f43cb7e9d3ecfac51d2a42cdcc4f4e60"
|
|
83
83
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { datasetCanonicalUrl } from "../dataset-canonical-url"
|
|
2
|
+
import { dataset } from "../../fixtures/dataset-query"
|
|
3
|
+
import { vitest } from "vitest"
|
|
4
|
+
|
|
5
|
+
vitest.mock("../../config", () => ({
|
|
6
|
+
config: { url: "http://localhost:9876" },
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
describe("datasetCanonicalUrl", () => {
|
|
10
|
+
it("returns the canonical URL for a dataset with snapshots", () => {
|
|
11
|
+
const url = datasetCanonicalUrl(dataset)
|
|
12
|
+
expect(url.href).toBe("http://localhost:9876/datasets/ds001032/versions/2.0.0")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("returns the canonical URL for a draft dataset", () => {
|
|
16
|
+
const draftDataset = { ...dataset, snapshots: [] }
|
|
17
|
+
const url = datasetCanonicalUrl(draftDataset)
|
|
18
|
+
expect(url.href).toBe("http://localhost:9876/datasets/ds001032")
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { config } from "../config"
|
|
2
|
+
|
|
3
|
+
export function datasetCanonicalUrl(dataset): URL {
|
|
4
|
+
const siteUrl = config.url
|
|
5
|
+
if (dataset.snapshots.length) {
|
|
6
|
+
const latestSnapshot = dataset.snapshots.slice(-1)[0]
|
|
7
|
+
return new URL(
|
|
8
|
+
`/datasets/${dataset.id}/versions/${latestSnapshot.tag}`,
|
|
9
|
+
siteUrl,
|
|
10
|
+
)
|
|
11
|
+
} else {
|
|
12
|
+
return new URL(`/datasets/${dataset.id}`, siteUrl)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as Sentry from "@sentry/react"
|
|
|
3
3
|
import PropTypes from "prop-types"
|
|
4
4
|
import { useNavigate, useParams } from "react-router-dom"
|
|
5
5
|
import { useApolloClient, useQuery } from "@apollo/client"
|
|
6
|
+
import Helmet from "react-helmet"
|
|
6
7
|
import { Loading } from "../components/loading/Loading"
|
|
7
8
|
|
|
8
9
|
import DatasetQueryContext from "../datalad/dataset/dataset-query-context.js"
|
|
@@ -16,6 +17,7 @@ import { trackAnalytics } from "../utils/datalad"
|
|
|
16
17
|
import FourOFourPage from "../errors/404page"
|
|
17
18
|
import FourOThreePage from "../errors/403page"
|
|
18
19
|
import { getDatasetPage, getDraftPage } from "../queries/dataset"
|
|
20
|
+
import { datasetCanonicalUrl } from "./dataset-canonical-url"
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Query to load and render dataset page - most dataset loading is done here
|
|
@@ -63,6 +65,12 @@ export const DatasetQueryHook = ({ datasetId, draft }) => {
|
|
|
63
65
|
}
|
|
64
66
|
return (
|
|
65
67
|
<DatasetContext.Provider value={data.dataset}>
|
|
68
|
+
<Helmet>
|
|
69
|
+
<link
|
|
70
|
+
rel="canonical"
|
|
71
|
+
href={datasetCanonicalUrl(data.dataset).toString()}
|
|
72
|
+
/>
|
|
73
|
+
</Helmet>
|
|
66
74
|
<ErrorBoundary subject={"error in dataset page"}>
|
|
67
75
|
<DatasetQueryContext.Provider
|
|
68
76
|
value={{
|
|
@@ -43,65 +43,48 @@ class DownloadAbortError extends Error {
|
|
|
43
43
|
let downloadCanceled
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Download for file trees via browser file access API
|
|
47
47
|
*/
|
|
48
48
|
const downloadTree = async (
|
|
49
49
|
{ datasetId, snapshotTag, client, dirHandle, toastId },
|
|
50
50
|
path = "",
|
|
51
|
-
tree = null,
|
|
52
51
|
) => {
|
|
53
52
|
const filesToDownload = await downloadDataset(client)({
|
|
54
53
|
datasetId,
|
|
55
54
|
snapshotTag,
|
|
56
|
-
tree,
|
|
57
55
|
})
|
|
58
56
|
for (const [_index, file] of filesToDownload.entries()) {
|
|
59
57
|
const downloadPath = path ? `${path}/${file.filename}` : file.filename
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
// Regular file
|
|
59
|
+
if (downloadCanceled) {
|
|
60
|
+
throw new DownloadAbortError("Download canceled by user request")
|
|
61
|
+
}
|
|
62
|
+
const fileHandle = await openFileTree(
|
|
63
|
+
dirHandle,
|
|
64
|
+
path ? `${path}/${file.filename}` : file.filename,
|
|
65
|
+
)
|
|
66
|
+
// Skip files which are already complete
|
|
67
|
+
if (fileHandle.size == file.size) continue
|
|
68
|
+
const writable = await fileHandle.createWritable()
|
|
69
|
+
const { body, status, statusText } = await fetch(file.urls[0])
|
|
70
|
+
let loaded = 0
|
|
71
|
+
const progress = new TransformStream({
|
|
72
|
+
transform(chunk, controller) {
|
|
73
|
+
downloadToastUpdate(toastId, loaded / file.size, {
|
|
64
74
|
datasetId,
|
|
65
75
|
snapshotTag,
|
|
66
|
-
|
|
67
|
-
dirHandle,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
downloadPath,
|
|
77
|
+
dirName: dirHandle.name,
|
|
78
|
+
})
|
|
79
|
+
loaded += chunk.length
|
|
80
|
+
controller.enqueue(chunk)
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
if (status === 200) {
|
|
84
|
+
await body.pipeThrough(progress).pipeTo(writable)
|
|
73
85
|
} else {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new DownloadAbortError("Download canceled by user request")
|
|
77
|
-
}
|
|
78
|
-
const fileHandle = await openFileTree(
|
|
79
|
-
dirHandle,
|
|
80
|
-
path ? `${path}/${file.filename}` : file.filename,
|
|
81
|
-
)
|
|
82
|
-
// Skip files which are already complete
|
|
83
|
-
if (fileHandle.size == file.size) continue
|
|
84
|
-
const writable = await fileHandle.createWritable()
|
|
85
|
-
const { body, status, statusText } = await fetch(file.urls[0])
|
|
86
|
-
let loaded = 0
|
|
87
|
-
const progress = new TransformStream({
|
|
88
|
-
transform(chunk, controller) {
|
|
89
|
-
downloadToastUpdate(toastId, loaded / file.size, {
|
|
90
|
-
datasetId,
|
|
91
|
-
snapshotTag,
|
|
92
|
-
downloadPath,
|
|
93
|
-
dirName: dirHandle.name,
|
|
94
|
-
})
|
|
95
|
-
loaded += chunk.length
|
|
96
|
-
controller.enqueue(chunk)
|
|
97
|
-
},
|
|
98
|
-
})
|
|
99
|
-
if (status === 200) {
|
|
100
|
-
await body.pipeThrough(progress).pipeTo(writable)
|
|
101
|
-
} else {
|
|
102
|
-
Sentry.captureException(statusText)
|
|
103
|
-
return requestFailureToast(file.filename)
|
|
104
|
-
}
|
|
86
|
+
Sentry.captureException(statusText)
|
|
87
|
+
return requestFailureToast(file.filename)
|
|
105
88
|
}
|
|
106
89
|
}
|
|
107
90
|
}
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { gql } from "@apollo/client"
|
|
2
2
|
|
|
3
3
|
export const DOWNLOAD_DATASET = gql`
|
|
4
|
-
query downloadDraft($datasetId: ID
|
|
5
|
-
|
|
6
|
-
id
|
|
7
|
-
draft {
|
|
4
|
+
query downloadDraft($datasetId: ID!) {
|
|
5
|
+
dataset(id: $datasetId) {
|
|
8
6
|
id
|
|
9
|
-
|
|
7
|
+
draft {
|
|
10
8
|
id
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
files(recursive: true) {
|
|
10
|
+
id
|
|
11
|
+
directory
|
|
12
|
+
filename
|
|
13
|
+
size
|
|
14
|
+
urls
|
|
15
|
+
}
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
}
|
|
20
19
|
`
|
|
21
20
|
|
|
22
21
|
export const DOWNLOAD_SNAPSHOT = gql`
|
|
23
|
-
query downloadSnapshot($datasetId: ID!, $tag: String
|
|
22
|
+
query downloadSnapshot($datasetId: ID!, $tag: String!) {
|
|
24
23
|
snapshot(datasetId: $datasetId, tag: $tag) {
|
|
25
24
|
id
|
|
26
|
-
files(
|
|
25
|
+
files(recursive: true) {
|
|
27
26
|
id
|
|
28
|
-
key
|
|
29
27
|
directory
|
|
30
28
|
filename
|
|
31
29
|
size
|
|
@@ -36,14 +34,13 @@ export const DOWNLOAD_SNAPSHOT = gql`
|
|
|
36
34
|
`
|
|
37
35
|
|
|
38
36
|
export const downloadDataset =
|
|
39
|
-
(client) => async ({ datasetId, snapshotTag
|
|
37
|
+
(client) => async ({ datasetId, snapshotTag }) => {
|
|
40
38
|
if (snapshotTag) {
|
|
41
39
|
const { data } = await client.query({
|
|
42
40
|
query: DOWNLOAD_SNAPSHOT,
|
|
43
41
|
variables: {
|
|
44
42
|
datasetId,
|
|
45
43
|
tag: snapshotTag,
|
|
46
|
-
tree: tree,
|
|
47
44
|
},
|
|
48
45
|
})
|
|
49
46
|
return data.snapshot.files
|
|
@@ -52,7 +49,6 @@ export const downloadDataset =
|
|
|
52
49
|
query: DOWNLOAD_DATASET,
|
|
53
50
|
variables: {
|
|
54
51
|
datasetId,
|
|
55
|
-
tree,
|
|
56
52
|
},
|
|
57
53
|
})
|
|
58
54
|
return data.dataset.draft.files
|
|
@@ -207,8 +207,15 @@ export const ADVANCED_SEARCH_DATASETS_QUERY = gql`
|
|
|
207
207
|
}
|
|
208
208
|
`
|
|
209
209
|
|
|
210
|
+
interface UseUserOptions {
|
|
211
|
+
errorPolicy: "none" | "ignore" | "all"
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
// Reusable hook to fetch user data
|
|
211
|
-
export const useUser = (
|
|
215
|
+
export const useUser = (
|
|
216
|
+
userId?: string,
|
|
217
|
+
options: UseUserOptions = { errorPolicy: "none" },
|
|
218
|
+
) => {
|
|
212
219
|
const [cookies] = useCookies()
|
|
213
220
|
const profile = getProfile(cookies)
|
|
214
221
|
const profileSub = profile?.sub
|
|
@@ -222,6 +229,7 @@ export const useUser = (userId?: string) => {
|
|
|
222
229
|
} = useQuery(GET_USER, {
|
|
223
230
|
variables: { userId: finalUserId },
|
|
224
231
|
skip: !finalUserId,
|
|
232
|
+
errorPolicy: options.errorPolicy,
|
|
225
233
|
})
|
|
226
234
|
|
|
227
235
|
if (userError) {
|
|
@@ -93,6 +93,7 @@ export const mapRawEventToMappedNotification = (
|
|
|
93
93
|
reason,
|
|
94
94
|
} = event
|
|
95
95
|
|
|
96
|
+
// eslint-disable-next-line no-useless-assignment
|
|
96
97
|
let title = "General Notification"
|
|
97
98
|
const mappedType: MappedNotification["type"] = type
|
|
98
99
|
let approval: MappedNotification["approval"]
|
|
@@ -68,7 +68,7 @@ describe("<UserAccountView />", () => {
|
|
|
68
68
|
render(
|
|
69
69
|
<BrowserRouter>
|
|
70
70
|
<MockedProvider mocks={mocks} addTypename={false}>
|
|
71
|
-
<UserAccountView orcidUser={baseUser} />
|
|
71
|
+
<UserAccountView orcidUser={baseUser} hasEdit={true} />
|
|
72
72
|
</MockedProvider>
|
|
73
73
|
</BrowserRouter>,
|
|
74
74
|
)
|
|
@@ -106,7 +106,7 @@ describe("<UserAccountView />", () => {
|
|
|
106
106
|
render(
|
|
107
107
|
<BrowserRouter>
|
|
108
108
|
<MockedProvider mocks={mocks} addTypename={false}>
|
|
109
|
-
<UserAccountView orcidUser={baseUser} />
|
|
109
|
+
<UserAccountView orcidUser={baseUser} hasEdit={true} />
|
|
110
110
|
</MockedProvider>
|
|
111
111
|
</BrowserRouter>,
|
|
112
112
|
)
|
|
@@ -146,7 +146,7 @@ describe("<UserAccountView />", () => {
|
|
|
146
146
|
render(
|
|
147
147
|
<BrowserRouter>
|
|
148
148
|
<MockedProvider mocks={mocks} addTypename={false}>
|
|
149
|
-
<UserAccountView orcidUser={baseUser} />
|
|
149
|
+
<UserAccountView orcidUser={baseUser} hasEdit={true} />
|
|
150
150
|
</MockedProvider>,
|
|
151
151
|
</BrowserRouter>,
|
|
152
152
|
)
|
|
@@ -186,7 +186,7 @@ describe("<UserAccountView />", () => {
|
|
|
186
186
|
render(
|
|
187
187
|
<BrowserRouter>
|
|
188
188
|
<MockedProvider mocks={mocks} addTypename={false}>
|
|
189
|
-
<UserAccountView orcidUser={baseUser} />
|
|
189
|
+
<UserAccountView orcidUser={baseUser} hasEdit={true} />
|
|
190
190
|
</MockedProvider>,
|
|
191
191
|
</BrowserRouter>,
|
|
192
192
|
)
|
|
@@ -226,7 +226,7 @@ describe("<UserAccountView />", () => {
|
|
|
226
226
|
render(
|
|
227
227
|
<BrowserRouter>
|
|
228
228
|
<MockedProvider mocks={mocks} addTypename={false}>
|
|
229
|
-
<UserAccountView orcidUser={baseUser} />
|
|
229
|
+
<UserAccountView orcidUser={baseUser} hasEdit={true} />
|
|
230
230
|
</MockedProvider>,
|
|
231
231
|
</BrowserRouter>,
|
|
232
232
|
)
|
|
@@ -285,11 +285,6 @@ describe("UserRoutes Component", () => {
|
|
|
285
285
|
expect(await screen.findByTestId("404-page")).toBeInTheDocument()
|
|
286
286
|
})
|
|
287
287
|
|
|
288
|
-
it("renders FourOThreePage for restricted route /account when hasEdit is false", async () => {
|
|
289
|
-
setupUserRoutes(userToPass, "/account", false, true)
|
|
290
|
-
expect(await screen.findByTestId("403-page")).toBeInTheDocument()
|
|
291
|
-
})
|
|
292
|
-
|
|
293
288
|
it("renders FourOThreePage for restricted route /notifications when hasEdit is false", async () => {
|
|
294
289
|
setupUserRoutes(userToPass, "/notifications", false, true)
|
|
295
290
|
expect(await screen.findByTestId("403-page")).toBeInTheDocument()
|
|
@@ -23,14 +23,14 @@ const UserAccountTabsWrapper: React.FC = () => {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
describe("UserAccountTabs Component", () => {
|
|
26
|
-
it("should not render
|
|
26
|
+
it("should not notification render tab when hasEdit is false", () => {
|
|
27
27
|
render(<UserAccountTabsWrapper />)
|
|
28
28
|
|
|
29
29
|
expect(screen.getByText("My Datasets")).toBeInTheDocument()
|
|
30
30
|
|
|
31
31
|
fireEvent.click(screen.getByText("Toggle hasEdit"))
|
|
32
32
|
|
|
33
|
-
expect(screen.queryByText("
|
|
33
|
+
expect(screen.queryByText("Notifications")).not.toBeInTheDocument()
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
it("should render tabs when hasEdit is toggled back to true", () => {
|
|
@@ -39,10 +39,10 @@ describe("UserAccountTabs Component", () => {
|
|
|
39
39
|
expect(screen.getByText("My Datasets")).toBeInTheDocument()
|
|
40
40
|
|
|
41
41
|
fireEvent.click(screen.getByText("Toggle hasEdit"))
|
|
42
|
-
expect(screen.queryByText("
|
|
42
|
+
expect(screen.queryByText("Notifications")).not.toBeInTheDocument()
|
|
43
43
|
|
|
44
44
|
fireEvent.click(screen.getByText("Toggle hasEdit"))
|
|
45
|
-
expect(screen.getByText("
|
|
45
|
+
expect(screen.getByText("Notifications")).toBeInTheDocument()
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
it("should update active class on the correct NavLink based on route", () => {
|
|
@@ -13,6 +13,7 @@ interface EditableContentProps {
|
|
|
13
13
|
heading: string
|
|
14
14
|
validation?: RegExp | ((value: string) => boolean)
|
|
15
15
|
validationMessage?: string
|
|
16
|
+
hasEdit?: boolean
|
|
16
17
|
"data-testid"?: string
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -23,6 +24,7 @@ export const EditableContent: React.FC<EditableContentProps> = ({
|
|
|
23
24
|
heading,
|
|
24
25
|
validation,
|
|
25
26
|
validationMessage,
|
|
27
|
+
hasEdit,
|
|
26
28
|
"data-testid": testId,
|
|
27
29
|
}) => {
|
|
28
30
|
const [editing, setEditing] = useState(false)
|
|
@@ -52,7 +54,7 @@ export const EditableContent: React.FC<EditableContentProps> = ({
|
|
|
52
54
|
<h4>{heading}</h4>
|
|
53
55
|
{editing
|
|
54
56
|
? <CloseButton action={closeEditing} />
|
|
55
|
-
: <EditButton action={() => setEditing(true)} />}
|
|
57
|
+
: hasEdit && <EditButton action={() => setEditing(true)} />}
|
|
56
58
|
</span>
|
|
57
59
|
{editing
|
|
58
60
|
? (
|
|
@@ -13,6 +13,7 @@ import { pageTitle } from "../resources/strings.js"
|
|
|
13
13
|
|
|
14
14
|
export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
15
15
|
orcidUser,
|
|
16
|
+
hasEdit,
|
|
16
17
|
}) => {
|
|
17
18
|
const [userLinks, setLinks] = useState<string[]>(orcidUser.links ?? [])
|
|
18
19
|
const [userLocation, setLocation] = useState<string>(
|
|
@@ -89,7 +90,7 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
89
90
|
<>
|
|
90
91
|
<Helmet>
|
|
91
92
|
<title>
|
|
92
|
-
|
|
93
|
+
{orcidUser.name || "User"} profile - {pageTitle}
|
|
93
94
|
</title>
|
|
94
95
|
</Helmet>
|
|
95
96
|
<div data-testid="user-account-view" className={styles.useraccountview}>
|
|
@@ -126,9 +127,10 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
126
127
|
validation={validateHttpHttpsUrl}
|
|
127
128
|
validationMessage="Invalid URL format. Please start with http:// or https://"
|
|
128
129
|
data-testid="links-section"
|
|
130
|
+
hasEdit={hasEdit}
|
|
129
131
|
/>
|
|
130
132
|
|
|
131
|
-
{orcidUser.orcid !== undefined && (
|
|
133
|
+
{hasEdit && orcidUser.orcid !== undefined && (
|
|
132
134
|
<div className={styles.umbOrcidConsent}>
|
|
133
135
|
<div className={styles.umbOrcidHeading}>
|
|
134
136
|
<h4>ORCID Integration</h4>
|
|
@@ -144,11 +146,13 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
144
146
|
editableContent={userLocation}
|
|
145
147
|
setRows={handleLocationChange}
|
|
146
148
|
heading="Location"
|
|
149
|
+
hasEdit={hasEdit}
|
|
147
150
|
data-testid="location-section"
|
|
148
151
|
/>
|
|
149
152
|
<EditableContent
|
|
150
153
|
editableContent={userInstitution}
|
|
151
154
|
setRows={handleInstitutionChange}
|
|
155
|
+
hasEdit={hasEdit}
|
|
152
156
|
heading="Institution"
|
|
153
157
|
data-testid="institution-section"
|
|
154
158
|
/>
|
|
@@ -3,6 +3,7 @@ import { Link } from "react-router-dom"
|
|
|
3
3
|
import { Dropdown } from "../components/dropdown/Dropdown"
|
|
4
4
|
import { useUser } from "../queries/user"
|
|
5
5
|
import { useNotifications } from "./notifications/user-notifications-context"
|
|
6
|
+
import orcidIcon from "../../assets/orcid_24x24.png"
|
|
6
7
|
import "./scss/user-menu.scss"
|
|
7
8
|
|
|
8
9
|
interface UserMenuListProps {
|
|
@@ -101,10 +102,18 @@ export const UserMenu: React.FC<UserMenuProps> = ({ signOutAndRedirect }) => {
|
|
|
101
102
|
<p>
|
|
102
103
|
<span>Hello</span> <br />
|
|
103
104
|
{user.name} <br />
|
|
104
|
-
{user.email}
|
|
105
105
|
</p>
|
|
106
106
|
<p>
|
|
107
107
|
<span>signed in via {user.provider}</span>
|
|
108
|
+
{user?.orcid && (
|
|
109
|
+
<a
|
|
110
|
+
href={`https://orcid.org/${user.orcid}`}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
>
|
|
114
|
+
<img src={orcidIcon} alt="ORCID iD" /> {user.orcid}
|
|
115
|
+
</a>
|
|
116
|
+
) || user.email}
|
|
108
117
|
</p>
|
|
109
118
|
</li>
|
|
110
119
|
|
|
@@ -10,23 +10,18 @@ import { useUser } from "../queries/user"
|
|
|
10
10
|
|
|
11
11
|
export const UserQuery: React.FC = () => {
|
|
12
12
|
const { orcid } = useParams()
|
|
13
|
-
const
|
|
14
|
-
const { user, loading, error } = useUser(orcid)
|
|
13
|
+
const { user, loading } = useUser(orcid, { errorPolicy: "all" })
|
|
15
14
|
|
|
16
15
|
const [cookies] = useCookies()
|
|
17
16
|
const profile = getProfile(cookies)
|
|
18
17
|
const isAdminUser = isAdmin()
|
|
19
18
|
|
|
20
|
-
if (!
|
|
19
|
+
if (!isValidOrcid(orcid)) {
|
|
21
20
|
return <FourOFourPage />
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
if (loading) return <div>Loading...</div>
|
|
25
24
|
|
|
26
|
-
if (error || !user) {
|
|
27
|
-
return <FourOFourPage />
|
|
28
|
-
}
|
|
29
|
-
|
|
30
25
|
// is admin or profile matches id from the user data being returned
|
|
31
26
|
const isUser = (user?.id === profile?.sub) ? true : false
|
|
32
27
|
const hasEdit = isAdminUser || (user?.id === profile?.sub) ? true : false
|
|
@@ -60,9 +60,9 @@ export const UserRoutes: React.FC<UserRoutesProps> = (
|
|
|
60
60
|
{/* This route handles the user account page */}
|
|
61
61
|
<Route
|
|
62
62
|
path="account"
|
|
63
|
-
element={
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
element={
|
|
64
|
+
<UserAccountView orcidUser={orcidUser} hasEdit={hasEdit} />
|
|
65
|
+
}
|
|
66
66
|
/>
|
|
67
67
|
{/* This route handles the user notifications and its sub-routes */}
|
|
68
68
|
<Route
|
|
@@ -29,8 +29,6 @@ export const UserAccountTabs: React.FC<UserAccountTabsProps> = (
|
|
|
29
29
|
setClicked(true)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
if (!hasEdit) return null
|
|
33
|
-
|
|
34
32
|
return (
|
|
35
33
|
<div className={styles.userAccountTabLinks}>
|
|
36
34
|
<ul
|
|
@@ -50,16 +48,19 @@ export const UserAccountTabs: React.FC<UserAccountTabsProps> = (
|
|
|
50
48
|
{isUser ? "My" : "User"} Datasets
|
|
51
49
|
</NavLink>
|
|
52
50
|
</li>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
|
|
52
|
+
{hasEdit && (
|
|
53
|
+
<li>
|
|
54
|
+
<NavLink
|
|
55
|
+
data-testid="user-notifications-tab"
|
|
56
|
+
to="notifications"
|
|
57
|
+
className={({ isActive }) => (isActive ? styles.active : "")}
|
|
58
|
+
onClick={handleClick}
|
|
59
|
+
>
|
|
60
|
+
Notifications
|
|
61
|
+
</NavLink>
|
|
62
|
+
</li>
|
|
63
|
+
)}
|
|
63
64
|
|
|
64
65
|
<li>
|
|
65
66
|
<NavLink
|