@openneuro/app 5.0.0 → 5.1.1
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/download/download-native.js +28 -45
- package/src/scripts/dataset/download/download-query.js +13 -17
- package/src/scripts/queries/user.ts +17 -5
- 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 +2 -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/components/profile-privacy.tsx +62 -0
- package/src/scripts/users/user-account-view.tsx +31 -13
- package/src/scripts/users/user-datasets-view.tsx +7 -0
- package/src/scripts/users/user-menu.tsx +10 -1
- package/src/scripts/users/user-query.tsx +5 -5
- 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.1",
|
|
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": "035b0b4544d4b287506ea00aff3b424ed816611a"
|
|
83
83
|
}
|
|
@@ -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
|
|
@@ -64,7 +64,8 @@ export const GET_USER = gql`
|
|
|
64
64
|
status
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
orcidConsent
|
|
67
|
+
orcidConsent
|
|
68
|
+
profilePrivate
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
`
|
|
@@ -74,22 +75,25 @@ export const UPDATE_USER = gql`
|
|
|
74
75
|
mutation updateUser(
|
|
75
76
|
$id: ID!
|
|
76
77
|
$location: String
|
|
77
|
-
$links: [String]
|
|
78
|
+
$links: [String!]
|
|
78
79
|
$institution: String
|
|
79
|
-
$orcidConsent: Boolean
|
|
80
|
+
$orcidConsent: Boolean
|
|
81
|
+
$profilePrivate: Boolean
|
|
80
82
|
) {
|
|
81
83
|
updateUser(
|
|
82
84
|
id: $id
|
|
83
85
|
location: $location
|
|
84
86
|
links: $links
|
|
85
87
|
institution: $institution
|
|
86
|
-
orcidConsent: $orcidConsent
|
|
88
|
+
orcidConsent: $orcidConsent
|
|
89
|
+
profilePrivate: $profilePrivate
|
|
87
90
|
) {
|
|
88
91
|
id
|
|
89
92
|
location
|
|
90
93
|
links
|
|
91
94
|
institution
|
|
92
95
|
orcidConsent
|
|
96
|
+
profilePrivate
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
`
|
|
@@ -207,8 +211,15 @@ export const ADVANCED_SEARCH_DATASETS_QUERY = gql`
|
|
|
207
211
|
}
|
|
208
212
|
`
|
|
209
213
|
|
|
214
|
+
interface UseUserOptions {
|
|
215
|
+
errorPolicy: "none" | "ignore" | "all"
|
|
216
|
+
}
|
|
217
|
+
|
|
210
218
|
// Reusable hook to fetch user data
|
|
211
|
-
export const useUser = (
|
|
219
|
+
export const useUser = (
|
|
220
|
+
userId?: string,
|
|
221
|
+
options: UseUserOptions = { errorPolicy: "none" },
|
|
222
|
+
) => {
|
|
212
223
|
const [cookies] = useCookies()
|
|
213
224
|
const profile = getProfile(cookies)
|
|
214
225
|
const profileSub = profile?.sub
|
|
@@ -222,6 +233,7 @@ export const useUser = (userId?: string) => {
|
|
|
222
233
|
} = useQuery(GET_USER, {
|
|
223
234
|
variables: { userId: finalUserId },
|
|
224
235
|
skip: !finalUserId,
|
|
236
|
+
errorPolicy: options.errorPolicy,
|
|
225
237
|
})
|
|
226
238
|
|
|
227
239
|
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"]
|
|
@@ -20,6 +20,7 @@ export interface User {
|
|
|
20
20
|
githubSynced?: Date
|
|
21
21
|
notifications?: Event[]
|
|
22
22
|
orcidConsent?: boolean | null
|
|
23
|
+
profilePrivate?: boolean
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface UserRoutesProps {
|
|
@@ -33,6 +34,7 @@ export interface UserCardProps {
|
|
|
33
34
|
|
|
34
35
|
export interface UserAccountViewProps {
|
|
35
36
|
orcidUser: User
|
|
37
|
+
hasEdit: boolean
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/** ------------------ Dataset ------------------ */
|
|
@@ -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
|
? (
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react"
|
|
2
|
+
import * as Sentry from "@sentry/react"
|
|
3
|
+
import { useMutation } from "@apollo/client"
|
|
4
|
+
import { GET_USER, UPDATE_USER } from "../../queries/user"
|
|
5
|
+
import { RadioGroup } from "../../components/radio/RadioGroup"
|
|
6
|
+
|
|
7
|
+
export interface ProfilePrivacyProps {
|
|
8
|
+
userId: string
|
|
9
|
+
initialProfilePrivate: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ProfilePrivacy: React.FC<ProfilePrivacyProps> = ({
|
|
13
|
+
userId,
|
|
14
|
+
initialProfilePrivate,
|
|
15
|
+
}) => {
|
|
16
|
+
const initialValue = initialProfilePrivate ? "true" : "false"
|
|
17
|
+
const [value, setValue] = useState(initialValue)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setValue(initialProfilePrivate ? "true" : "false")
|
|
21
|
+
}, [initialProfilePrivate])
|
|
22
|
+
|
|
23
|
+
const [updateUser] = useMutation(UPDATE_USER, {
|
|
24
|
+
refetchQueries: [{ query: GET_USER, variables: { userId } }],
|
|
25
|
+
onError: (error) => {
|
|
26
|
+
Sentry.captureException(error)
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const handleChange = async (newValue: string) => {
|
|
31
|
+
setValue(newValue)
|
|
32
|
+
try {
|
|
33
|
+
await updateUser({
|
|
34
|
+
variables: { id: userId, profilePrivate: newValue === "true" },
|
|
35
|
+
})
|
|
36
|
+
} catch (error) {
|
|
37
|
+
Sentry.captureException(error)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const radioOptions = [
|
|
42
|
+
{ label: "Public", value: "false" },
|
|
43
|
+
{ label: "Private", value: "true" },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<p>
|
|
49
|
+
Set your profile to private to hide your profile page from public view.
|
|
50
|
+
Your datasets and contributions will still be visible based on each
|
|
51
|
+
dataset's published state.
|
|
52
|
+
</p>
|
|
53
|
+
<RadioGroup
|
|
54
|
+
name="profilePrivate"
|
|
55
|
+
layout="row"
|
|
56
|
+
radioArr={radioOptions}
|
|
57
|
+
selected={value}
|
|
58
|
+
setSelected={handleChange}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -8,11 +8,13 @@ import styles from "./scss/useraccountview.module.scss"
|
|
|
8
8
|
import { GitHubAuthButton } from "./github-auth-button"
|
|
9
9
|
import type { UserAccountViewProps } from "../types/user-types"
|
|
10
10
|
import { OrcidConsentForm } from "./components/orcid-consent-form"
|
|
11
|
+
import { ProfilePrivacy } from "./components/profile-privacy"
|
|
11
12
|
import { validateHttpHttpsUrl } from "../utils/validationUtils"
|
|
12
13
|
import { pageTitle } from "../resources/strings.js"
|
|
13
14
|
|
|
14
15
|
export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
15
16
|
orcidUser,
|
|
17
|
+
hasEdit,
|
|
16
18
|
}) => {
|
|
17
19
|
const [userLinks, setLinks] = useState<string[]>(orcidUser.links ?? [])
|
|
18
20
|
const [userLocation, setLocation] = useState<string>(
|
|
@@ -89,7 +91,7 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
89
91
|
<>
|
|
90
92
|
<Helmet>
|
|
91
93
|
<title>
|
|
92
|
-
|
|
94
|
+
{orcidUser.name || "User"} profile - {pageTitle}
|
|
93
95
|
</title>
|
|
94
96
|
</Helmet>
|
|
95
97
|
<div data-testid="user-account-view" className={styles.useraccountview}>
|
|
@@ -114,21 +116,26 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
114
116
|
{orcidUser.github}
|
|
115
117
|
</li>
|
|
116
118
|
)}
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
119
|
+
{hasEdit && (
|
|
120
|
+
<li>
|
|
121
|
+
<GitHubAuthButton sync={orcidUser.githubSynced} />
|
|
122
|
+
</li>
|
|
123
|
+
)}
|
|
120
124
|
</ul>
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
{hasEdit && (
|
|
127
|
+
<div className={styles.umbOrcidConsent}>
|
|
128
|
+
<div className={styles.umbOrcidHeading}>
|
|
129
|
+
<h4>Profile Privacy</h4>
|
|
130
|
+
</div>
|
|
131
|
+
<ProfilePrivacy
|
|
132
|
+
userId={orcidUser.id}
|
|
133
|
+
initialProfilePrivate={orcidUser.profilePrivate ?? false}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
130
137
|
|
|
131
|
-
{orcidUser.orcid !== undefined && (
|
|
138
|
+
{hasEdit && orcidUser.orcid !== undefined && (
|
|
132
139
|
<div className={styles.umbOrcidConsent}>
|
|
133
140
|
<div className={styles.umbOrcidHeading}>
|
|
134
141
|
<h4>ORCID Integration</h4>
|
|
@@ -140,15 +147,26 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({
|
|
|
140
147
|
</div>
|
|
141
148
|
)}
|
|
142
149
|
|
|
150
|
+
<EditableContent
|
|
151
|
+
editableContent={userLinks}
|
|
152
|
+
setRows={handleLinksChange}
|
|
153
|
+
heading="Links"
|
|
154
|
+
validation={validateHttpHttpsUrl}
|
|
155
|
+
validationMessage="Invalid URL format. Please start with http:// or https://"
|
|
156
|
+
data-testid="links-section"
|
|
157
|
+
hasEdit={hasEdit}
|
|
158
|
+
/>
|
|
143
159
|
<EditableContent
|
|
144
160
|
editableContent={userLocation}
|
|
145
161
|
setRows={handleLocationChange}
|
|
146
162
|
heading="Location"
|
|
163
|
+
hasEdit={hasEdit}
|
|
147
164
|
data-testid="location-section"
|
|
148
165
|
/>
|
|
149
166
|
<EditableContent
|
|
150
167
|
editableContent={userInstitution}
|
|
151
168
|
setRows={handleInstitutionChange}
|
|
169
|
+
hasEdit={hasEdit}
|
|
152
170
|
heading="Institution"
|
|
153
171
|
data-testid="institution-section"
|
|
154
172
|
/>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from "react"
|
|
2
2
|
import { useQuery } from "@apollo/client"
|
|
3
3
|
import * as Sentry from "@sentry/react"
|
|
4
|
+
import Helmet from "react-helmet"
|
|
5
|
+
import { pageTitle } from "../resources/strings"
|
|
4
6
|
import { DatasetCard } from "./dataset-card"
|
|
5
7
|
import { UserDatasetFilters } from "./components/user-dataset-filters"
|
|
6
8
|
import { ADVANCED_SEARCH_DATASETS_QUERY } from "../queries/user"
|
|
@@ -260,6 +262,11 @@ export const UserDatasetsView: React.FC<UserDatasetsViewProps> = ({
|
|
|
260
262
|
className={styles.userDatasetsWrapper}
|
|
261
263
|
data-testid="user-datasets-view"
|
|
262
264
|
>
|
|
265
|
+
<Helmet>
|
|
266
|
+
<title>
|
|
267
|
+
{orcidUser.name || "User"}'s Datasets - {pageTitle}
|
|
268
|
+
</title>
|
|
269
|
+
</Helmet>
|
|
263
270
|
<h3>{orcidUser.name}'s Datasets</h3>
|
|
264
271
|
|
|
265
272
|
<UserDatasetFilters
|
|
@@ -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
|
|
|
@@ -7,24 +7,24 @@ import { isAdmin } from "../authentication/admin-user"
|
|
|
7
7
|
import { useCookies } from "react-cookie"
|
|
8
8
|
import { getProfile } from "../authentication/profile"
|
|
9
9
|
import { useUser } from "../queries/user"
|
|
10
|
+
import FourOThreePage from "../errors/403page"
|
|
10
11
|
|
|
11
12
|
export const UserQuery: React.FC = () => {
|
|
12
13
|
const { orcid } = useParams()
|
|
13
|
-
const
|
|
14
|
-
const { user, loading, error } = useUser(orcid)
|
|
14
|
+
const { user, loading } = useUser(orcid, { errorPolicy: "all" })
|
|
15
15
|
|
|
16
16
|
const [cookies] = useCookies()
|
|
17
17
|
const profile = getProfile(cookies)
|
|
18
18
|
const isAdminUser = isAdmin()
|
|
19
19
|
|
|
20
|
-
if (!
|
|
20
|
+
if (!isValidOrcid(orcid)) {
|
|
21
21
|
return <FourOFourPage />
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
if (loading) return <div>Loading...</div>
|
|
25
25
|
|
|
26
|
-
if (
|
|
27
|
-
return <
|
|
26
|
+
if (!profile?.admin && user.profilePrivate) {
|
|
27
|
+
return <FourOThreePage />
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// is admin or profile matches id from the user data being returned
|
|
@@ -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
|