@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.
Files changed (28) hide show
  1. package/.scss-lint.yml +11 -11
  2. package/maintenance.html +26 -20
  3. package/package.json +3 -3
  4. package/src/@types/custom.d.ts +2 -4
  5. package/src/index.html +14 -10
  6. package/src/scripts/datalad/routes/dataset-redirect.tsx +2 -0
  7. package/src/scripts/dataset/mutations/__tests__/update-permissions.spec.jsx +1 -1
  8. package/src/scripts/dataset/mutations/update-permissions.tsx +1 -1
  9. package/src/scripts/routes.tsx +16 -3
  10. package/src/scripts/users/__tests__/user-account-view.spec.tsx +146 -63
  11. package/src/scripts/users/__tests__/user-card.spec.tsx +62 -47
  12. package/src/scripts/users/__tests__/user-query.spec.tsx +65 -60
  13. package/src/scripts/users/__tests__/user-routes.spec.tsx +71 -40
  14. package/src/scripts/users/__tests__/user-tabs.spec.tsx +63 -66
  15. package/src/scripts/users/components/edit-list.tsx +53 -29
  16. package/src/scripts/users/components/edit-string.tsx +63 -22
  17. package/src/scripts/users/components/editable-content.tsx +85 -50
  18. package/src/scripts/users/user-account-view.tsx +101 -21
  19. package/src/scripts/users/user-card.tsx +26 -24
  20. package/src/scripts/users/user-container.tsx +39 -38
  21. package/src/scripts/users/user-datasets-view.tsx +22 -22
  22. package/src/scripts/users/user-notifications-view.tsx +9 -10
  23. package/src/scripts/users/user-query.tsx +62 -66
  24. package/src/scripts/users/user-routes.tsx +31 -24
  25. package/src/scripts/users/user-tabs.tsx +25 -21
  26. package/src/scripts/utils/__tests__/markdown.spec.tsx +1 -2
  27. package/src/scripts/utils/validationUtils.ts +2 -3
  28. package/src/scripts/validation/validation.tsx +11 -8
@@ -1,41 +1,58 @@
1
- import React, { useState } from 'react';
2
- import { Button } from '@openneuro/components/button';
3
- import '../scss/user-meta-blocks.scss';
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> = ({ placeholder = 'Enter item', elements = [], setElements }) => {
16
- const [newElement, setNewElement] = useState<string>('');
17
- const [warnEmpty, setWarnEmpty] = useState<boolean>(false);
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
- setNewElement('');
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 && <small className="warning-text">Your input was empty</small>}
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={() => removeElement(index)}
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 'react';
2
- import { Button } from '@openneuro/components/button';
3
- import '../scss/user-meta-blocks.scss';
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
- * EditString Component
13
- * Allows editing a single string value.
14
- */
15
- export const EditString: React.FC<EditStringProps> = ({ value = '', setValue, placeholder = 'Enter text' }) => {
16
- const [currentValue, setCurrentValue] = useState<string>(value);
17
- const [warnEmpty, setWarnEmpty] = useState<boolean>(false);
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
- const handleSave = (): void => {
20
- if (!currentValue.trim()) {
21
- setWarnEmpty(true);
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(false);
24
- setValue(currentValue.trim());
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
- {warnEmpty && <small className="warning-text">The input cannot be empty</small>}
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 '../scss/editable-content.scss'
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 { EditableContent } from "./components/editable-content";
3
- import styles from './scss/useraccountview.module.scss'
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>(user.institution || "");
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 ? <li><span>github:</span>{user.github}</li> : <li>Connect your github</li>}
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={setLinks}
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={(newLocation: string) => setLocation(newLocation)}
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={(newInstitution: string) => setInstitution(newInstitution)}
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
- <li className={styles.orcid}>
47
- <i className="fab fa-orcid" aria-hidden="true"></i>
48
- <a
49
- href={`https://orcid.org/${orcid}`}
50
- target="_blank"
51
- rel="noopener noreferrer"
52
- aria-label={`ORCID profile of ${name}`}
53
- >
54
- {orcid}
55
- </a>
56
- </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
+ )}
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
+ }