@openneuro/app 4.29.9 → 4.30.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,48 +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
-
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"
7
6
 
8
7
  interface User {
9
- id: string;
10
- name: string;
11
- location: string;
12
- github?: string;
13
- institution: string;
14
- email: string;
15
- avatar: string;
16
- orcid: string;
17
- links: string[];
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[]
18
17
  }
19
18
 
20
19
  interface AccountContainerProps {
21
- user: User;
22
- hasEdit: boolean;
20
+ user: User
21
+ hasEdit: boolean
23
22
  }
24
23
 
25
24
  export const UserAccountContainer: React.FC<AccountContainerProps> = ({
26
- user,
27
- hasEdit,
25
+ user,
26
+ hasEdit,
28
27
  }) => {
29
- return (
30
- <>
31
- <div className='container'>
32
- <header className={styles.userHeader}>
33
- <img className={styles.avatar} src={user.avatar} alt={user.name} />
34
- <h2 className={styles.username}>{user.name}</h2>
35
- </header>
36
- </div>
37
- <div className={styles.usercontainer + ' container'}>
38
- <section className={styles.userSidebar}>
39
- <UserCard user={user} />
40
- <UserAccountTabs hasEdit={hasEdit} />
41
- </section>
42
- <section className={styles.userViews}>
43
- <Outlet />
44
- </section>
45
- </div>
46
- </>
47
- )
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
+ )
48
49
  }
@@ -1,37 +1,37 @@
1
- import React from 'react';
1
+ import React from "react"
2
2
 
3
3
  interface User {
4
- name: string;
4
+ name: string
5
5
  }
6
6
 
7
7
  interface Dataset {
8
- id: string;
9
- created: string;
10
- ownerId: string;
11
- name: string;
12
- type: string;
8
+ id: string
9
+ created: string
10
+ ownerId: string
11
+ name: string
12
+ type: string
13
13
  }
14
14
 
15
15
  interface UserDatasetsViewProps {
16
- user: User;
16
+ user: User
17
17
  }
18
18
 
19
19
  const dummyDatasets: Dataset[] = [
20
20
  {
21
- id: 'ds00001',
22
- created: '2023-11-01T12:00:00Z',
23
- ownerId: '1',
24
- name: 'Dataset 1',
25
- type: 'public',
21
+ id: "ds00001",
22
+ created: "2023-11-01T12:00:00Z",
23
+ ownerId: "1",
24
+ name: "Dataset 1",
25
+ type: "public",
26
26
  },
27
27
  {
28
- id: 'ds00002',
29
- created: '2023-11-02T12:00:00Z',
30
- ownerId: '2',
31
- name: 'Dataset 2',
32
- type: 'private',
28
+ id: "ds00002",
29
+ created: "2023-11-02T12:00:00Z",
30
+ ownerId: "2",
31
+ name: "Dataset 2",
32
+ type: "private",
33
33
  },
34
- ];
34
+ ]
35
35
 
36
36
  export const UserDatasetsView: React.FC<UserDatasetsViewProps> = ({ user }) => {
37
37
  return (
@@ -47,7 +47,7 @@ export const UserDatasetsView: React.FC<UserDatasetsViewProps> = ({ user }) => {
47
47
  ))}
48
48
  </div>
49
49
  </div>
50
- );
51
- };
50
+ )
51
+ }
52
52
 
53
- export default UserDatasetsView;
53
+ export default UserDatasetsView
@@ -1,12 +1,11 @@
1
- import React from "react";
1
+ import React from "react"
2
2
 
3
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
- };
12
-
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
+ }
@@ -1,80 +1,76 @@
1
- // UserQuery.tsx
2
-
3
- import React from 'react';
4
- import { useParams } from 'react-router-dom';
5
- import { UserRoutes } from './user-routes';
6
- import FourOFourPage from '../errors/404page';
7
- import { isValidOrcid } from "../utils/validationUtils";
8
-
9
-
10
-
11
- // Dummy user data
12
- const dummyUsers: Record<string, User> = {
13
- '0000-0001-6755-0259': {
14
- id: '1',
15
- name: 'Gregory Noack',
16
- location: 'Stanford, CA',
17
- github: 'thinknoack',
18
- institution: 'Stanford University',
19
- email: 'gregorynoack@thinknoack.com',
20
- avatar: 'https://dummyimage.com/200x200/000/fff',
21
- orcid: '0000-0001-6755-0259',
22
- links: ['onelink.com', 'https://www.twolink.com'],
23
- },
24
- '0000-0002-1234-5678': {
25
- id: '2',
26
- name: 'Jane Doe',
27
- location: 'Stanford, CA',
28
- institution: 'Stanford University',
29
- email: 'janedoe@example.com',
30
- avatar: 'https://dummyimage.com/200x200/000/fff',
31
- orcid: '0000-0002-1234-5678',
32
- links: ['onelink.com', 'https://www.twolink.com'],
33
- },
34
- '0000-0003-2345-6789': {
35
- id: '3',
36
- name: 'John Smith',
37
- location: 'Stanford, CA',
38
- institution: 'Stanford University',
39
- email: 'johnsmith@example.com',
40
- avatar: 'https://dummyimage.com/200x200/000/fff',
41
- orcid: '0000-0003-2345-6789',
42
- links: ['onelink.com', 'https://www.twolink.com'],
43
- },
44
- };
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"
45
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
+ `
46
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
+ `
47
37
 
