@openneuro/app 4.29.7 → 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.
- package/package.json +3 -3
- package/src/@types/custom.d.ts +8 -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 +4 -0
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +69 -0
- package/src/scripts/users/__tests__/user-card.spec.tsx +95 -0
- package/src/scripts/users/__tests__/user-query.spec.tsx +60 -0
- package/src/scripts/users/__tests__/user-routes.spec.tsx +71 -0
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +87 -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 +79 -0
- package/src/scripts/users/components/edit-string.tsx +49 -0
- package/src/scripts/users/components/editable-content.tsx +63 -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 +62 -0
- package/src/scripts/users/user-card.tsx +84 -0
- package/src/scripts/users/user-container.tsx +48 -0
- package/src/scripts/users/user-datasets-view.tsx +53 -0
- package/src/scripts/users/user-notifications-view.tsx +12 -0
- package/src/scripts/users/user-query.tsx +80 -0
- package/src/scripts/users/user-routes.tsx +45 -0
- package/src/scripts/users/user-tabs.tsx +70 -0
- package/src/scripts/utils/validationUtils.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/app",
|
|
3
|
-
"version": "4.29.
|
|
3
|
+
"version": "4.29.9",
|
|
4
4
|
"description": "React JS web frontend for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "public/client.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@emotion/react": "11.11.1",
|
|
21
21
|
"@emotion/styled": "11.11.0",
|
|
22
22
|
"@niivue/niivue": "0.45.1",
|
|
23
|
-
"@openneuro/components": "^4.29.
|
|
23
|
+
"@openneuro/components": "^4.29.9",
|
|
24
24
|
"@sentry/react": "^8.25.0",
|
|
25
25
|
"@tanstack/react-table": "^8.9.3",
|
|
26
26
|
"buffer": "^6.0.3",
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"publishConfig": {
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "992fc9e3044c67f6d85757ecf252845cfda6e809"
|
|
78
78
|
}
|
package/src/@types/custom.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ declare module "*.svg" {
|
|
|
11
11
|
export = value
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// Allow custom scss modules
|
|
17
|
+
declare module "*.module.scss" {
|
|
18
|
+
const classes: { [key: string]: string };
|
|
19
|
+
export default classes;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
// Allow .scss imports
|
|
15
23
|
declare module "*.scss" {
|
|
16
24
|
const value: string
|
|
@@ -2,12 +2,13 @@ import React from "react"
|
|
|
2
2
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
|
3
3
|
import { MockedProvider } from "@apollo/client/testing"
|
|
4
4
|
import {
|
|
5
|
-
isValidOrcid,
|
|
6
5
|
UPDATE_ORCID_PERMISSIONS,
|
|
7
6
|
UPDATE_PERMISSIONS,
|
|
8
7
|
UpdateDatasetPermissions,
|
|
9
8
|
} from "../update-permissions"
|
|
10
9
|
|
|
10
|
+
import { isValidOrcid } from "../../../utils/validationUtils.ts";
|
|
11
|
+
|
|
11
12
|
function permissionMocksFactory(
|
|
12
13
|
updatePermissionsCalled,
|
|
13
14
|
updateOrcidPermissionsCalled,
|
|
@@ -7,15 +7,7 @@ import ToastContent from "../../common/partials/toast-content"
|
|
|
7
7
|
import { validate as isValidEmail } from "email-validator"
|
|
8
8
|
import { Button } from "@openneuro/components/button"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
if (orcid) {
|
|
12
|
-
return /^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$/.test(orcid)
|
|
13
|
-
? true
|
|
14
|
-
: false
|
|
15
|
-
} else {
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
}
|
|
10
|
+
import { isValidOrcid } from "../../utils/validationUtils";
|
|
19
11
|
|
|
20
12
|
export const UPDATE_PERMISSIONS = gql`
|
|
21
13
|
mutation updatePermissions(
|
package/src/scripts/routes.tsx
CHANGED
|
@@ -5,6 +5,8 @@ import { Navigate, Route, Routes } from "react-router-dom"
|
|
|
5
5
|
import DatasetQuery from "./dataset/dataset-query"
|
|
6
6
|
//import PreRefactorDatasetProps from './dataset/dataset-pre-refactor-container'
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
|
|
8
10
|
import FaqPage from "./pages/faq/faq"
|
|
9
11
|
import FrontPageContainer from "./pages/front-page/front-page"
|
|
10
12
|
import Admin from "./pages/admin/admin"
|
|
@@ -17,6 +19,7 @@ import FourOFourPage from "./errors/404page"
|
|
|
17
19
|
import { ImportDataset } from "./pages/import-dataset"
|
|
18
20
|
import { DatasetMetadata } from "./pages/metadata/dataset-metadata"
|
|
19
21
|
import { TermsPage } from "./pages/terms"
|
|
22
|
+
import { UserQuery } from "./users/user-query"
|
|
20
23
|
|
|
21
24
|
const AppRoutes: React.VoidFunctionComponent = () => (
|
|
22
25
|
<Routes>
|
|
@@ -33,6 +36,7 @@ const AppRoutes: React.VoidFunctionComponent = () => (
|
|
|
33
36
|
<Route path="/import" element={<ImportDataset />} />
|
|
34
37
|
<Route path="/metadata" element={<DatasetMetadata />} />
|
|
35
38
|
<Route path="/public" element={<Navigate to="/search" replace />} />
|
|
39
|
+
<Route path="/user/:orcid/*" element={<UserQuery />} />
|
|
36
40
|
<Route
|
|
37
41
|
path="/saved"
|
|
38
42
|
element={<Navigate to="/search?bookmarks" replace />}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, within, waitFor} from '@testing-library/react';
|
|
3
|
+
import { UserAccountView } from '../user-account-view';
|
|
4
|
+
|
|
5
|
+
const baseUser = {
|
|
6
|
+
name: "John Doe",
|
|
7
|
+
email: "johndoe@example.com",
|
|
8
|
+
orcid: "0000-0001-2345-6789",
|
|
9
|
+
location: "San Francisco, CA",
|
|
10
|
+
institution: "University of California",
|
|
11
|
+
links: ["https://example.com", "https://example.org"],
|
|
12
|
+
github: "johndoe",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('<UserAccountView />', () => {
|
|
16
|
+
it('should render the user details correctly', () => {
|
|
17
|
+
render(<UserAccountView user={baseUser} />);
|
|
18
|
+
|
|
19
|
+
// Check if user details are rendered
|
|
20
|
+
expect(screen.getByText('Name:')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText('Email:')).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText('johndoe@example.com')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('ORCID:')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('0000-0001-2345-6789')).toBeInTheDocument();
|
|
26
|
+
expect(screen.getByText('johndoe')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should render links with EditableContent', async () => {
|
|
30
|
+
render(<UserAccountView user={baseUser} />);
|
|
31
|
+
const institutionSection = within(screen.getByText('Institution').closest('.user-meta-block'));
|
|
32
|
+
expect(screen.getByText('Institution')).toBeInTheDocument();
|
|
33
|
+
const editButton = institutionSection.getByText('Edit');
|
|
34
|
+
fireEvent.click(editButton);
|
|
35
|
+
const textbox = institutionSection.getByRole('textbox');
|
|
36
|
+
fireEvent.change(textbox, { target: { value: 'New University' } });
|
|
37
|
+
const saveButton = institutionSection.getByText('Save');
|
|
38
|
+
const closeButton = institutionSection.getByText('Close');
|
|
39
|
+
fireEvent.click(saveButton);
|
|
40
|
+
fireEvent.click(closeButton);
|
|
41
|
+
// Add debug step
|
|
42
|
+
await waitFor(() => screen.debug());
|
|
43
|
+
// Use a flexible matcher to check for text
|
|
44
|
+
await waitFor(() =>
|
|
45
|
+
expect(institutionSection.getByText('New University')).toBeInTheDocument()
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
it('should render location with EditableContent', async () => {
|
|
51
|
+
render(<UserAccountView user={baseUser} />);
|
|
52
|
+
const locationSection = within(screen.getByText('Location').closest('.user-meta-block'));
|
|
53
|
+
expect(screen.getByText('Location')).toBeInTheDocument();
|
|
54
|
+
const editButton = locationSection.getByText('Edit');
|
|
55
|
+
fireEvent.click(editButton);
|
|
56
|
+
const textbox = locationSection.getByRole('textbox');
|
|
57
|
+
fireEvent.change(textbox, { target: { value: 'Marin, CA' } });
|
|
58
|
+
const saveButton = locationSection.getByText('Save');
|
|
59
|
+
const closeButton = locationSection.getByText('Close');
|
|
60
|
+
fireEvent.click(saveButton);
|
|
61
|
+
fireEvent.click(closeButton);
|
|
62
|
+
// Add debug step
|
|
63
|
+
await waitFor(() => screen.debug());
|
|
64
|
+
// Use a flexible matcher to check for text
|
|
65
|
+
await waitFor(() =>
|
|
66
|
+
expect(locationSection.getByText('Marin, CA')).toBeInTheDocument()
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import type { User } from "../user-card";
|
|
4
|
+
import { UserCard } from "../user-card";
|
|
5
|
+
|
|
6
|
+
describe("UserCard Component", () => {
|
|
7
|
+
const baseUser: User = {
|
|
8
|
+
name: "John Doe",
|
|
9
|
+
email: "johndoe@example.com",
|
|
10
|
+
orcid: "0000-0001-2345-6789",
|
|
11
|
+
location: "San Francisco, CA",
|
|
12
|
+
institution: "University of California",
|
|
13
|
+
links: ["https://example.com", "https://example.org"],
|
|
14
|
+
github: "johndoe",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
it("renders all user details when all data is provided", () => {
|
|
18
|
+
|
|
19
|
+
render(<UserCard user={baseUser} />);
|
|
20
|
+
|
|
21
|
+
const orcidLink = screen.getByRole("link", {
|
|
22
|
+
name: "ORCID profile of John Doe",
|
|
23
|
+
});
|
|
24
|
+
expect(orcidLink).toHaveAttribute("href", "https://orcid.org/0000-0001-2345-6789");
|
|
25
|
+
expect(screen.getByText("University of California")).toBeInTheDocument();
|
|
26
|
+
expect(screen.getByText("San Francisco, CA")).toBeInTheDocument();
|
|
27
|
+
|
|
28
|
+
const emailLink = screen.getByRole("link", { name: "johndoe@example.com" });
|
|
29
|
+
expect(emailLink).toHaveAttribute("href", "mailto:johndoe@example.com");
|
|
30
|
+
|
|
31
|
+
const githubLink = screen.getByRole("link", { name: "Github profile of John Doe", });
|
|
32
|
+
expect(githubLink).toHaveAttribute("href", "https://github.com/johndoe");
|
|
33
|
+
expect(
|
|
34
|
+
screen.getByRole("link", { name: "https://example.com" })
|
|
35
|
+
).toHaveAttribute("href", "https://example.com");
|
|
36
|
+
expect(
|
|
37
|
+
screen.getByRole("link", { name: "https://example.org" })
|
|
38
|
+
).toHaveAttribute("href", "https://example.org");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders without optional fields", () => {
|
|
42
|
+
const minimalUser: User = {
|
|
43
|
+
name: "Jane Doe",
|
|
44
|
+
email: "janedoe@example.com",
|
|
45
|
+
orcid: "0000-0002-3456-7890",
|
|
46
|
+
links: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
render(<UserCard user={minimalUser} />);
|
|
50
|
+
|
|
51
|
+
const orcidLink = screen.getByRole("link", {
|
|
52
|
+
name: "ORCID profile of Jane Doe",
|
|
53
|
+
});
|
|
54
|
+
expect(orcidLink).toHaveAttribute("href", "https://orcid.org/0000-0002-3456-7890");
|
|
55
|
+
const emailLink = screen.getByRole("link", { name: "janedoe@example.com" });
|
|
56
|
+
expect(emailLink).toHaveAttribute("href", "mailto:janedoe@example.com");
|
|
57
|
+
expect(screen.queryByText("University of California")).not.toBeInTheDocument();
|
|
58
|
+
expect(screen.queryByText("San Francisco, CA")).not.toBeInTheDocument();
|
|
59
|
+
expect(screen.queryByRole("link", { name: "Github profile of Jane Doe" })).not.toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders correctly when links are empty", () => {
|
|
63
|
+
const userWithEmptyLinks: User = {
|
|
64
|
+
...baseUser,
|
|
65
|
+
links: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
render(<UserCard user={userWithEmptyLinks} />);
|
|
69
|
+
|
|
70
|
+
expect(screen.queryByRole("link", { name: "https://example.com" })).not.toBeInTheDocument();
|
|
71
|
+
expect(screen.queryByRole("link", { name: "https://example.org" })).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders correctly when location and institution are missing", () => {
|
|
75
|
+
const userWithoutLocationAndInstitution: User = {
|
|
76
|
+
name: "Emily Doe",
|
|
77
|
+
email: "emilydoe@example.com",
|
|
78
|
+
orcid: "0000-0003-4567-8901",
|
|
79
|
+
links: ["https://example.com"],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
render(<UserCard user={userWithoutLocationAndInstitution} />);
|
|
83
|
+
|
|
84
|
+
const orcidLink = screen.getByRole("link", {
|
|
85
|
+
name: "ORCID profile of Emily Doe",
|
|
86
|
+
});
|
|
87
|
+
expect(orcidLink).toHaveAttribute("href", "https://orcid.org/0000-0003-4567-8901");
|
|
88
|
+
const emailLink = screen.getByRole("link", { name: "emilydoe@example.com" });
|
|
89
|
+
expect(emailLink).toHaveAttribute("href", "mailto:emilydoe@example.com");
|
|
90
|
+
const link = screen.getByRole("link", { name: "https://example.com" });
|
|
91
|
+
expect(link).toHaveAttribute("href", "https://example.com");
|
|
92
|
+
expect(screen.queryByText("San Francisco, CA")).not.toBeInTheDocument();
|
|
93
|
+
expect(screen.queryByText("University of California")).not.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
4
|
+
import { UserQuery } from "../user-query";
|
|
5
|
+
import FourOFourPage from "../../errors/404page";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// TODO update these once the correct query is in place and dummy data is not used.
|
|
9
|
+
// maybe there is a better way to do this
|
|
10
|
+
const VALID_ORCID = "0000-0001-6755-0259";
|
|
11
|
+
const INVALID_ORCID = "0000-000X-1234-5678";
|
|
12
|
+
const UNKNOWN_ORCID = "0000-0000-0000-0000";
|
|
13
|
+
|
|
14
|
+
const renderWithRouter = (orcid: string) => {
|
|
15
|
+
return render(
|
|
16
|
+
<MemoryRouter initialEntries={[`/user/${orcid}`]}>
|
|
17
|
+
<Routes>
|
|
18
|
+
<Route path="/user/:orcid" element={<UserQuery />} />
|
|
19
|
+
<Route path="*" element={<FourOFourPage />} />
|
|
20
|
+
</Routes>
|
|
21
|
+
</MemoryRouter>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("UserQuery Component", () => {
|
|
26
|
+
// TODO update these once the correct query is in place and dummy data is not used.
|
|
27
|
+
// maybe there is a better way to do this
|
|
28
|
+
it("renders UserRoutes for a valid ORCID", async () => {
|
|
29
|
+
renderWithRouter(VALID_ORCID);
|
|
30
|
+
const userName = await screen.findByText("Gregory Noack");
|
|
31
|
+
expect(userName).toBeInTheDocument();
|
|
32
|
+
|
|
33
|
+
const userLocation = screen.getByText("Stanford, CA");
|
|
34
|
+
expect(userLocation).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders FourOFourPage for an invalid ORCID", async () => {
|
|
38
|
+
renderWithRouter(INVALID_ORCID);
|
|
39
|
+
const errorMessage = await screen.findByText(
|
|
40
|
+
/404: The page you are looking for does not exist./i
|
|
41
|
+
);
|
|
42
|
+
expect(errorMessage).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders FourOFourPage for a missing ORCID", async () => {
|
|
46
|
+
renderWithRouter("");
|
|
47
|
+
const errorMessage = await screen.findByText(
|
|
48
|
+
/404: The page you are looking for does not exist./i
|
|
49
|
+
);
|
|
50
|
+
expect(errorMessage).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders FourOFourPage for an unknown ORCID", async () => {
|
|
54
|
+
renderWithRouter(UNKNOWN_ORCID);
|
|
55
|
+
const errorMessage = await screen.findByText(
|
|
56
|
+
/404: The page you are looking for does not exist./i
|
|
57
|
+
);
|
|
58
|
+
expect(errorMessage).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { MemoryRouter } from "react-router-dom";
|
|
4
|
+
import { UserRoutes } from "../user-routes";
|
|
5
|
+
import type { User } from "../user-routes";
|
|
6
|
+
|
|
7
|
+
const defaultUser: User = {
|
|
8
|
+
id: "1",
|
|
9
|
+
name: "John Doe",
|
|
10
|
+
location: "Unknown",
|
|
11
|
+
github: "",
|
|
12
|
+
institution: "Unknown Institution",
|
|
13
|
+
email: "john.doe@example.com",
|
|
14
|
+
avatar: "https://dummyimage.com/200x200/000/fff",
|
|
15
|
+
orcid: "0000-0000-0000-0000",
|
|
16
|
+
links: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const renderWithRouter = (user: User, route: string, hasEdit: boolean) => {
|
|
20
|
+
return render(
|
|
21
|
+
<MemoryRouter initialEntries={[route]}>
|
|
22
|
+
<UserRoutes user={user} hasEdit={hasEdit} />
|
|
23
|
+
</MemoryRouter>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("UserRoutes Component", () => {
|
|
28
|
+
const user: User = defaultUser;
|
|
29
|
+
|
|
30
|
+
it("renders UserDatasetsView for the default route", async () => {
|
|
31
|
+
renderWithRouter(user, "/", true);
|
|
32
|
+
expect(screen.getByText(`${user.name}'s Datasets`)).toBeInTheDocument();
|
|
33
|
+
const datasetsView = await screen.findByTestId("user-datasets-view");
|
|
34
|
+
expect(datasetsView).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders FourOFourPage for an invalid route", async () => {
|
|
38
|
+
renderWithRouter(user, "/nonexistent-route", true);
|
|
39
|
+
const errorMessage = await screen.findByText(
|
|
40
|
+
/404: The page you are looking for does not exist./i
|
|
41
|
+
);
|
|
42
|
+
expect(errorMessage).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders UserAccountView when hasEdit is true", async () => {
|
|
46
|
+
renderWithRouter(user, "/account", true);
|
|
47
|
+
const accountView = await screen.findByTestId("user-account-view");
|
|
48
|
+
expect(accountView).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders UserNotificationsView when hasEdit is true", async () => {
|
|
52
|
+
renderWithRouter(user, "/notifications", true);
|
|
53
|
+
const notificationsView = await screen.findByTestId(
|
|
54
|
+
"user-notifications-view"
|
|
55
|
+
);
|
|
56
|
+
expect(notificationsView).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders FourOThreePage when hasEdit is false for restricted routes", async () => {
|
|
60
|
+
const restrictedRoutes = ["/account", "/notifications"];
|
|
61
|
+
|
|
62
|
+
for (const route of restrictedRoutes) {
|
|
63
|
+
cleanup();
|
|
64
|
+
renderWithRouter(user, route, false);
|
|
65
|
+
const errorMessage = await screen.findByText(
|
|
66
|
+
/403: You do not have access to this page, you may need to sign in./i
|
|
67
|
+
);
|
|
68
|
+
expect(errorMessage).toBeInTheDocument();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
4
|
+
import { UserAccountTabs } from "../user-tabs";
|
|
5
|
+
|
|
6
|
+
// Wrapper component to allow dynamic modification of `hasEdit`
|
|
7
|
+
const UserAccountTabsWrapper: React.FC = () => {
|
|
8
|
+
const [hasEdit, setHasEdit] = useState(true);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<button onClick={() => setHasEdit(!hasEdit)}>Toggle hasEdit</button>
|
|
13
|
+
<MemoryRouter>
|
|
14
|
+
<Routes>
|
|
15
|
+
<Route path="*" element={<UserAccountTabs hasEdit={hasEdit} />} />
|
|
16
|
+
</Routes>
|
|
17
|
+
</MemoryRouter>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("UserAccountTabs Component", () => {
|
|
23
|
+
it("should not render tabs when hasEdit is false", () => {
|
|
24
|
+
render(<UserAccountTabsWrapper />);
|
|
25
|
+
|
|
26
|
+
expect(screen.getByText("User Datasets")).toBeInTheDocument();
|
|
27
|
+
|
|
28
|
+
fireEvent.click(screen.getByText("Toggle hasEdit"));
|
|
29
|
+
|
|
30
|
+
expect(screen.queryByText("User Datasets")).not.toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should render tabs when hasEdit is toggled back to true", () => {
|
|
34
|
+
render(<UserAccountTabsWrapper />);
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText("User Datasets")).toBeInTheDocument();
|
|
37
|
+
|
|
38
|
+
fireEvent.click(screen.getByText("Toggle hasEdit"));
|
|
39
|
+
expect(screen.queryByText("User Datasets")).not.toBeInTheDocument();
|
|
40
|
+
|
|
41
|
+
fireEvent.click(screen.getByText("Toggle hasEdit"));
|
|
42
|
+
expect(screen.getByText("User Datasets")).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should update active class on the correct NavLink based on route", () => {
|
|
46
|
+
render(<UserAccountTabsWrapper />);
|
|
47
|
+
|
|
48
|
+
// Utility function to check if an element has 'active' class - used because of CSS module discrepancies between classNames
|
|
49
|
+
const hasActiveClass = (element) => element.className.includes('active');
|
|
50
|
+
|
|
51
|
+
const datasetsTab = screen.getByText("User Datasets");
|
|
52
|
+
expect(hasActiveClass(datasetsTab)).toBe(true);
|
|
53
|
+
|
|
54
|
+
const notificationsTab = screen.getByText("User Notifications");
|
|
55
|
+
|
|
56
|
+
fireEvent.click(notificationsTab);
|
|
57
|
+
|
|
58
|
+
expect(hasActiveClass(notificationsTab)).toBe(true);
|
|
59
|
+
expect(hasActiveClass(datasetsTab)).toBe(false);
|
|
60
|
+
|
|
61
|
+
const accountTab = screen.getByText("Account Info");
|
|
62
|
+
|
|
63
|
+
fireEvent.click(accountTab);
|
|
64
|
+
|
|
65
|
+
expect(hasActiveClass(accountTab)).toBe(true);
|
|
66
|
+
expect(hasActiveClass(datasetsTab)).toBe(false);
|
|
67
|
+
expect(hasActiveClass(notificationsTab)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
it("should trigger animation state when a tab is clicked", async () => {
|
|
72
|
+
render(<UserAccountTabsWrapper />);
|
|
73
|
+
|
|
74
|
+
const notificationsTab = screen.getByText("User Notifications");
|
|
75
|
+
// Utility function to check if an element has 'clicked' class - used because of CSS module discrepancies between classNames
|
|
76
|
+
const hasClickedClass = (element) => element.className.includes('clicked');
|
|
77
|
+
const tabsContainer = await screen.findByRole("list");
|
|
78
|
+
|
|
79
|
+
expect(hasClickedClass(tabsContainer)).toBe(false);
|
|
80
|
+
|
|
81
|
+
fireEvent.click(notificationsTab);
|
|
82
|
+
|
|
83
|
+
expect(hasClickedClass(tabsContainer)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { FC } from "react"
|
|
3
|
+
import { Button } from "@openneuro/components/button"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An edit button, calls action when clicked
|
|
7
|
+
*/
|
|
8
|
+
interface CloseButtonProps {
|
|
9
|
+
action: () => void
|
|
10
|
+
}
|
|
11
|
+
export const CloseButton: FC<CloseButtonProps> = ({ action }) => {
|
|
12
|
+
return (
|
|
13
|
+
<Button
|
|
14
|
+
className="description-btn description-button-cancel"
|
|
15
|
+
label="Close"
|
|
16
|
+
icon="fas fa-times"
|
|
17
|
+
onClick={() => action()}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { FC } from "react"
|
|
3
|
+
import { Button } from "@openneuro/components/button"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An edit button, calls action when clicked
|
|
7
|
+
*/
|
|
8
|
+
interface EditButtonProps {
|
|
9
|
+
action: () => void
|
|
10
|
+
}
|
|
11
|
+
export const EditButton: FC<EditButtonProps> = ({ action }) => {
|
|
12
|
+
return (
|
|
13
|
+
<Button
|
|
14
|
+
className="description-btn description-button-edit"
|
|
15
|
+
label="Edit"
|
|
16
|
+
icon="fas fa-edit"
|
|
17
|
+
onClick={() => action()}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button } from '@openneuro/components/button';
|
|
3
|
+
import '../scss/user-meta-blocks.scss';
|
|
4
|
+
|
|
5
|
+
interface EditListProps {
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
elements?: string[];
|
|
8
|
+
setElements: (elements: string[]) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* EditList Component
|
|
13
|
+
* Allows adding and removing strings from a list.
|
|
14
|
+
*/
|
|
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);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Remove an element from the list by index
|
|
21
|
+
* @param index - The index of the element to remove
|
|
22
|
+
*/
|
|
23
|
+
const removeElement = (index: number): void => {
|
|
24
|
+
setElements(elements.filter((_, i) => i !== index));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add a new element to the list
|
|
29
|
+
*/
|
|
30
|
+
const addElement = (): void => {
|
|
31
|
+
if (!newElement.trim()) {
|
|
32
|
+
setWarnEmpty(true);
|
|
33
|
+
} else {
|
|
34
|
+
setElements([...elements, newElement.trim()]);
|
|
35
|
+
setWarnEmpty(false);
|
|
36
|
+
setNewElement('');
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="edit-list-container">
|
|
42
|
+
<div className="el-group">
|
|
43
|
+
<input
|
|
44
|
+
type="text"
|
|
45
|
+
className="form-control"
|
|
46
|
+
placeholder={placeholder}
|
|
47
|
+
value={newElement}
|
|
48
|
+
onChange={(e) => setNewElement(e.target.value)}
|
|
49
|
+
/>
|
|
50
|
+
<Button
|
|
51
|
+
className="edit-list-add"
|
|
52
|
+
primary={true}
|
|
53
|
+
size="small"
|
|
54
|
+
label="Add"
|
|
55
|
+
onClick={addElement}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
{warnEmpty && <small className="warning-text">Your input was empty</small>}
|
|
59
|
+
<div className="edit-list-items">
|
|
60
|
+
{elements.map((element, index) => (
|
|
61
|
+
<div key={index} className="edit-list-group-item">
|
|
62
|
+
{element}
|
|
63
|
+
<Button
|
|
64
|
+
className="edit-list-remove"
|
|
65
|
+
nobg={true}
|
|
66
|
+
size="xsmall"
|
|
67
|
+
icon="fa fa-times"
|
|
68
|
+
label="Remove"
|
|
69
|
+
color="red"
|
|
70
|
+
onClick={() => removeElement(index)}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button } from '@openneuro/components/button';
|
|
3
|
+
import '../scss/user-meta-blocks.scss';
|
|
4
|
+
|
|
5
|
+
interface EditStringProps {
|
|
6
|
+
value?: string;
|
|
7
|
+
setValue: (value: string) => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
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);
|
|
18
|
+
|
|
19
|
+
const handleSave = (): void => {
|
|
20
|
+
if (!currentValue.trim()) {
|
|
21
|
+
setWarnEmpty(true);
|
|
22
|
+
} else {
|
|
23
|
+
setWarnEmpty(false);
|
|
24
|
+
setValue(currentValue.trim());
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="edit-string-container">
|
|
30
|
+
<div className="edit-string-group">
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
className="form-control"
|
|
34
|
+
placeholder={placeholder}
|
|
35
|
+
value={currentValue}
|
|
36
|
+
onChange={(e) => setCurrentValue(e.target.value)}
|
|
37
|
+
/>
|
|
38
|
+
<Button
|
|
39
|
+
className="edit-string-save"
|
|
40
|
+
primary={true}
|
|
41
|
+
size="small"
|
|
42
|
+
label="Save"
|
|
43
|
+
onClick={handleSave}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
{warnEmpty && <small className="warning-text">The input cannot be empty</small>}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|