Skip to content

Commit 794f576

Browse files
authored
Merge pull request #3272 from OpenNeuroOrg/userSchema
update user schema to include new fields and ability to updateUser
2 parents c118f15 + a4ef889 commit 794f576

File tree

12 files changed

+413
-76
lines changed

12 files changed

+413
-76
lines changed

packages/openneuro-app/src/scripts/routes.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { ImportDataset } from "./pages/import-dataset"
1818
import { DatasetMetadata } from "./pages/metadata/dataset-metadata"
1919
import { TermsPage } from "./pages/terms"
2020
import { UserQuery } from "./users/user-query"
21+
import LoggedIn from "../scripts/authentication/logged-in"
22+
import LoggedOut from "../scripts/authentication/logged-out"
23+
import FourOThreePage from "./errors/403page"
2124

2225
const AppRoutes: React.VoidFunctionComponent = () => (
2326
<Routes>
@@ -34,7 +37,19 @@ const AppRoutes: React.VoidFunctionComponent = () => (
3437
<Route path="/import" element={<ImportDataset />} />
3538
<Route path="/metadata" element={<DatasetMetadata />} />
3639
<Route path="/public" element={<Navigate to="/search" replace />} />
37-
<Route path="/user/:orcid/*" element={<UserQuery />} />
40+
<Route
41+
path="/user/:orcid/*"
42+
element={
43+
<>
44+
<LoggedIn>
45+
<UserQuery />
46+
</LoggedIn>
47+
<LoggedOut>
48+
<FourOThreePage />
49+
</LoggedOut>
50+
</>
51+
}
52+
/>
3853
<Route
3954
path="/saved"
4055
element={<Navigate to="/search?bookmarks" replace />}
Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react"
2+
import { MockedProvider } from "@apollo/client/testing"
23
import {
34
fireEvent,
45
render,
@@ -7,8 +8,10 @@ import {
78
within,
89
} from "@testing-library/react"
910
import { UserAccountView } from "../user-account-view"
11+
import { GET_USER_BY_ORCID, UPDATE_USER } from "../user-query"
1012

1113
const baseUser = {
14+
id: "1",
1215
name: "John Doe",
1316
1417
orcid: "0000-0001-2345-6789",
@@ -18,61 +21,132 @@ const baseUser = {
1821
github: "johndoe",
1922
}
2023

24+
const mocks = [
25+
{
26+
request: {
27+
query: GET_USER_BY_ORCID,
28+
variables: { userId: baseUser.id },
29+
},
30+
result: {
31+
data: {
32+
user: baseUser,
33+
},
34+
},
35+
},
36+
{
37+
request: {
38+
query: UPDATE_USER,
39+
variables: {
40+
id: baseUser.id,
41+
location: "Marin, CA",
42+
links: ["https://newlink.com"],
43+
institution: "New University",
44+
},
45+
},
46+
result: {
47+
data: {
48+
updateUser: {
49+
id: baseUser.id,
50+
location: "Marin, CA",
51+
links: ["https://newlink.com"],
52+
institution: "New University",
53+
},
54+
},
55+
},
56+
},
57+
]
58+
2159
describe("<UserAccountView />", () => {
2260
it("should render the user details correctly", () => {
23-
render(<UserAccountView user={baseUser} />)
24-
25-
// Check if user details are rendered
61+
render(
62+
<MockedProvider mocks={mocks} addTypename={false}>
63+
<UserAccountView user={baseUser} />
64+
</MockedProvider>,
65+
)
2666
expect(screen.getByText("Name:")).toBeInTheDocument()
2767
expect(screen.getByText("John Doe")).toBeInTheDocument()
2868
expect(screen.getByText("Email:")).toBeInTheDocument()
2969
expect(screen.getByText("[email protected]")).toBeInTheDocument()
3070
expect(screen.getByText("ORCID:")).toBeInTheDocument()
3171
expect(screen.getByText("0000-0001-2345-6789")).toBeInTheDocument()
72+
expect(screen.getByText("GitHub:")).toBeInTheDocument()
3273
expect(screen.getByText("johndoe")).toBeInTheDocument()
3374
})
3475

35-
it("should render links with EditableContent", async () => {
36-
render(<UserAccountView user={baseUser} />)
37-
const institutionSection = within(
38-
screen.getByText("Institution").closest(".user-meta-block"),
76+
it("should render location with EditableContent", async () => {
77+
render(
78+
<MockedProvider mocks={mocks} addTypename={false}>
79+
<UserAccountView user={baseUser} />
80+
</MockedProvider>,
3981
)
82+
const locationSection = within(screen.getByTestId("location-section"))
83+
expect(screen.getByText("Location")).toBeInTheDocument()
84+
const editButton = locationSection.getByText("Edit")
85+
fireEvent.click(editButton)
86+
const textbox = locationSection.getByRole("textbox")
87+
fireEvent.change(textbox, { target: { value: "Marin, CA" } })
88+
const saveButton = locationSection.getByText("Save")
89+
fireEvent.click(saveButton)
90+
await waitFor(() => {
91+
expect(locationSection.getByText("Marin, CA")).toBeInTheDocument()
92+
})
93+
})
94+
95+
it("should render institution with EditableContent", async () => {
96+
render(
97+
<MockedProvider mocks={mocks} addTypename={false}>
98+
<UserAccountView user={baseUser} />
99+
</MockedProvider>,
100+
)
101+
const institutionSection = within(screen.getByTestId("institution-section"))
40102
expect(screen.getByText("Institution")).toBeInTheDocument()
41103
const editButton = institutionSection.getByText("Edit")
42104
fireEvent.click(editButton)
43105
const textbox = institutionSection.getByRole("textbox")
44106
fireEvent.change(textbox, { target: { value: "New University" } })
45107
const saveButton = institutionSection.getByText("Save")
46-
const closeButton = institutionSection.getByText("Close")
47108
fireEvent.click(saveButton)
48-
fireEvent.click(closeButton)
49-
// Add debug step
50-
await waitFor(() => screen.debug())
51-
// Use a flexible matcher to check for text
52-
await waitFor(() =>
109+
await waitFor(() => {
53110
expect(institutionSection.getByText("New University")).toBeInTheDocument()
54-
)
111+
})
55112
})
56113

57-
it("should render location with EditableContent", async () => {
58-
render(<UserAccountView user={baseUser} />)
59-
const locationSection = within(
60-
screen.getByText("Location").closest(".user-meta-block"),
114+
it("should render links with EditableContent and validation", async () => {
115+
render(
116+
<MockedProvider mocks={mocks} addTypename={false}>
117+
<UserAccountView user={baseUser} />
118+
</MockedProvider>,
61119
)
62-
expect(screen.getByText("Location")).toBeInTheDocument()
63-
const editButton = locationSection.getByText("Edit")
120+
const linksSection = within(screen.getByTestId("links-section"))
121+
expect(screen.getByText("Links")).toBeInTheDocument()
122+
const editButton = linksSection.getByText("Edit")
64123
fireEvent.click(editButton)
65-
const textbox = locationSection.getByRole("textbox")
66-
fireEvent.change(textbox, { target: { value: "Marin, CA" } })
67-
const saveButton = locationSection.getByText("Save")
68-
const closeButton = locationSection.getByText("Close")
124+
const textbox = linksSection.getByRole("textbox")
125+
fireEvent.change(textbox, { target: { value: "https://newlink.com" } })
126+
const saveButton = linksSection.getByText("Add")
69127
fireEvent.click(saveButton)
70-
fireEvent.click(closeButton)
71-
// Add debug step
72-
await waitFor(() => screen.debug())
73-
// Use a flexible matcher to check for text
74-
await waitFor(() =>
75-
expect(locationSection.getByText("Marin, CA")).toBeInTheDocument()
128+
await waitFor(() => {
129+
expect(linksSection.getByText("https://newlink.com")).toBeInTheDocument()
130+
})
131+
})
132+
133+
it("should show an error message when invalid URL is entered in links section", async () => {
134+
render(
135+
<MockedProvider mocks={mocks} addTypename={false}>
136+
<UserAccountView user={baseUser} />
137+
</MockedProvider>,
76138
)
139+
const linksSection = within(screen.getByTestId("links-section"))
140+
const editButton = linksSection.getByText("Edit")
141+
fireEvent.click(editButton)
142+
const textbox = linksSection.getByRole("textbox")
143+
fireEvent.change(textbox, { target: { value: "invalid-url" } })
144+
const saveButton = linksSection.getByText("Add")
145+
fireEvent.click(saveButton)
146+
await waitFor(() => {
147+
expect(
148+
linksSection.getByText("Invalid URL format. Please use a valid link."),
149+
).toBeInTheDocument()
150+
})
77151
})
78152
})

packages/openneuro-app/src/scripts/users/__tests__/user-routes.spec.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React from "react"
22
import { cleanup, render, screen } from "@testing-library/react"
33
import { MemoryRouter } from "react-router-dom"
4+
import { MockedProvider } from "@apollo/client/testing"
45
import { UserRoutes } from "../user-routes"
56
import type { User } from "../user-routes"
7+
import { UPDATE_USER } from "../user-query"
68

79
const defaultUser: User = {
810
id: "1",
@@ -16,11 +18,40 @@ const defaultUser: User = {
1618
links: [],
1719
}
1820

21+
const mocks = [
22+
{
23+
request: {
24+
query: UPDATE_USER,
25+
variables: {
26+
id: "1",
27+
name: "John Doe",
28+
location: "Unknown",
29+
github: "",
30+
institution: "Unknown Institution",
31+
32+
avatar: "https://dummyimage.com/200x200/000/fff",
33+
orcid: "0000-0000-0000-0000",
34+
links: [],
35+
},
36+
},
37+
result: {
38+
data: {
39+
updateUser: {
40+
id: "1",
41+
name: "John Doe",
42+
},
43+
},
44+
},
45+
},
46+
]
47+
1948
const renderWithRouter = (user: User, route: string, hasEdit: boolean) => {
2049
return render(
21-
<MemoryRouter initialEntries={[route]}>
22-
<UserRoutes user={user} hasEdit={hasEdit} />
23-
</MemoryRouter>,
50+
<MockedProvider mocks={mocks} addTypename={false}>
51+
<MemoryRouter initialEntries={[route]}>
52+
<UserRoutes user={user} hasEdit={hasEdit} />
53+
</MemoryRouter>
54+
</MockedProvider>,
2455
)
2556
}
2657

packages/openneuro-app/src/scripts/users/components/edit-list.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,54 @@ interface EditListProps {
66
placeholder?: string
77
elements?: string[]
88
setElements: (elements: string[]) => void
9+
validation?: RegExp
10+
validationMessage?: string
911
}
1012

1113
/**
1214
* EditList Component
1315
* Allows adding and removing strings from a list.
1416
*/
1517
export const EditList: React.FC<EditListProps> = (
16-
{ placeholder = "Enter item", elements = [], setElements },
18+
{
19+
placeholder = "Enter item",
20+
elements = [],
21+
setElements,
22+
validation,
23+
validationMessage,
24+
},
1725
) => {
1826
const [newElement, setNewElement] = useState<string>("")
1927
const [warnEmpty, setWarnEmpty] = useState<boolean>(false)
28+
const [warnValidation, setWarnValidation] = useState<string | null>(null)
2029

21-
/**
22-
* Remove an element from the list by index
23-
* @param index - The index of the element to remove
24-
*/
2530
const removeElement = (index: number): void => {
2631
setElements(elements.filter((_, i) => i !== index))
2732
}
2833

29-
/**
30-
* Add a new element to the list
31-
*/
34+
// Add a new element to the list
3235
const addElement = (): void => {
3336
if (!newElement.trim()) {
3437
setWarnEmpty(true)
38+
setWarnValidation(null)
39+
} else if (validation && !validation.test(newElement.trim())) {
40+
setWarnValidation(validationMessage || "Invalid input format")
41+
setWarnEmpty(false)
3542
} else {
3643
setElements([...elements, newElement.trim()])
3744
setWarnEmpty(false)
45+
setWarnValidation(null)
3846
setNewElement("")
3947
}
4048
}
4149

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+
}
56+
4257
return (
4358
<div className="edit-list-container">
4459
<div className="el-group">
@@ -48,6 +63,7 @@ export const EditList: React.FC<EditListProps> = (
4863
placeholder={placeholder}
4964
value={newElement}
5065
onChange={(e) => setNewElement(e.target.value)}
66+
onKeyDown={handleKeyDown}
5167
/>
5268
<Button
5369
className="edit-list-add"
@@ -62,6 +78,9 @@ export const EditList: React.FC<EditListProps> = (
6278
Your input was empty
6379
</small>
6480
)}
81+
{warnValidation && (
82+
<small className="warning-text">{warnValidation}</small>
83+
)}
6584
<div className="edit-list-items">
6685
{elements.map((element, index) => (
6786
<div key={index} className="edit-list-group-item">

0 commit comments

Comments
 (0)