48
38
  export interface User {
49
- id: string;
50
- name: string;
51
- location: string;
52
- github?: string;
53
- institution: string;
54
- email: string;
55
- avatar: string;
56
- orcid: string;
57
- links: string[];
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[]
58
48
  }
59
49
 
60
50
  export const UserQuery: React.FC = () => {
61
- const { orcid } = useParams<{ orcid: string }>();
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
+ })
62
57
 
63
- // Validate ORCID and return 404 if invalid or missing
64
- if (!orcid || !isValidOrcid(orcid)) {
65
- return <FourOFourPage />;
58
+ const [cookies] = useCookies()
59
+ const profile = getProfile(cookies)
60
+
61
+ if (!isOrcidValid) {
62
+ return <FourOFourPage />
66
63
  }
67
64
 
68
- // Check if the user exists in the dummyUsers data
69
- const user = dummyUsers[orcid];
65
+ if (loading) return <div>Loading...</div>
70
66
 
71
- if (!user) {
72
- // If user is not found, render 404 page
73
- return <FourOFourPage />;
67
+ if (error || !data?.user || data.user.orcid !== orcid) {
68
+ return <FourOFourPage />
74
69
  }
75
70
 
76
- // Mocked for now
77
- const hasEdit = true;
71
+ // is admin or profile matches id from the user data being returned
72
+ const hasEdit = isAdmin || data.user.id !== profile.sub ? true : false
78
73
 
79
- return <UserRoutes user={user} hasEdit={hasEdit} />;
80
- };
74
+ // Render user data with UserRoutes
75
+ return <UserRoutes user={data.user} hasEdit={hasEdit} />
76
+ }
@@ -1,45 +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";
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
9
 
10
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[];
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
20
  }
21
21
 
22
22
  interface UserRoutesProps {
23
- user: User;
24
- hasEdit: boolean;
23
+ user: User
24
+ hasEdit: boolean
25
25
  }
26
26
 
27
27
  export const UserRoutes: React.FC<UserRoutesProps> = ({ user, hasEdit }) => {
28
28
  return (
29
29
  <Routes>
30
30
  <Route path="/*" element={<FourOFourPage />} />
31
- <Route path="*" element={<UserAccountContainer user={user} hasEdit={hasEdit} />}>
31
+ <Route
32
+ path="*"
33
+ element={<UserAccountContainer user={user} hasEdit={hasEdit} />}
34
+ >
32
35
  <Route path="" element={<UserDatasetsView user={user} />} />
33
36
  <Route
34
37
  path="account"
35
- element={hasEdit ? <UserAccountView user={user} /> : <FourOThreePage />}
38
+ element={hasEdit
39
+ ? <UserAccountView user={user} />
40
+ : <FourOThreePage />}
36
41
  />
37
42
  <Route
38
43
  path="notifications"
39
- element={hasEdit ? <UserNotificationsView user={user} /> : <FourOThreePage />}
44
+ element={hasEdit
45
+ ? <UserNotificationsView user={user} />
46
+ : <FourOThreePage />}
40
47
  />
41
48
  <Route path="*" element={<FourOFourPage />} />
42
49
  </Route>
43
50
  </Routes>
44
- );
45
- };
51
+ )
52
+ }
@@ -1,39 +1,43 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
- import { NavLink, useLocation } from "react-router-dom";
3
- import styles from "./scss/usertabs.module.scss";
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
4
 
5
5
  export interface UserAccountTabsProps {
6
- hasEdit: boolean;
6
+ hasEdit: boolean
7
7
  }
8
8
 
9
- export const UserAccountTabs: React.FC<UserAccountTabsProps> = ({ hasEdit }) => {
10
- const ulRef = useRef<HTMLUListElement>(null);
11
- const [activePosition, setActivePosition] = useState<number>(0);
12
- const [clicked, setClicked] = useState(false);
13
- const location = useLocation();
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()
14
16
 
15
17
  useEffect(() => {
16
- const activeLink = ulRef.current?.querySelector(`.${styles.active}`) as HTMLElement;
18
+ const activeLink = ulRef.current?.querySelector(
19
+ `.${styles.active}`,
20
+ ) as HTMLElement
17
21
  if (activeLink) {
18
- const li = activeLink.parentElement as HTMLElement;
19
- setActivePosition(li.offsetTop);
22
+ const li = activeLink.parentElement as HTMLElement
23
+ setActivePosition(li.offsetTop)
20
24
  }
21
- }, [location]);
25
+ }, [location])
22
26
 
