@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.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.29.7",
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.7",
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": "a11e8c6dfe702377946a26e9d86234593465cd04"
77
+ "gitHead": "992fc9e3044c67f6d85757ecf252845cfda6e809"
78
78
  }
@@ -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
- export function isValidOrcid(orcid: string) {
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(
@@ -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
+ };