@openneuro/app 4.29.9 → 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 +2 -4
- 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 +1 -1
- package/src/scripts/dataset/mutations/update-permissions.tsx +1 -1
- package/src/scripts/routes.tsx +16 -3
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +146 -63
- package/src/scripts/users/__tests__/user-card.spec.tsx +62 -47
- package/src/scripts/users/__tests__/user-query.spec.tsx +65 -60
- package/src/scripts/users/__tests__/user-routes.spec.tsx +71 -40
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +63 -66
- package/src/scripts/users/components/edit-list.tsx +53 -29
- package/src/scripts/users/components/edit-string.tsx +63 -22
- package/src/scripts/users/components/editable-content.tsx +85 -50
- package/src/scripts/users/user-account-view.tsx +101 -21
- package/src/scripts/users/user-card.tsx +26 -24
- package/src/scripts/users/user-container.tsx +39 -38
- package/src/scripts/users/user-datasets-view.tsx +22 -22
- package/src/scripts/users/user-notifications-view.tsx +9 -10
- package/src/scripts/users/user-query.tsx +62 -66
- package/src/scripts/users/user-routes.tsx +31 -24
- package/src/scripts/users/user-tabs.tsx +25 -21
- package/src/scripts/utils/__tests__/markdown.spec.tsx +1 -2
- package/src/scripts/utils/validationUtils.ts +2 -3
- package/src/scripts/validation/validation.tsx +11 -8
|
@@ -1,41 +1,58 @@
|
|
|
1
|
-
import React, { useState } from
|
|
2
|
-
import { Button } from
|
|
3
|
-
import
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { Button } from "@openneuro/components/button"
|
|
3
|
+
import "../scss/user-meta-blocks.scss"
|
|
4
4
|
|
|
5
5
|
interface EditListProps {
|
|
6
|
-
placeholder?: string
|
|
7
|
-
elements?: string[]
|
|
8
|
-
setElements: (elements: string[]) => void
|
|
6
|
+
placeholder?: string
|
|
7
|
+
elements?: string[]
|
|
8
|
+
setElements: (elements: string[]) => void
|
|
9
|
+
validation?: RegExp
|
|
10
|
+
validationMessage?: string
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* EditList Component
|
|
13
15
|
* Allows adding and removing strings from a list.
|
|
14
16
|
*/
|
|
15
|
-
export const EditList: React.FC<EditListProps> = (
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
export const EditList: React.FC<EditListProps> = (
|
|
18
|
+
{
|
|
19
|
+
placeholder = "Enter item",
|
|
20
|
+
elements = [],
|
|
21
|
+
setElements,
|
|
22
|
+
validation,
|
|
23
|
+
validationMessage,
|
|
24
|
+
},
|
|
25
|
+
) => {
|
|
26
|
+
const [newElement, setNewElement] = useState<string>("")
|
|
27
|
+
const [warnEmpty, setWarnEmpty] = useState<boolean>(false)
|
|
28
|
+
const [warnValidation, setWarnValidation] = useState<string | null>(null)
|
|
18
29
|
|
|
19
|
-
/**
|
|
20
|
-
* Remove an element from the list by index
|
|
21
|
-
* @param index - The index of the element to remove
|
|
22
|
-
*/
|
|
23
30
|
const removeElement = (index: number): void => {
|
|
24
|
-
setElements(elements.filter((_, i) => i !== index))
|
|
25
|
-
}
|
|
31
|
+
setElements(elements.filter((_, i) => i !== index))
|
|
32
|
+
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
* Add a new element to the list
|
|
29
|
-
*/
|
|
34
|
+
// Add a new element to the list
|
|
30
35
|
const addElement = (): void => {
|
|
31
36
|
if (!newElement.trim()) {
|
|
32
|
-
setWarnEmpty(true)
|
|
37
|
+
setWarnEmpty(true)
|
|
38
|
+
setWarnValidation(null)
|
|
39
|
+
} else if (validation && !validation.test(newElement.trim())) {
|
|
40
|
+
setWarnValidation(validationMessage || "Invalid input format")
|
|
41
|
+
setWarnEmpty(false)
|
|
33
42
|
} else {
|
|
34
|
-
setElements([...elements, newElement.trim()])
|
|
35
|
-
setWarnEmpty(false)
|
|
36
|
-
|
|
43
|
+
setElements([...elements, newElement.trim()])
|
|
44
|
+
setWarnEmpty(false)
|
|
45
|
+
setWarnValidation(null)
|
|
46
|
+
setNewElement("")
|
|
37
47
|
}
|
|
38
|
-
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle Enter/Return key press to add element
|
|
51
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
52
|
+
if (e.key === "Enter") {
|
|
53
|
+
addElement()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
39
56
|
|
|
40
57
|
return (
|
|
41
58
|
<div className="edit-list-container">
|
|
@@ -46,6 +63,7 @@ export const EditList: React.FC<EditListProps> = ({ placeholder = 'Enter item',
|
|
|
46
63
|
placeholder={placeholder}
|
|
47
64
|
value={newElement}
|
|
48
65
|
onChange={(e) => setNewElement(e.target.value)}
|
|
66
|
+
onKeyDown={handleKeyDown}
|
|
49
67
|
/>
|
|
50
68
|
<Button
|
|
51
69
|
className="edit-list-add"
|
|
@@ -55,7 +73,14 @@ export const EditList: React.FC<EditListProps> = ({ placeholder = 'Enter item',
|
|
|
55
73
|
onClick={addElement}
|
|
56
74
|
/>
|
|
57
75
|
</div>
|
|
58
|
-
{warnEmpty &&
|
|
76
|
+
{warnEmpty && (
|
|
77
|
+
<small className="warning-text">
|
|
78
|
+
Your input was empty
|
|
79
|
+
</small>
|
|
80
|
+
)}
|
|
81
|
+
{warnValidation && (
|
|
82
|
+
<small className="warning-text">{warnValidation}</small>
|
|
83
|
+
)}
|
|
59
84
|
<div className="edit-list-items">
|
|
60
85
|
{elements.map((element, index) => (
|
|
61
86
|
<div key={index} className="edit-list-group-item">
|
|
@@ -67,13 +92,12 @@ export const EditList: React.FC<EditListProps> = ({ placeholder = 'Enter item',
|
|
|
67
92
|
icon="fa fa-times"
|
|
68
93
|
label="Remove"
|
|
69
94
|
color="red"
|
|
70
|
-
onClick={() =>
|
|
95
|
+
onClick={() =>
|
|
96
|
+
removeElement(index)}
|
|
71
97
|
/>
|
|
72
98
|
</div>
|
|
73
99
|
))}
|
|
74
100
|
</div>
|
|
75
101
|
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -1,29 +1,62 @@
|
|
|
1
|
-
import React, { useState } from
|
|
2
|
-
import { Button } from
|
|
3
|
-
import
|
|
1
|
+
import React, { useEffect, useState } from "react"
|
|
2
|
+
import { Button } from "@openneuro/components/button"
|
|
3
|
+
import "../scss/user-meta-blocks.scss"
|
|
4
4
|
|
|
5
5
|
interface EditStringProps {
|
|
6
|
-
value?: string
|
|
7
|
-
setValue: (value: string) => void
|
|
8
|
-
placeholder?: string
|
|
6
|
+
value?: string
|
|
7
|
+
setValue: (value: string) => void
|
|
8
|
+
placeholder?: string
|
|
9
|
+
closeEditing: () => void
|
|
10
|
+
validation?: RegExp
|
|
11
|
+
validationMessage?: string
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export const EditString: React.FC<EditStringProps> = (
|
|
15
|
+
{
|
|
16
|
+
value = "",
|
|
17
|
+
setValue,
|
|
18
|
+
placeholder = "Enter text",
|
|
19
|
+
closeEditing,
|
|
20
|
+
validation,
|
|
21
|
+
validationMessage,
|
|
22
|
+
},
|
|
23
|
+
) => {
|
|
24
|
+
const [currentValue, setCurrentValue] = useState<string>(value)
|
|
25
|
+
const [warnEmpty, setWarnEmpty] = useState<string | null>(null)
|
|
26
|
+
const [warnValidation, setWarnValidation] = useState<string | null>(null)
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
if
|
|
21
|
-
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Show warning only if there was an initial value and it was deleted
|
|
30
|
+
if (value !== "" && currentValue === "") {
|
|
31
|
+
setWarnEmpty(
|
|
32
|
+
"Your input is empty. This will delete the previously saved value..",
|
|
33
|
+
)
|
|
22
34
|
} else {
|
|
23
|
-
setWarnEmpty(
|
|
24
|
-
|
|
35
|
+
setWarnEmpty(null)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validation logic
|
|
39
|
+
if (validation && currentValue && !validation.test(currentValue)) {
|
|
40
|
+
setWarnValidation(validationMessage || "Invalid input")
|
|
41
|
+
} else {
|
|
42
|
+
setWarnValidation(null)
|
|
43
|
+
}
|
|
44
|
+
}, [currentValue, value, validation, validationMessage])
|
|
45
|
+
|
|
46
|
+
const handleSave = (): void => {
|
|
47
|
+
if (!warnValidation) {
|
|
48
|
+
setValue(currentValue.trim())
|
|
49
|
+
closeEditing()
|
|
25
50
|
}
|
|
26
|
-
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle Enter key press for saving
|
|
54
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
55
|
+
if (event.key === "Enter") {
|
|
56
|
+
event.preventDefault()
|
|
57
|
+
handleSave()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
27
60
|
|
|
28
61
|
return (
|
|
29
62
|
<div className="edit-string-container">
|
|
@@ -34,6 +67,7 @@ export const EditString: React.FC<EditStringProps> = ({ value = '', setValue, pl
|
|
|
34
67
|
placeholder={placeholder}
|
|
35
68
|
value={currentValue}
|
|
36
69
|
onChange={(e) => setCurrentValue(e.target.value)}
|
|
70
|
+
onKeyDown={handleKeyDown}
|
|
37
71
|
/>
|
|
38
72
|
<Button
|
|
39
73
|
className="edit-string-save"
|
|
@@ -43,7 +77,14 @@ export const EditString: React.FC<EditStringProps> = ({ value = '', setValue, pl
|
|
|
43
77
|
onClick={handleSave}
|
|
44
78
|
/>
|
|
45
79
|
</div>
|
|
46
|
-
{
|
|
80
|
+
{/* Show empty value warning only if content was deleted */}
|
|
81
|
+
{warnEmpty && currentValue === "" && (
|
|
82
|
+
<small className="warning-text">{warnEmpty}</small>
|
|
83
|
+
)}
|
|
84
|
+
{/* Show validation error */}
|
|
85
|
+
{warnValidation && (
|
|
86
|
+
<small className="warning-text">{warnValidation}</small>
|
|
87
|
+
)}
|
|
47
88
|
</div>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import React, { useState } from "react"
|
|
2
|
-
import { EditList } from "./edit-list"
|
|
3
|
-
import { EditString } from "./edit-string"
|
|
4
|
-
import { EditButton } from "./edit-button"
|
|
5
|
-
import { CloseButton } from "./close-button"
|
|
6
|
-
import { Markdown } from "../../utils/markdown"
|
|
7
|
-
import
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { EditList } from "./edit-list"
|
|
3
|
+
import { EditString } from "./edit-string"
|
|
4
|
+
import { EditButton } from "./edit-button"
|
|
5
|
+
import { CloseButton } from "./close-button"
|
|
6
|
+
import { Markdown } from "../../utils/markdown"
|
|
7
|
+
import "../scss/editable-content.scss"
|
|
8
8
|
|
|
9
9
|
interface EditableContentProps {
|
|
10
|
-
editableContent: string[] | string
|
|
11
|
-
setRows: React.Dispatch<React.SetStateAction<string[] | string
|
|
12
|
-
className: string
|
|
13
|
-
heading: string
|
|
10
|
+
editableContent: string[] | string
|
|
11
|
+
setRows: React.Dispatch<React.SetStateAction<string[] | string>>
|
|
12
|
+
className: string
|
|
13
|
+
heading: string
|
|
14
|
+
validation?: RegExp
|
|
15
|
+
validationMessage?: string
|
|
16
|
+
"data-testid"?: string
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export const EditableContent: React.FC<EditableContentProps> = ({
|
|
@@ -18,46 +21,78 @@ export const EditableContent: React.FC<EditableContentProps> = ({
|
|
|
18
21
|
setRows,
|
|
19
22
|
className,
|
|
20
23
|
heading,
|
|
24
|
+
validation,
|
|
25
|
+
validationMessage,
|
|
26
|
+
"data-testid": testId,
|
|
21
27
|
}) => {
|
|
22
|
-
const [editing, setEditing] = useState(false)
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div className={`user-meta-block ${className}`}>
|
|
26
|
-
<span className="umb-heading"><h4>{heading}</h4>{editing ? <CloseButton action={() => setEditing(false)} /> : <EditButton action={() => setEditing(true)} />}</span>
|
|
27
|
-
{editing ? (
|
|
28
|
-
<>
|
|
29
|
-
{Array.isArray(editableContent) ? (
|
|
30
|
-
<EditList
|
|
31
|
-
placeholder="Add new item"
|
|
32
|
-
elements={editableContent}
|
|
33
|
-
setElements={setRows as React.Dispatch<React.SetStateAction<string[]>>}
|
|
34
|
-
/>
|
|
35
|
-
) : (
|
|
36
|
-
<EditString
|
|
37
|
-
value={editableContent}
|
|
38
|
-
setValue={setRows as React.Dispatch<React.SetStateAction<string>>}
|
|
39
|
-
placeholder="Edit content"
|
|
40
|
-
/>
|
|
41
|
-
)}
|
|
42
|
-
</>
|
|
43
|
-
) : (
|
|
44
|
-
<>
|
|
45
|
-
{Array.isArray(editableContent) ? (
|
|
46
|
-
<ul>
|
|
47
|
-
{editableContent.map((item, index) => (
|
|
48
|
-
<li key={index}>
|
|
49
|
-
<Markdown>{item}</Markdown>
|
|
50
|
-
</li>
|
|
51
|
-
))}
|
|
52
|
-
</ul>
|
|
53
|
-
) : (
|
|
54
|
-
<Markdown>{editableContent}</Markdown>
|
|
55
|
-
)}
|
|
56
|
-
</>
|
|
57
|
-
)}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
};
|
|
28
|
+
const [editing, setEditing] = useState(false)
|
|
61
29
|
|
|
30
|
+
const closeEditing = () => {
|
|
31
|
+
setEditing(false)
|
|
32
|
+
}
|
|
62
33
|
|
|
34
|
+
// Function to handle validation of user input
|
|
35
|
+
const handleValidation = (value: string): boolean => {
|
|
36
|
+
if (validation && !validation.test(value)) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
63
41
|
|
|
42
|
+
return (
|
|
43
|
+
<div className={`user-meta-block ${className}`} data-testid={testId}>
|
|
44
|
+
<span className="umb-heading">
|
|
45
|
+
<h4>{heading}</h4>
|
|
46
|
+
{editing
|
|
47
|
+
? <CloseButton action={closeEditing} />
|
|
48
|
+
: <EditButton action={() => setEditing(true)} />}
|
|
49
|
+
</span>
|
|
50
|
+
{editing
|
|
51
|
+
? (
|
|
52
|
+
<>
|
|
53
|
+
{Array.isArray(editableContent)
|
|
54
|
+
? (
|
|
55
|
+
<EditList
|
|
56
|
+
placeholder="Add new item"
|
|
57
|
+
elements={editableContent}
|
|
58
|
+
setElements={setRows as React.Dispatch<
|
|
59
|
+
React.SetStateAction<string[]>
|
|
60
|
+
>}
|
|
61
|
+
validation={validation}
|
|
62
|
+
validationMessage={validationMessage}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
: (
|
|
66
|
+
<EditString
|
|
67
|
+
value={editableContent}
|
|
68
|
+
setValue={(newValue: string) => {
|
|
69
|
+
if (handleValidation(newValue)) {
|
|
70
|
+
setRows(newValue)
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
73
|
+
placeholder="Edit content"
|
|
74
|
+
closeEditing={closeEditing}
|
|
75
|
+
validation={validation}
|
|
76
|
+
validationMessage={validationMessage}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</>
|
|
80
|
+
)
|
|
81
|
+
: (
|
|
82
|
+
<>
|
|
83
|
+
{Array.isArray(editableContent)
|
|
84
|
+
? (
|
|
85
|
+
<ul>
|
|
86
|
+
{editableContent.map((item, index) => (
|
|
87
|
+
<li key={index}>
|
|
88
|
+
<Markdown>{item}</Markdown>
|
|
89
|
+
</li>
|
|
90
|
+
))}
|
|
91
|
+
</ul>
|
|
92
|
+
)
|
|
93
|
+
: <Markdown>{editableContent}</Markdown>}
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -1,23 +1,90 @@
|
|
|
1
|
-
import React, { useState } from "react"
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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"
|
|
4
6
|
|
|
5
7
|
interface UserAccountViewProps {
|
|
6
8
|
user: {
|
|
7
|
-
name: string
|
|
8
|
-
email: string
|
|
9
|
-
orcid: string
|
|
10
|
-
links: string[]
|
|
11
|
-
location: string
|
|
12
|
-
institution: string
|
|
13
|
-
github?: string
|
|
14
|
-
}
|
|
9
|
+
name: string
|
|
10
|
+
email: string
|
|
11
|
+
orcid: string
|
|
12
|
+
links: string[]
|
|
13
|
+
location: string
|
|
14
|
+
institution: string
|
|
15
|
+
github?: string
|
|
16
|
+
}
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const UserAccountView: React.FC<UserAccountViewProps> = ({ user }) => {
|
|
18
|
-
const [userLinks, setLinks] = useState<string[]>(user.links || [])
|
|
19
|
-
const [userLocation, setLocation] = useState<string>(user.location || "")
|
|
20
|
-
const [userInstitution, setInstitution] = useState<string>(
|
|
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
|
+
}
|
|
21
88
|
|
|
22
89
|
return (
|
|
23
90
|
<div data-testid="user-account-view" className={styles.useraccountview}>
|
|
@@ -35,28 +102,41 @@ export const UserAccountView: React.FC<UserAccountViewProps> = ({ user }) => {
|
|
|
35
102
|
<span>ORCID:</span>
|
|
36
103
|
{user.orcid}
|
|
37
104
|
</li>
|
|
38
|
-
{user.github
|
|
105
|
+
{user.github
|
|
106
|
+
? (
|
|
107
|
+
<li>
|
|
108
|
+
<span>GitHub:</span>
|
|
109
|
+
{user.github}
|
|
110
|
+
</li>
|
|
111
|
+
)
|
|
112
|
+
: <li>Connect your GitHub</li>}
|
|
39
113
|
</ul>
|
|
40
114
|
|
|
41
115
|
<EditableContent
|
|
42
116
|
editableContent={userLinks}
|
|
43
|
-
setRows={
|
|
117
|
+
setRows={handleLinksChange}
|
|
44
118
|
className="custom-class"
|
|
45
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"
|
|
46
124
|
/>
|
|
125
|
+
|
|
47
126
|
<EditableContent
|
|
48
127
|
editableContent={userLocation}
|
|
49
|
-
setRows={
|
|
128
|
+
setRows={handleLocationChange}
|
|
50
129
|
className="custom-class"
|
|
51
130
|
heading="Location"
|
|
131
|
+
data-testid="location-section"
|
|
52
132
|
/>
|
|
53
133
|
<EditableContent
|
|
54
134
|
editableContent={userInstitution}
|
|
55
|
-
setRows={
|
|
135
|
+
setRows={handleInstitutionChange}
|
|
56
136
|
className="custom-class"
|
|
57
137
|
heading="Institution"
|
|
138
|
+
data-testid="institution-section"
|
|
58
139
|
/>
|
|
59
140
|
</div>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import styles from "./scss/usercard.module.scss"
|
|
1
|
+
import React from "react"
|
|
2
|
+
import styles from "./scss/usercard.module.scss"
|
|
3
3
|
|
|
4
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
|
|
5
|
+
name: string
|
|
6
|
+
location?: string
|
|
7
|
+
email: string
|
|
8
|
+
orcid: string
|
|
9
|
+
institution?: string
|
|
10
|
+
links?: string[]
|
|
11
|
+
github?: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface UserCardProps {
|
|
15
|
-
user: User
|
|
15
|
+
user: User
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const UserCard: React.FC<UserCardProps> = ({ user }) => {
|
|
19
|
-
const { location, institution, email, orcid, links = [], github, name } = user
|
|
19
|
+
const { location, institution, email, orcid, links = [], github, name } = user
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
22
|
<div className={styles.userCard}>
|
|
@@ -43,17 +43,19 @@ export const UserCard: React.FC<UserCardProps> = ({ user }) => {
|
|
|
43
43
|
{email}
|
|
44
44
|
</a>
|
|
45
45
|
</li>
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
)}
|
|
57
59
|
{github && (
|
|
58
60
|
<li>
|
|
59
61
|
<i className="fab fa-github"></i>
|
|
@@ -80,5 +82,5 @@ export const UserCard: React.FC<UserCardProps> = ({ user }) => {
|
|
|
80
82
|
))}
|
|
81
83
|
</ul>
|
|
82
84
|
</div>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
+
)
|
|
86
|
+
}
|