@openneuro/app 4.29.8 → 4.29.9

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 (30) hide show
  1. package/package.json +3 -3
  2. package/src/@types/custom.d.ts +8 -0
  3. package/src/scripts/dataset/mutations/__tests__/update-permissions.spec.jsx +2 -1
  4. package/src/scripts/dataset/mutations/update-permissions.tsx +1 -9
  5. package/src/scripts/routes.tsx +4 -0
  6. package/src/scripts/users/__tests__/user-account-view.spec.tsx +69 -0
  7. package/src/scripts/users/__tests__/user-card.spec.tsx +95 -0
  8. package/src/scripts/users/__tests__/user-query.spec.tsx +60 -0
  9. package/src/scripts/users/__tests__/user-routes.spec.tsx +71 -0
  10. package/src/scripts/users/__tests__/user-tabs.spec.tsx +87 -0
  11. package/src/scripts/users/components/close-button.tsx +20 -0
  12. package/src/scripts/users/components/edit-button.tsx +20 -0
  13. package/src/scripts/users/components/edit-list.tsx +79 -0
  14. package/src/scripts/users/components/edit-string.tsx +49 -0
  15. package/src/scripts/users/components/editable-content.tsx +63 -0
  16. package/src/scripts/users/scss/editable-content.scss +15 -0
  17. package/src/scripts/users/scss/user-meta-blocks.scss +14 -0
  18. package/src/scripts/users/scss/useraccountview.module.scss +20 -0
  19. package/src/scripts/users/scss/usercard.module.scss +24 -0
  20. package/src/scripts/users/scss/usercontainer.module.scss +38 -0
  21. package/src/scripts/users/scss/usertabs.module.scss +63 -0
  22. package/src/scripts/users/user-account-view.tsx +62 -0
  23. package/src/scripts/users/user-card.tsx +84 -0
  24. package/src/scripts/users/user-container.tsx +48 -0
  25. package/src/scripts/users/user-datasets-view.tsx +53 -0
  26. package/src/scripts/users/user-notifications-view.tsx +12 -0
  27. package/src/scripts/users/user-query.tsx +80 -0
  28. package/src/scripts/users/user-routes.tsx +45 -0
  29. package/src/scripts/users/user-tabs.tsx +70 -0
  30. package/src/scripts/utils/validationUtils.ts +9 -0
