@openneuro/app 4.29.8 → 4.30.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/.scss-lint.yml +11 -11
- package/maintenance.html +26 -20
- package/package.json +3 -3
- package/src/@types/custom.d.ts +6 -0
- package/src/index.html +14 -10
- package/src/scripts/datalad/routes/dataset-redirect.tsx +2 -0
- package/src/scripts/dataset/mutations/__tests__/update-permissions.spec.jsx +2 -1
- package/src/scripts/dataset/mutations/update-permissions.tsx +1 -9
- package/src/scripts/routes.tsx +17 -0
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +152 -0
- package/src/scripts/users/__tests__/user-card.spec.tsx +110 -0
- package/src/scripts/users/__tests__/user-query.spec.tsx +65 -0
- package/src/scripts/users/__tests__/user-routes.spec.tsx +102 -0
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +84 -0
- package/src/scripts/users/components/close-button.tsx +20 -0
- package/src/scripts/users/components/edit-button.tsx +20 -0
- package/src/scripts/users/components/edit-list.tsx +103 -0
- package/src/scripts/users/components/edit-string.tsx +90 -0
- package/src/scripts/users/components/editable-content.tsx +98 -0
- package/src/scripts/users/scss/editable-content.scss +15 -0
- package/src/scripts/users/scss/user-meta-blocks.scss +14 -0
- package/src/scripts/users/scss/useraccountview.module.scss +20 -0
- package/src/scripts/users/scss/usercard.module.scss +24 -0
- package/src/scripts/users/scss/usercontainer.module.scss +38 -0
- package/src/scripts/users/scss/usertabs.module.scss +63 -0
- package/src/scripts/users/user-account-view.tsx +142 -0
- package/src/scripts/users/user-card.tsx +86 -0
- package/src/scripts/users/user-container.tsx +49 -0
- package/src/scripts/users/user-datasets-view.tsx +53 -0
- package/src/scripts/users/user-notifications-view.tsx +11 -0
- package/src/scripts/users/user-query.tsx +76 -0
- package/src/scripts/users/user-routes.tsx +52 -0
- package/src/scripts/users/user-tabs.tsx +74 -0
- package/src/scripts/utils/__tests__/markdown.spec.tsx +1 -2
- package/src/scripts/utils/validationUtils.ts +8 -0
- package/src/scripts/validation/validation.tsx +11 -8
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.usercontainer {
|
|
2
|
+
display: flex;
|
|
3
|
+
min-height: calc(100vh - 350px);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.userHeader {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
margin: 10px 0 0;
|
|
10
|
+
height: 100px;
|
|
11
|
+
.avatar {
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
margin-right: 10px;
|
|
14
|
+
width: 60px;
|
|
15
|
+
height: 60px;
|
|
16
|
+
}
|
|
17
|
+
.username {
|
|
18
|
+
}
|
|
19
|
+
h2 {
|
|
20
|
+
font-size: 35px;
|
|
21
|
+
line-height: 38px;
|
|
22
|
+
font-weight: 400;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.userSidebar {
|
|
27
|
+
width: 350px;
|
|
28
|
+
flex: 0 0 auto;
|
|
29
|
+
padding: 0;
|
|
30
|
+
width: 430px;
|
|
31
|
+
}
|
|
32
|
+
.userViews {
|
|
33
|
+
border-left: 1px solid #e0e0e0;
|
|
34
|
+
flex: 1;
|
|
35
|
+
padding: 0 0 0 50px;
|
|
36
|
+
position: relative;
|
|
37
|
+
max-width: 100%;
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
.userAccountTabLinks {
|
|
2
|
+
ul {
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
list-style-type: none;
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
&:before,
|
|
9
|
+
&:after {
|
|
10
|
+
content: '';
|
|
11
|
+
width: calc(100% - 60px);
|
|
12
|
+
height: 32px;
|
|
13
|
+
display: inline-block;
|
|
14
|
+
position: absolute;
|
|
15
|
+
left: 0;
|
|
16
|
+
top: 0;
|
|
17
|
+
transform: translateY(var(--active-offset, 0));
|
|
18
|
+
margin-left: 25px;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
transition: transform 0.3s ease; /* Always apply transition */
|
|
21
|
+
}
|
|
22
|
+
/* Apply animation when clicked */
|
|
23
|
+
// not working for some reason
|
|
24
|
+
// &.clicked:before,
|
|
25
|
+
// &.clicked:after {
|
|
26
|
+
// transform: translateY(var(--active-offset, 0));
|
|
27
|
+
// transition: transform 0.3s ease; /* Ensure transition */
|
|
28
|
+
// }
|
|
29
|
+
|
|
30
|
+
&:before {
|
|
31
|
+
background: #eee;
|
|
32
|
+
margin: 0 20px 0 35px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&:after {
|
|
36
|
+
height: 23px;
|
|
37
|
+
margin: 5px 0 0px 25px;
|
|
38
|
+
width: 5px;
|
|
39
|
+
background: #204e5b;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
li {
|
|
43
|
+
margin: 10px 25px;
|
|
44
|
+
position: relative;
|
|
45
|
+
|
|
46
|
+
a {
|
|
47
|
+
padding: 5px 10px 5px 20px;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
display: block;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
background-color: transparent;
|
|
52
|
+
transition: background-color 0.3s;
|
|
53
|
+
&.active {
|
|
54
|
+
font-weight: bold;
|
|
55
|
+
&:hover {
|
|
56
|
+
color: #204e5b;
|
|
57
|
+
cursor: default;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { useMutation } from "@apollo/client"
|
|
3
|
+
import { EditableContent } from "./components/editable-content"
|
|
4
|
+
import styles from "./scss/useraccountview.module.scss"
|
|
5
|
+
import { GET_USER_BY_ORCID, UPDATE_USER } from "./user-query"
|
|
6
|
+
|
|
7
|
+
interface UserAccountViewProps {
|
|
8
|
+
user: {
|
|
9
|
+
name: string
|
|
10
|
+
email: string
|
|
11
|
+
orcid: string
|
|
12
|
+
links: string[]
|
|
13
|
+
location: string
|
|
14
|
+
institution: string
|
|
15
|
+
github?: string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const UserAccountView: React.FC<UserAccountViewProps> = ({ user }) => {
|
|
20
|
+
const [userLinks, setLinks] = useState<string[]>(user.links || [])
|
|
21
|
+
const [userLocation, setLocation] = useState<string>(user.location || "")
|
|
22
|
+
const [userInstitution, setInstitution] = useState<string>(
|
|
23
|
+
user.institution || "",
|
|
24
|
+
)
|
|
25
|
+
const [updateUser] = useMutation(UPDATE_USER)
|
|
26
|
+
|
|
27
|
+
const handleLinksChange = async (newLinks: string[]) => {
|
|
28
|
+
setLinks(newLinks)
|
|
29
|
+
try {
|
|
30
|
+
await updateUser({
|
|
31
|
+
variables: {
|
|
32
|
+
id: user.orcid,
|
|
33
|
+
links: newLinks,
|
|
34
|
+
},
|
|
35
|
+
refetchQueries: [
|
|
36
|
+
{
|
|
37
|
+
query: GET_USER_BY_ORCID,
|
|
38
|
+
variables: { id: user.orcid },
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
} catch {
|
|
43
|
+
// Error handling can be implemented here if needed
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleLocationChange = async (newLocation: string) => {
|
|
48
|
+
setLocation(newLocation)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await updateUser({
|
|
52
|
+
variables: {
|
|
53
|
+
id: user.orcid,
|
|
54
|
+
location: newLocation,
|
|
55
|
+
},
|
|
56
|
+
refetchQueries: [
|
|
57
|
+
{
|
|
58
|
+
query: GET_USER_BY_ORCID,
|
|
59
|
+
variables: { id: user.orcid },
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
})
|
|
63
|
+
} catch {
|
|
64
|
+
// Error handling can be implemented here if needed
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handleInstitutionChange = async (newInstitution: string) => {
|
|
69
|
+
setInstitution(newInstitution)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await updateUser({
|
|
73
|
+
variables: {
|
|
74
|
+
id: user.orcid,
|
|
75
|
+
institution: newInstitution,
|
|
76
|
+
},
|
|
77
|
+
refetchQueries: [
|
|
78
|
+
{
|
|
79
|
+
query: GET_USER_BY_ORCID,
|
|
80
|
+
variables: { id: user.orcid },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
} catch {
|
|
85
|
+
// Error handling can be implemented here if needed
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div data-testid="user-account-view" className={styles.useraccountview}>
|
|
91
|
+
<h3>Account</h3>
|
|
92
|
+
<ul className={styles.accountDetail}>
|
|
93
|
+
<li>
|
|
94
|
+
<span>Name:</span>
|
|
95
|
+
{user.name}
|
|
96
|
+
</li>
|
|
97
|
+
<li>
|
|
98
|
+
<span>Email:</span>
|
|
99
|
+
{user.email}
|
|
100
|
+
</li>
|
|
101
|
+
<li>
|
|
102
|
+
<span>ORCID:</span>
|
|
103
|
+
{user.orcid}
|
|
104
|
+
</li>
|
|
105
|
+
{user.github
|
|
106
|
+
? (
|
|
107
|
+
<li>
|
|
108
|
+
<span>GitHub:</span>
|
|
109
|
+
{user.github}
|
|
110
|
+
</li>
|
|
111
|
+
)
|
|
112
|
+
: <li>Connect your GitHub</li>}
|
|
113
|
+
</ul>
|
|
114
|
+
|
|
115
|
+
<EditableContent
|
|
116
|
+
editableContent={userLinks}
|
|
117
|
+
setRows={handleLinksChange}
|
|
118
|
+
className="custom-class"
|
|
119
|
+
heading="Links"
|
|
120
|
+
// eslint-disable-next-line no-useless-escape
|
|
121
|
+
validation={/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/} // URL validation regex
|
|
122
|
+
validationMessage="Invalid URL format. Please use a valid link."
|
|
123
|
+
data-testid="links-section"
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<EditableContent
|
|
127
|
+
editableContent={userLocation}
|
|
128
|
+
setRows={handleLocationChange}
|
|
129
|
+
className="custom-class"
|
|
130
|
+
heading="Location"
|
|
131
|
+
data-testid="location-section"
|
|
132
|
+
/>
|
|
133
|
+
<EditableContent
|
|
134
|
+
editableContent={userInstitution}
|
|
135
|
+
setRows={handleInstitutionChange}
|
|
136
|
+
className="custom-class"
|
|
137
|
+
heading="Institution"
|
|
138
|
+
data-testid="institution-section"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import styles from "./scss/usercard.module.scss"
|
|
3
|
+
|
|
4
|
+
export interface User {
|
|
5
|
+
name: string
|
|
6
|
+
location?: string
|
|
7
|
+
email: string
|
|
8
|
+
orcid: string
|
|
9
|
+
institution?: string
|
|
10
|
+
links?: string[]
|
|
11
|
+
github?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UserCardProps {
|
|
15
|
+
user: User
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const UserCard: React.FC<UserCardProps> = ({ user }) => {
|
|
19
|
+
const { location, institution, email, orcid, links = [], github, name } = user
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={styles.userCard}>
|
|
23
|
+
<ul>
|
|
24
|
+
{institution && (
|
|
25
|
+
<li>
|
|
26
|
+
<i className="fa fa-building"></i>
|
|
27
|
+
{institution}
|
|
28
|
+
</li>
|
|
29
|
+
)}
|
|
30
|
+
{location && (
|
|
31
|
+
<li>
|
|
32
|
+
<i className="fas fa-map-marker-alt"></i>
|
|
33
|
+
{location}
|
|
34
|
+
</li>
|
|
35
|
+
)}
|
|
36
|
+
<li>
|
|
37
|
+
<i className="fas fa-envelope"></i>
|
|
38
|
+
<a
|
|
39
|
+
href={"mailto:" + email}
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
>
|
|
43
|
+
{email}
|
|
44
|
+
</a>
|
|
45
|
+
</li>
|
|
46
|
+
{orcid && (
|
|
47
|
+
<li className={styles.orcid}>
|
|
48
|
+
<i className="fab fa-orcid" aria-hidden="true"></i>
|
|
49
|
+
<a
|
|
50
|
+
href={`https://orcid.org/${orcid}`}
|
|
51
|
+
target="_blank"
|
|
52
|
+
rel="noopener noreferrer"
|
|
53
|
+
aria-label={`ORCID profile of ${name}`}
|
|
54
|
+
>
|
|
55
|
+
{orcid}
|
|
56
|
+
</a>
|
|
57
|
+
</li>
|
|
58
|
+
)}
|
|
59
|
+
{github && (
|
|
60
|
+
<li>
|
|
61
|
+
<i className="fab fa-github"></i>
|
|
62
|
+
<a
|
|
63
|
+
href={`https://github.com/${github}`}
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
aria-label={`Github profile of ${name}`}
|
|
67
|
+
>
|
|
68
|
+
{github}
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
)}
|
|
72
|
+
{links.length > 0 &&
|
|
73
|
+
links
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((link, index) => (
|
|
76
|
+
<li key={index}>
|
|
77
|
+
<i className="fa fa-link"></i>
|
|
78
|
+
<a href={link} target="_blank" rel="noopener noreferrer">
|
|
79
|
+
{link}
|
|
80
|
+
</a>
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { Outlet } from "react-router-dom"
|
|
3
|
+
import { UserCard } from "./user-card"
|
|
4
|
+
import { UserAccountTabs } from "./user-tabs"
|
|
5
|
+
import styles from "./scss/usercontainer.module.scss"
|
|
6
|
+
|
|
7
|
+
interface User {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
location: string
|
|
11
|
+
github?: string
|
|
12
|
+
institution: string
|
|
13
|
+
email: string
|
|
14
|
+
avatar: string
|
|
15
|
+
orcid: string
|
|
16
|
+
links: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AccountContainerProps {
|
|
20
|
+
user: User
|
|
21
|
+
hasEdit: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const UserAccountContainer: React.FC<AccountContainerProps> = ({
|
|
25
|
+
user,
|
|
26
|
+
hasEdit,
|
|
27
|
+
}) => {
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<div className="container">
|
|
31
|
+
<header className={styles.userHeader}>
|
|
32
|
+
{user.avatar && (
|
|
33
|
+
<img className={styles.avatar} src={user.avatar} alt={user.name} />
|
|
34
|
+
)}
|
|
35
|
+
<h2 className={styles.username}>{user.name}</h2>
|
|
36
|
+
</header>
|
|
37
|
+
</div>
|
|
38
|
+
<div className={styles.usercontainer + " container"}>
|
|
39
|
+
<section className={styles.userSidebar}>
|
|
40
|
+
<UserCard user={user} />
|
|
41
|
+
<UserAccountTabs hasEdit={hasEdit} />
|
|
42
|
+
</section>
|
|
43
|
+
<section className={styles.userViews}>
|
|
44
|
+
<Outlet />
|
|
45
|
+
</section>
|
|
46
|
+
</div>
|
|
47
|
+
</>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
interface User {
|
|
4
|
+
name: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Dataset {
|
|
8
|
+
id: string
|
|
9
|
+
created: string
|
|
10
|
+
ownerId: string
|
|
11
|
+
name: string
|
|
12
|
+
type: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UserDatasetsViewProps {
|
|
16
|
+
user: User
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const dummyDatasets: Dataset[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "ds00001",
|
|
22
|
+
created: "2023-11-01T12:00:00Z",
|
|
23
|
+
ownerId: "1",
|
|
24
|
+
name: "Dataset 1",
|
|
25
|
+
type: "public",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "ds00002",
|
|
29
|
+
created: "2023-11-02T12:00:00Z",
|
|
30
|
+
ownerId: "2",
|
|
31
|
+
name: "Dataset 2",
|
|
32
|
+
type: "private",
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export const UserDatasetsView: React.FC<UserDatasetsViewProps> = ({ user }) => {
|
|
37
|
+
return (
|
|
38
|
+
<div data-testid="user-datasets-view">
|
|
39
|
+
<h1>{user.name}'s Datasets</h1>
|
|
40
|
+
<div>
|
|
41
|
+
{dummyDatasets.map((dataset) => (
|
|
42
|
+
<div key={dataset.id} data-testid={`dataset-${dataset.id}`}>
|
|
43
|
+
<h2>{dataset.name}</h2>
|
|
44
|
+
<p>Type: {dataset.type}</p>
|
|
45
|
+
<p>Created: {dataset.created}</p>
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default UserDatasetsView
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export const UserNotificationsView = ({ user }) => {
|
|
4
|
+
// this is a placeholder for the user notification feature
|
|
5
|
+
return (
|
|
6
|
+
<div data-testid="user-notifications-view">
|
|
7
|
+
<h3>UserNotificationsPAge for {user.name}</h3>
|
|
8
|
+
<p>This should show user info</p>
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { useParams } from "react-router-dom"
|
|
3
|
+
import { UserRoutes } from "./user-routes"
|
|
4
|
+
import FourOFourPage from "../errors/404page"
|
|
5
|
+
import { isValidOrcid } from "../utils/validationUtils"
|
|
6
|
+
import { gql, useQuery } from "@apollo/client"
|
|
7
|
+
import { isAdmin } from "../authentication/admin-user"
|
|
8
|
+
import { useCookies } from "react-cookie"
|
|
9
|
+
import { getProfile } from "../authentication/profile"
|
|
10
|
+
|
|
11
|
+
// GraphQL query to fetch user by ORCID
|
|
12
|
+
export const GET_USER_BY_ORCID = gql`
|
|
13
|
+
query User($userId: ID!) {
|
|
14
|
+
user(id: $userId) {
|
|
15
|
+
id
|
|
16
|
+
name
|
|
17
|
+
orcid
|
|
18
|
+
email
|
|
19
|
+
avatar
|
|
20
|
+
location
|
|
21
|
+
institution
|
|
22
|
+
links
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
export const UPDATE_USER = gql`
|
|
28
|
+
mutation updateUser($id: ID!, $location: String, $links: [String], $institution: String) {
|
|
29
|
+
updateUser(id: $id, location: $location, links: $links, institution: $institution) {
|
|
30
|
+
id
|
|
31
|
+
location
|
|
32
|
+
links
|
|
33
|
+
institution
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`
|
|
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
|
+
export const UserQuery: React.FC = () => {
|
|
51
|
+
const { orcid } = useParams()
|
|
52
|
+
const isOrcidValid = orcid && isValidOrcid(orcid)
|
|
53
|
+
const { data, loading, error } = useQuery(GET_USER_BY_ORCID, {
|
|
54
|
+
variables: { userId: orcid },
|
|
55
|
+
skip: !isOrcidValid,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const [cookies] = useCookies()
|
|
59
|
+
const profile = getProfile(cookies)
|
|
60
|
+
|
|
61
|
+
if (!isOrcidValid) {
|
|
62
|
+
return <FourOFourPage />
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (loading) return <div>Loading...</div>
|
|
66
|
+
|
|
67
|
+
if (error || !data?.user || data.user.orcid !== orcid) {
|
|
68
|
+
return <FourOFourPage />
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// is admin or profile matches id from the user data being returned
|
|
72
|
+
const hasEdit = isAdmin || data.user.id !== profile.sub ? true : false
|
|
73
|
+
|
|
74
|
+
// Render user data with UserRoutes
|
|
75
|
+
return <UserRoutes user={data.user} hasEdit={hasEdit} />
|
|
76
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { Route, Routes } from "react-router-dom"
|
|
3
|
+
import { UserAccountContainer } from "./user-container"
|
|
4
|
+
import { UserAccountView } from "./user-account-view"
|
|
5
|
+
import { UserNotificationsView } from "./user-notifications-view"
|
|
6
|
+
import { UserDatasetsView } from "./user-datasets-view"
|
|
7
|
+
import FourOFourPage from "../errors/404page"
|
|
8
|
+
import FourOThreePage from "../errors/403page"
|
|
9
|
+
|
|
10
|
+
export interface User {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
location: string
|
|
14
|
+
github?: string
|
|
15
|
+
institution: string
|
|
16
|
+
email: string
|
|
17
|
+
avatar: string
|
|
18
|
+
orcid: string
|
|
19
|
+
links: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface UserRoutesProps {
|
|
23
|
+
user: User
|
|
24
|
+
hasEdit: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const UserRoutes: React.FC<UserRoutesProps> = ({ user, hasEdit }) => {
|
|
28
|
+
return (
|
|
29
|
+
<Routes>
|
|
30
|
+
<Route path="/*" element={<FourOFourPage />} />
|
|
31
|
+
<Route
|
|
32
|
+
path="*"
|
|
33
|
+
element={<UserAccountContainer user={user} hasEdit={hasEdit} />}
|
|
34
|
+
>
|
|
35
|
+
<Route path="" element={<UserDatasetsView user={user} />} />
|
|
36
|
+
<Route
|
|
37
|
+
path="account"
|
|
38
|
+
element={hasEdit
|
|
39
|
+
? <UserAccountView user={user} />
|
|
40
|
+
: <FourOThreePage />}
|
|
41
|
+
/>
|
|
42
|
+
<Route
|
|
43
|
+
path="notifications"
|
|
44
|
+
element={hasEdit
|
|
45
|
+
? <UserNotificationsView user={user} />
|
|
46
|
+
: <FourOThreePage />}
|
|
47
|
+
/>
|
|
48
|
+
<Route path="*" element={<FourOFourPage />} />
|
|
49
|
+
</Route>
|
|
50
|
+
</Routes>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react"
|
|
2
|
+
import { NavLink, useLocation } from "react-router-dom"
|
|
3
|
+
import styles from "./scss/usertabs.module.scss"
|
|
4
|
+
|
|
5
|
+
export interface UserAccountTabsProps {
|
|
6
|
+
hasEdit: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const UserAccountTabs: React.FC<UserAccountTabsProps> = (
|
|
10
|
+
{ hasEdit },
|
|
11
|
+
) => {
|
|
12
|
+
const ulRef = useRef<HTMLUListElement>(null)
|
|
13
|
+
const [activePosition, setActivePosition] = useState<number>(0)
|
|
14
|
+
const [clicked, setClicked] = useState(false)
|
|
15
|
+
const location = useLocation()
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const activeLink = ulRef.current?.querySelector(
|
|
19
|
+
`.${styles.active}`,
|
|
20
|
+
) as HTMLElement
|
|
21
|
+
if (activeLink) {
|
|
22
|
+
const li = activeLink.parentElement as HTMLElement
|
|
23
|
+
setActivePosition(li.offsetTop)
|
|
24
|
+
}
|
|
25
|
+
}, [location])
|
|
26
|
+
|
|
27
|
+
const handleClick = () => {
|
|
28
|
+
setClicked(true)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!hasEdit) return null
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={styles.userAccountTabLinks}>
|
|
35
|
+
<ul
|
|
36
|
+
ref={ulRef}
|
|
37
|
+
className={`${clicked ? "clicked" : ""} ${styles.userAccountTabLinks}`}
|
|
38
|
+
style={{
|
|
39
|
+
"--active-offset": `${activePosition}px`,
|
|
40
|
+
} as React.CSSProperties}
|
|
41
|
+
>
|
|
42
|
+
<li>
|
|
43
|
+
<NavLink
|
|
44
|
+
to=""
|
|
45
|
+
end
|
|
46
|
+
className={({ isActive }) => (isActive ? styles.active : "")}
|
|
47
|
+
onClick={handleClick}
|
|
48
|
+
>
|
|
49
|
+
User Datasets
|
|
50
|
+
</NavLink>
|
|
51
|
+
</li>
|
|
52
|
+
<li>
|
|
53
|
+
<NavLink
|
|
54
|
+
data-testid="user-notifications-tab"
|
|
55
|
+
to="notifications"
|
|
56
|
+
className={({ isActive }) => (isActive ? styles.active : "")}
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
>
|
|
59
|
+
User Notifications
|
|
60
|
+
</NavLink>
|
|
61
|
+
</li>
|
|
62
|
+
<li>
|
|
63
|
+
<NavLink
|
|
64
|
+
to="account"
|
|
65
|
+
className={({ isActive }) => (isActive ? styles.active : "")}
|
|
66
|
+
onClick={handleClick}
|
|
67
|
+
>
|
|
68
|
+
Account Info
|
|
69
|
+
</NavLink>
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -27,8 +27,7 @@ describe("Test <Markdown> component", () => {
|
|
|
27
27
|
expect(asFragment()).toMatchSnapshot()
|
|
28
28
|
})
|
|
29
29
|
it("filters close-break tags", () => {
|
|
30
|
-
const hrefExample =
|
|
31
|
-
'<br>sample text</br>'
|
|
30
|
+
const hrefExample = "<br>sample text</br>"
|
|
32
31
|
const { asFragment } = render(<Markdown>{hrefExample}</Markdown>)
|
|
33
32
|
expect(asFragment()).toMatchSnapshot()
|
|
34
33
|
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates an ORCID.
|
|
3
|
+
* @param orcid - The ORCID string to validate.
|
|
4
|
+
* @returns `true` if the ORCID is valid, `false` otherwise.
|
|
5
|
+
*/
|
|
6
|
+
export function isValidOrcid(orcid: string): boolean {
|
|
7
|
+
return /^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$/.test(orcid || "")
|
|
8
|
+
}
|