23
27
  const handleClick = () => {
28
+ setClicked(true)
29
+ }
24
30
 
25
- setClicked(true);
26
-
27
- };
28
-
29
- if (!hasEdit) return null;
31
+ if (!hasEdit) return null
30
32
 
31
33
  return (
32
34
  <div className={styles.userAccountTabLinks}>
33
35
  <ul
34
36
  ref={ulRef}
35
37
  className={`${clicked ? "clicked" : ""} ${styles.userAccountTabLinks}`}
36
- style={{ "--active-offset": `${activePosition}px` } as React.CSSProperties}
38
+ style={{
39
+ "--active-offset": `${activePosition}px`,
40
+ } as React.CSSProperties}
37
41
  >
38
42
  <li>
39
43
  <NavLink
@@ -66,5 +70,5 @@ export const UserAccountTabs: React.FC<UserAccountTabsProps> = ({ hasEdit }) =>
66
70
  </li>
67
71
  </ul>
68
72
  </div>
69
- );
70
- };
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
  })
@@ -1,9 +1,8 @@
1
-
2
1
  /**
3
2
  * Validates an ORCID.
4
3
  * @param orcid - The ORCID string to validate.
5
4
  * @returns `true` if the ORCID is valid, `false` otherwise.
6
5
  */
7
6
  export function isValidOrcid(orcid: string): boolean {
8
- return /^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$/.test(orcid || '');
9
- }
7
+ return /^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$/.test(orcid || "")
8
+ }
@@ -31,12 +31,12 @@ const warningHeader = (count) => (
31
31
  </div>
32
32
  )
33
33
 
34
- const errorHeader = (count) => (
34
+ const errorHeader = (errorCount) => (
35
35
  <div>
36
36
  <h3 className="metaheader">BIDS Validation</h3>
37
37
 
38
38
  <span className="label text-warning pull-right">
39
- {count} {pluralize("Error", count)}
39
+ {errorCount} {pluralize("Error", errorCount)}
40
40
  </span>
41
41
  <span className="dataset-status ds-danger">
42
42
  <i className="fa fa-exclamation-circle" /> Invalid
@@ -53,10 +53,11 @@ const Valid = () => (
53
53
  )
54
54
 
55
55
  interface WarningsProps {
56
+ issues: DatasetIssues
56
57
  warnings: DatasetIssues
57
58
  }
58
59
 
59
- const Warnings = ({ warnings }: WarningsProps) => (
60
+ const Warnings = ({ issues, warnings }: WarningsProps) => (
60
61
  <ValidationPanel heading={warningHeader(warnings.size)}>
61
62
  <div>
62
63
  <span className="message error fade-in">
@@ -69,15 +70,17 @@ const Warnings = ({ warnings }: WarningsProps) => (
69
70
  </span>
70
71
  </div>
71
72
  <br />
72
- <Results issues={warnings} />
73
+ <Results issues={issues} />
73
74
  </ValidationPanel>
74
75
  )
75
76
 
76
77
  interface ErrorsProps {
78
+ issues: DatasetIssues
77
79
  errors: DatasetIssues
80
+ warnings: DatasetIssues
78
81
  }
79
82
 
80
- const Errors = ({ errors }: ErrorsProps) => (
83
+ const Errors = ({ issues, errors }: ErrorsProps) => (
81
84
  <ValidationPanel heading={errorHeader(errors.size)}>
82
85
  <span className="message error fade-in">
83
86
  Your dataset is no longer valid. You must fix the{" "}
@@ -86,7 +89,7 @@ const Errors = ({ errors }: ErrorsProps) => (
86
89
  to use all of the site features.
87
90
  </span>
88
91
  <br />
89
- <Results issues={errors} />
92
+ <Results issues={issues} />
90
93
  </ValidationPanel>
91
94
  )
92
95
 
@@ -100,9 +103,9 @@ export const Validation = ({ issues }: ValidationProps) => {
100
103
  const warnings = grouped.get("warning")
101
104
  const errors = grouped.get("error")
102
105
  if (errors?.size) {
103
- return <Errors errors={issues} />
106
+ return <Errors issues={issues} errors={errors} warnings={warnings} />
104
107
  } else if (warnings?.size) {
105
- return <Warnings warnings={issues} />
108
+ return <Warnings issues={issues} warnings={warnings} />
106
109
  } else {
107
110
  return <Valid />
108
111
  }