@@ -0,0 +1,63 @@
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
+
9
+ interface EditableContentProps {
10
+ editableContent: string[] | string;
11
+ setRows: React.Dispatch<React.SetStateAction<string[] | string>>;
12
+ className: string;
13
+ heading: string;
14
+ }
15
+
16
+ export const EditableContent: React.FC<EditableContentProps> = ({
17
+ editableContent,
18
+ setRows,
19
+ className,
20
+ heading,
21
+ }) => {
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
+ };
61
+
62
+
63
+
@@ -0,0 +1,15 @@
1
+
2
+
3
+ .user-meta-block{
4
+ margin-top: 30px;
5
+ }
6
+ .umb-heading{
7
+ display: flex;
8
+ justify-content: space-between;
9
+ align-items: center;
10
+ border-bottom: 1px solid #e0e0e0;
11
+ h4{
12
+ margin: 0;
13
+ }
14
+ }
15
+
@@ -0,0 +1,14 @@
1
+ .user-meta-block {
2
+ .edit-string-container,
3
+ .edit-list-container{
4
+ margin-top: 20px;
5
+ }
6
+ ul{
7
+ list-style: none;
8
+ margin: 0;
9
+ padding: 0;
10
+ li {
11
+ p{ margin-bottom: 0;}
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ .useraccountview{
2
+ h3{
3
+ margin-top: 0;
4
+ font-size: 22px;
5
+ }
6
+ .accountDetail{
7
+ list-style: none;
8
+ margin: 0;
9
+ padding: 0;
10
+ li{
11
+ margin: 0 0 10px;
12
+ font-size: 16px;
13
+ }
14
+ span{
15
+ font-weight: 700;
16
+ margin-right: 10px;
17
+ }
18
+ }
19
+ }
20
+
@@ -0,0 +1,24 @@
1
+ /* usercard.module.scss */
2
+ .userCard {
3
+ padding: 20px;
4
+ ul{
5
+ list-style: none;
6
+ margin: 0;
7
+ padding: 0;
8
+ li{
9
+ margin-bottom: 5px;
10
+ font-size: 14px;
11
+ i{
12
+ margin-right: 10px;
13
+ color: #666;
14
+
15
+ }
16
+ &.orcid i{
17
+ color: #a6cf39;
18
+ }
19
+ &:last-of-type{
20
+ margin-bottom: 0;
21
+ }
22
+ }
23
+ }
24
+ }
@@ -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,62 @@
1
+ import React, { useState } from "react";
2
+ import { EditableContent } from "./components/editable-content";
3
+ import styles from './scss/useraccountview.module.scss'
4
+
5
+ interface UserAccountViewProps {
6
+ user: {
7
+ name: string;
8
+ email: string;
9
+ orcid: string;
10
+ links: string[];
11
+ location: string;
12
+ institution: string;
13
+ github?: string;
14
+ };
15
+ }
16
+
17
+ 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 || "");
21
+
22
+ return (
23
+ <div data-testid="user-account-view" className={styles.useraccountview}>
24
+ <h3>Account</h3>
25
+ <ul className={styles.accountDetail}>
26
+ <li>
27
+ <span>Name:</span>
28
+ {user.name}
29
+ </li>
30
+ <li>
31
+ <span>Email:</span>
32
+ {user.email}
33
+ </li>
34
+ <li>
35
+ <span>ORCID:</span>
36
+ {user.orcid}
37
+ </li>
38
+ {user.github ? <li><span>github:</span>{user.github}</li> : <li>Connect your github</li>}
39
+ </ul>
40
+
41
+ <EditableContent
42
+ editableContent={userLinks}
43
+ setRows={setLinks}
44
+ className="custom-class"
45
+ heading="Links"
46
+ />
47
+ <EditableContent
48
+ editableContent={userLocation}
49
+ setRows={(newLocation: string) => setLocation(newLocation)}
50
+ className="custom-class"
51
+ heading="Location"
52
+ />
53
+ <EditableContent
54
+ editableContent={userInstitution}
55
+ setRows={(newInstitution: string) => setInstitution(newInstitution)}
56
+ className="custom-class"
57
+ heading="Institution"
58
+ />
59
+ </div>
60
+ );
61
+ };
62
+
@@ -0,0 +1,84 @@
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
+ <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>
57
+ {github && (
58
+ <li>
59
+ <i className="fab fa-github"></i>
60
+ <a
61
+ href={`https://github.com/${github}`}
62
+ target="_blank"
63
+ rel="noopener noreferrer"
64
+ aria-label={`Github profile of ${name}`}
65
+ >
66
+ {github}
67
+ </a>
68
+ </li>
69
+ )}
70
+ {links.length > 0 &&
71
+ links
72
+ .filter(Boolean)
73
+ .map((link, index) => (
74
+ <li key={index}>
75
+ <i className="fa fa-link"></i>
76
+ <a href={link} target="_blank" rel="noopener noreferrer">
77
+ {link}
78
+ </a>
79
+ </li>
80
+ ))}
81
+ </ul>
82
+ </div>
83
+ );
84
+ };
@@ -0,0 +1,48 @@
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
+
8
+ 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[];
18
+ }
19
+
20
+ interface AccountContainerProps {
21
+ user: User;
22
+ hasEdit: boolean;
23
+ }
24
+
25
+ export const UserAccountContainer: React.FC<AccountContainerProps> = ({
26
+ user,
27
+ hasEdit,
28
+ }) => {
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
+ )
48
+ }
@@ -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,12 @@
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
+ };
12
+
@@ -0,0 +1,80 @@
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
+ };
45
+
46
+
47
+
48
+ 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[];
58
+ }
59
+
60
+ export const UserQuery: React.FC = () => {
61
+ const { orcid } = useParams<{ orcid: string }>();
62
+
63
+ // Validate ORCID and return 404 if invalid or missing
64
+ if (!orcid || !isValidOrcid(orcid)) {
65
+ return <FourOFourPage />;
66
+ }
67
+
68
+ // Check if the user exists in the dummyUsers data
69
+ const user = dummyUsers[orcid];
70
+
71
+ if (!user) {
72
+ // If user is not found, render 404 page
73
+ return <FourOFourPage />;
74
+ }
75
+
76
+ // Mocked for now
77
+ const hasEdit = true;
78
+
79
+ return <UserRoutes user={user} hasEdit={hasEdit} />;
80
+ };
@@ -0,0 +1,45 @@
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 path="*" element={<UserAccountContainer user={user} hasEdit={hasEdit} />}>
32
+ <Route path="" element={<UserDatasetsView user={user} />} />
33
+ <Route
34
+ path="account"
35
+ element={hasEdit ? <UserAccountView user={user} /> : <FourOThreePage />}
36
+ />
37
+ <Route
38
+ path="notifications"
39
+ element={hasEdit ? <UserNotificationsView user={user} /> : <FourOThreePage />}
40
+ />
41
+ <Route path="*" element={<FourOFourPage />} />
42
+ </Route>
43
+ </Routes>
44
+ );
45
+ };