-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into user-update
- Loading branch information
Showing
25 changed files
with
711 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { createContext } from "react"; | ||
|
||
import { Role } from "@kiesraad/api"; | ||
|
||
export type UserType = "fullname" | "anonymous"; | ||
|
||
export interface CreateUser { | ||
role?: Role; | ||
type?: UserType; | ||
username?: string; | ||
fullname?: string; | ||
password?: string; | ||
} | ||
|
||
export interface IUserCreateContext { | ||
user: CreateUser; | ||
updateUser: (user: CreateUser) => void; | ||
} | ||
|
||
export const UserCreateContext = createContext<IUserCreateContext | undefined>(undefined); |
17 changes: 17 additions & 0 deletions
17
frontend/app/module/users/create/UserCreateContextProvider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import * as React from "react"; | ||
import { useState } from "react"; | ||
|
||
import { User } from "@kiesraad/api"; | ||
|
||
import { IUserCreateContext, UserCreateContext } from "./UserCreateContext"; | ||
|
||
export function UserCreateContextProvider({ children }: { children: React.ReactNode }) { | ||
const [user, setUser] = useState<Partial<User>>({}); | ||
|
||
function updateUser(update: Partial<User>) { | ||
setUser((user) => ({ ...user, ...update })); | ||
} | ||
|
||
const context: IUserCreateContext = { user, updateUser }; | ||
return <UserCreateContext.Provider value={context}>{children}</UserCreateContext.Provider>; | ||
} |
113 changes: 113 additions & 0 deletions
113
frontend/app/module/users/create/UserCreateDetailsPage.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { screen } from "@testing-library/react"; | ||
import { userEvent } from "@testing-library/user-event"; | ||
import { describe, expect, test, vi } from "vitest"; | ||
|
||
import { UserCreateDetailsPage } from "app/module/users"; | ||
|
||
import { render } from "@kiesraad/test"; | ||
|
||
import { IUserCreateContext, UserCreateContext } from "./UserCreateContext"; | ||
|
||
const navigate = vi.fn(); | ||
|
||
vi.mock(import("react-router"), async (importOriginal) => ({ | ||
...(await importOriginal()), | ||
Navigate: ({ to }) => { | ||
navigate(to); | ||
return null; | ||
}, | ||
useNavigate: () => navigate, | ||
})); | ||
|
||
function renderPage(context: Partial<IUserCreateContext>) { | ||
return render( | ||
<UserCreateContext.Provider value={context as IUserCreateContext}> | ||
<UserCreateDetailsPage /> | ||
</UserCreateContext.Provider>, | ||
); | ||
} | ||
|
||
describe("UserCreateDetailsPage", () => { | ||
test("Redirect to start when no role in context", () => { | ||
renderPage({ user: {} }); | ||
expect(navigate).toHaveBeenCalledExactlyOnceWith("/users/create"); | ||
}); | ||
|
||
test("Show validation errors", async () => { | ||
const updateUser = vi.fn(); | ||
renderPage({ user: { role: "coordinator", type: "fullname" }, updateUser }); | ||
|
||
const user = userEvent.setup(); | ||
|
||
const submit = await screen.findByRole("button", { name: "Opslaan" }); | ||
await user.click(submit); | ||
|
||
const username = await screen.findByLabelText("Gebruikersnaam"); | ||
expect(username).toBeInvalid(); | ||
expect(username).toHaveAccessibleErrorMessage("Dit veld mag niet leeg zijn"); | ||
|
||
const fullname = await screen.findByLabelText("Volledige naam"); | ||
expect(fullname).toBeInvalid(); | ||
expect(fullname).toHaveAccessibleErrorMessage("Dit veld mag niet leeg zijn"); | ||
|
||
const password = await screen.findByLabelText("Tijdelijk wachtwoord"); | ||
expect(password).toBeInvalid(); | ||
expect(password).toHaveAccessibleErrorMessage("Dit veld mag niet leeg zijn"); | ||
|
||
await user.type(password, "mand"); | ||
await user.click(submit); | ||
|
||
expect(password).toBeInvalid(); | ||
expect(password).toHaveAccessibleErrorMessage("Dit wachtwoord is niet lang genoeg. Gebruik minimaal 12 karakters"); | ||
}); | ||
|
||
test("Continue after filling in fullname fields", async () => { | ||
const updateUser = vi.fn(); | ||
renderPage({ user: { role: "coordinator", type: "fullname" }, updateUser }); | ||
|
||
const user = userEvent.setup(); | ||
|
||
expect(await screen.findByRole("heading", { level: 1, name: "Coördinator toevoegen" })).toBeInTheDocument(); | ||
|
||
expect(await screen.findByLabelText("Gebruikersnaam")).toHaveValue(""); | ||
expect(await screen.findByLabelText("Volledige naam")).toHaveValue(""); | ||
expect(await screen.findByLabelText("Tijdelijk wachtwoord")).toHaveValue(""); | ||
|
||
await user.type(await screen.findByLabelText("Gebruikersnaam"), "NieuweGebruiker"); | ||
await user.type(await screen.findByLabelText("Volledige naam"), "Nieuwe Gebruiker"); | ||
await user.type(await screen.findByLabelText("Tijdelijk wachtwoord"), "Wachtwoord12"); | ||
|
||
const submit = await screen.findByRole("button", { name: "Opslaan" }); | ||
await user.click(submit); | ||
|
||
expect(updateUser).toHaveBeenCalledExactlyOnceWith({ | ||
username: "NieuweGebruiker", | ||
fullname: "Nieuwe Gebruiker", | ||
password: "Wachtwoord12", | ||
}); | ||
expect(navigate).toHaveBeenCalledExactlyOnceWith("/users"); | ||
}); | ||
|
||
test("Continue after filling in anonymous fields", async () => { | ||
const updateUser = vi.fn(); | ||
renderPage({ user: { role: "typist", type: "anonymous" }, updateUser }); | ||
|
||
const user = userEvent.setup(); | ||
|
||
expect(await screen.findByLabelText("Gebruikersnaam")).toHaveValue(""); | ||
expect(screen.queryByLabelText("Volledige naam")).not.toBeInTheDocument(); | ||
expect(await screen.findByLabelText("Tijdelijk wachtwoord")).toHaveValue(""); | ||
|
||
await user.type(await screen.findByLabelText("Gebruikersnaam"), "NieuweGebruiker"); | ||
await user.type(await screen.findByLabelText("Tijdelijk wachtwoord"), "Wachtwoord12"); | ||
|
||
const submit = await screen.findByRole("button", { name: "Opslaan" }); | ||
await user.click(submit); | ||
|
||
expect(updateUser).toHaveBeenCalledExactlyOnceWith({ | ||
username: "NieuweGebruiker", | ||
password: "Wachtwoord12", | ||
}); | ||
expect(navigate).toHaveBeenCalledExactlyOnceWith("/users"); | ||
}); | ||
}); |
119 changes: 119 additions & 0 deletions
119
frontend/app/module/users/create/UserCreateDetailsPage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { FormEvent, useState } from "react"; | ||
import { Navigate, useNavigate } from "react-router"; | ||
|
||
import { t } from "@kiesraad/i18n"; | ||
import { Button, Form, FormLayout, InputField, PageTitle } from "@kiesraad/ui"; | ||
|
||
import { useUserCreateContext } from "./useUserCreateContext"; | ||
|
||
interface UserDetails { | ||
username: string; | ||
fullname?: string; | ||
password: string; | ||
} | ||
|
||
type ValidationErrors = Partial<UserDetails>; | ||
|
||
const MIN_PASSWORD_LENGTH = 12; | ||
|
||
export function UserCreateDetailsPage() { | ||
const navigate = useNavigate(); | ||
const { user, updateUser } = useUserCreateContext(); | ||
const [validationErrors, setValidationErrors] = useState<ValidationErrors | null>(null); | ||
|
||
if (!user.role || !user.type) { | ||
return <Navigate to="/users/create" />; | ||
} | ||
|
||
function handleSubmit(event: FormEvent<HTMLFormElement>) { | ||
event.preventDefault(); | ||
const formData = new FormData(event.currentTarget); | ||
|
||
const details: UserDetails = { | ||
username: (formData.get("username") as string).trim(), | ||
fullname: (formData.get("fullname") as string | undefined)?.trim(), | ||
password: (formData.get("password") as string).trim(), | ||
}; | ||
|
||
if (!validate(details)) { | ||
return; | ||
} | ||
|
||
updateUser(details); | ||
|
||
void navigate(`/users`); | ||
} | ||
|
||
function validate(details: UserDetails): boolean { | ||
const errors: ValidationErrors = {}; | ||
|
||
const required = t("form_errors.FORM_VALIDATION_RESULT_REQUIRED"); | ||
|
||
if (details.username.length === 0) { | ||
errors.username = required; | ||
} | ||
|
||
if (details.fullname !== undefined && details.fullname.length === 0) { | ||
errors.fullname = required; | ||
} | ||
|
||
if (details.password.length === 0) { | ||
errors.password = required; | ||
} else if (details.password.length < MIN_PASSWORD_LENGTH) { | ||
errors.password = t("users.temporary_password_error_min_length", { min_length: MIN_PASSWORD_LENGTH }); | ||
} | ||
|
||
const isValid = Object.keys(errors).length === 0; | ||
setValidationErrors(isValid ? null : errors); | ||
return isValid; | ||
} | ||
|
||
return ( | ||
<> | ||
<PageTitle title={`${t("users.add")} - Abacus`} /> | ||
<header> | ||
<section> | ||
<h1>{t("users.add_role", { role: t(user.role) })}</h1> | ||
</section> | ||
</header> | ||
<main> | ||
<Form onSubmit={handleSubmit}> | ||
<FormLayout width="medium"> | ||
<FormLayout.Section title={t("users.details_title")}> | ||
<InputField | ||
id="username" | ||
name="username" | ||
label={t("users.username")} | ||
hint={t("users.username_hint")} | ||
error={validationErrors?.username} | ||
/> | ||
|
||
{user.type === "fullname" && ( | ||
<InputField | ||
id="fullname" | ||
name="fullname" | ||
label={t("users.fullname")} | ||
hint={t("users.fullname_hint")} | ||
error={validationErrors?.fullname} | ||
/> | ||
)} | ||
|
||
<InputField | ||
id="password" | ||
name="password" | ||
label={t("users.temporary_password")} | ||
hint={t("users.temporary_password_hint", { min_length: MIN_PASSWORD_LENGTH })} | ||
error={validationErrors?.password} | ||
/> | ||
</FormLayout.Section> | ||
</FormLayout> | ||
<FormLayout.Controls> | ||
<Button size="xl" type="submit"> | ||
{t("save")} | ||
</Button> | ||
</FormLayout.Controls> | ||
</Form> | ||
</main> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Outlet } from "react-router"; | ||
|
||
import { UserCreateContextProvider } from "./UserCreateContextProvider"; | ||
|
||
export function UserCreateLayout() { | ||
return ( | ||
<UserCreateContextProvider> | ||
<Outlet /> | ||
</UserCreateContextProvider> | ||
); | ||
} |
Oops, something went wrong.