Introduce routes to navigate as logged-in user
This also enables navigating to an empty user-management page if logged-in as an editor.
This commit is contained in:
parent
9944f8a38b
commit
ee263f52b1
|
@ -50,15 +50,15 @@ function App(): React.ReactElement {
|
|||
|
||||
return (
|
||||
<Switch>
|
||||
<PrivateRoute exact path={"/"}>
|
||||
{jwt && <Main userRole={jwt.role} userRowId={jwt.person_row_id} />}
|
||||
</PrivateRoute>
|
||||
<NotLoggedInOnlyRoute path={"/login"}>
|
||||
<SignIn />
|
||||
</NotLoggedInOnlyRoute>
|
||||
<NotLoggedInOnlyRoute path={"/signup"}>
|
||||
<SignUp />
|
||||
</NotLoggedInOnlyRoute>
|
||||
<PrivateRoute>
|
||||
{jwt && <Main userRole={jwt.role} userRowId={jwt.person_row_id} />}
|
||||
</PrivateRoute>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@ import { Container } from "@material-ui/core";
|
|||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import QuestionAnswersList from "./QuestionAnswerList";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { PersonRoutes } from "./Main";
|
||||
import { MenuOption } from "./MainMenu";
|
||||
import QuestionAnswerIcon from "@material-ui/icons/QuestionAnswer";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
|
@ -11,6 +15,18 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
interface CandidateRoutes extends PersonRoutes {
|
||||
question: MenuOption;
|
||||
}
|
||||
|
||||
export const candidateRoutes: CandidateRoutes = {
|
||||
question: {
|
||||
title: "Fragen beantworten",
|
||||
path: "/",
|
||||
icon: <QuestionAnswerIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
interface HomePageCandidateProps {
|
||||
personRowId: number;
|
||||
}
|
||||
|
@ -22,7 +38,11 @@ export function HomePageCandidate(
|
|||
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<QuestionAnswersList personRowId={props.personRowId} />
|
||||
<Switch>
|
||||
<Route exact path={candidateRoutes.question.path}>
|
||||
<QuestionAnswersList personRowId={props.personRowId} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,11 @@ import CategoryList from "./CategoryList";
|
|||
import { Copyright } from "./Copyright";
|
||||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import QuestionAnswerIcon from "@material-ui/icons/QuestionAnswer";
|
||||
import PeopleIcon from "@material-ui/icons/People";
|
||||
import { MenuOption } from "./MainMenu";
|
||||
import { PersonRoutes } from "./Main";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
|
@ -13,13 +18,38 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
interface EditorRoutes extends PersonRoutes {
|
||||
question: MenuOption;
|
||||
userManagement: MenuOption;
|
||||
}
|
||||
|
||||
export const editorRoutes: EditorRoutes = {
|
||||
question: {
|
||||
title: "Fragen bearbeiten",
|
||||
path: "/",
|
||||
icon: <QuestionAnswerIcon />,
|
||||
},
|
||||
userManagement: {
|
||||
title: "Benutzer verwalten",
|
||||
path: "/benutzer",
|
||||
icon: <PeopleIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export function HomePageEditor(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<QuestionList />
|
||||
<CategoryList />
|
||||
<Switch>
|
||||
<Route exact path={editorRoutes.question.path}>
|
||||
<QuestionList />
|
||||
<CategoryList />
|
||||
</Route>
|
||||
<Route path={editorRoutes.userManagement.path}>
|
||||
<div />
|
||||
</Route>
|
||||
</Switch>
|
||||
<Copyright />
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { Container } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { PersonRoutes } from "./Main";
|
||||
import { MenuOption } from "./MainMenu";
|
||||
import HomeIcon from "@material-ui/icons/Home";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
|
@ -10,12 +14,28 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
interface UserRoutes extends PersonRoutes {
|
||||
home: MenuOption;
|
||||
}
|
||||
|
||||
export const userRoutes: UserRoutes = {
|
||||
home: {
|
||||
title: "Home",
|
||||
path: "/",
|
||||
icon: <HomeIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export function HomePageUser(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
Sorry, für dich gibt es hier leider nichts zu sehen...
|
||||
<Switch>
|
||||
<Route exact path={userRoutes.home.path}>
|
||||
Sorry, für dich gibt es hier leider nichts zu sehen...
|
||||
</Route>
|
||||
</Switch>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MockedProvider } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import Main from "./Main";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import { JwtPayload } from "../jwt/jwt";
|
||||
import { queryAllMenuIconButtons } from "../integration-tests/test-helper";
|
||||
|
||||
function renderMainPage(jwt: JwtPayload) {
|
||||
render(
|
||||
|
@ -27,13 +28,14 @@ const baseJwt: JwtPayload = {
|
|||
person_row_id: 3,
|
||||
};
|
||||
|
||||
describe("The main page", () => {
|
||||
test("displays the editors page if an editor is logged in", () => {
|
||||
const jwt: JwtPayload = {
|
||||
...baseJwt,
|
||||
role: "candymat_editor",
|
||||
person_row_id: 1,
|
||||
};
|
||||
describe("As an editor, the main page", () => {
|
||||
const jwt: JwtPayload = {
|
||||
...baseJwt,
|
||||
role: "candymat_editor",
|
||||
person_row_id: 1,
|
||||
};
|
||||
|
||||
test("displays the editor's home page", () => {
|
||||
renderMainPage(jwt);
|
||||
|
||||
// it renders question and category lists
|
||||
|
@ -43,7 +45,24 @@ describe("The main page", () => {
|
|||
expect(categoryListHeadline).not.toBeNull();
|
||||
});
|
||||
|
||||
test("displays the candidates page if a candidate is logged in", () => {
|
||||
test("has a menu with two entries", async () => {
|
||||
renderMainPage(jwt);
|
||||
|
||||
const menuButton = queryAllMenuIconButtons();
|
||||
expect(menuButton).toHaveLength(1);
|
||||
fireEvent.click(menuButton[0]);
|
||||
|
||||
// renders the two menu entries for an editor
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryAllByRole("button", { name: /fragen|benutzer/i })
|
||||
).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("As a candidate, the main page", () => {
|
||||
test("displays the candidate's home page ", () => {
|
||||
const jwt: JwtPayload = {
|
||||
...baseJwt,
|
||||
role: "candymat_candidate",
|
||||
|
@ -51,13 +70,15 @@ describe("The main page", () => {
|
|||
};
|
||||
renderMainPage(jwt);
|
||||
|
||||
const questionListHeadline = screen.queryByText(/Fragen/);
|
||||
const questionListHeadline = screen.queryAllByText(/Fragen/);
|
||||
const categoryListHeadline = screen.queryByText(/Kategorien/);
|
||||
expect(questionListHeadline).not.toBeNull();
|
||||
expect(questionListHeadline.length).toBeGreaterThan(0);
|
||||
expect(categoryListHeadline).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays the user page if an normal user is logged in", () => {
|
||||
describe("As a simple user, the main page", () => {
|
||||
test("displays the user's home page.", () => {
|
||||
const jwt: JwtPayload = {
|
||||
...baseJwt,
|
||||
role: "candymat_person",
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import CustomAppBar from "./CustomAppBar";
|
||||
import React, { ReactElement } from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { HomePageEditor } from "./HomePageEditor";
|
||||
import { editorRoutes, HomePageEditor } from "./HomePageEditor";
|
||||
import { UserRole } from "../jwt/jwt";
|
||||
import { HomePageCandidate } from "./HomePageCandidate";
|
||||
import { HomePageUser } from "./HomePageUser";
|
||||
import { mainMenuWidth, MainMenu, mainMenuOpen, MenuOption } from "./MainMenu";
|
||||
import { candidateRoutes, HomePageCandidate } from "./HomePageCandidate";
|
||||
import { HomePageUser, userRoutes } from "./HomePageUser";
|
||||
import { MainMenu, mainMenuOpen, mainMenuWidth, MenuOption } from "./MainMenu";
|
||||
import clsx from "clsx";
|
||||
import QuestionAnswerIcon from "@material-ui/icons/QuestionAnswer";
|
||||
import PeopleIcon from "@material-ui/icons/People";
|
||||
import { useReactiveVar } from "@apollo/client";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
|
@ -38,6 +36,10 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export interface PersonRoutes {
|
||||
[id: string]: MenuOption;
|
||||
}
|
||||
|
||||
interface MainProps {
|
||||
userRole: UserRole;
|
||||
userRowId: number;
|
||||
|
@ -61,22 +63,11 @@ function Main(props: MainProps): ReactElement {
|
|||
const getMenuOptions = (): Array<MenuOption> => {
|
||||
switch (props.userRole) {
|
||||
case "candymat_editor":
|
||||
return [
|
||||
{
|
||||
title: "Fragen bearbeiten",
|
||||
path: "/fragen",
|
||||
icon: <QuestionAnswerIcon />,
|
||||
},
|
||||
{
|
||||
title: "Benutzer verwalten",
|
||||
path: "/benutzer",
|
||||
icon: <PeopleIcon />,
|
||||
},
|
||||
];
|
||||
return Object.values(editorRoutes);
|
||||
case "candymat_candidate":
|
||||
return [];
|
||||
return Object.values(candidateRoutes);
|
||||
case "candymat_person":
|
||||
return [];
|
||||
return Object.values(userRoutes);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import ListItem from "@material-ui/core/ListItem";
|
|||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import { makeVar, useReactiveVar } from "@apollo/client";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
export const mainMenuWidth = 240;
|
||||
|
||||
|
@ -46,6 +47,7 @@ interface MainMenuProps {
|
|||
|
||||
export function MainMenu(props: MainMenuProps): ReactElement {
|
||||
const classes = useStyles();
|
||||
const history = useHistory();
|
||||
const open = useReactiveVar(mainMenuOpen);
|
||||
|
||||
const handleClose = () => {
|
||||
|
@ -71,7 +73,11 @@ export function MainMenu(props: MainMenuProps): ReactElement {
|
|||
<Divider />
|
||||
<List>
|
||||
{props.options.map((option) => (
|
||||
<ListItem button key={option.title}>
|
||||
<ListItem
|
||||
button
|
||||
key={option.title}
|
||||
onClick={() => history.push(option.path)}
|
||||
>
|
||||
<ListItemIcon>{option.icon}</ListItemIcon>
|
||||
<ListItemText primary={option.title} />
|
||||
</ListItem>
|
||||
|
|
|
@ -20,6 +20,34 @@ import {
|
|||
queryAllEditIconButtons,
|
||||
} from "./test-helper";
|
||||
|
||||
function renderCategoryList(additionalMocks?: Array<MockedResponse>) {
|
||||
const initialMocks = [...getAllCategoriesMock, ...getCategoryByIdMock];
|
||||
const allMocks = additionalMocks
|
||||
? [...initialMocks, ...additionalMocks]
|
||||
: initialMocks;
|
||||
return render(
|
||||
<MockedProvider mocks={allMocks}>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<CategoryList />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const waitForInitialCategoriesToRender = async (): Promise<
|
||||
Array<HTMLElement>
|
||||
> => {
|
||||
const numberOfCategoriesInMockQuery = categoryNodesMock.length;
|
||||
let categoryCards: Array<HTMLElement> = [];
|
||||
await waitFor(() => {
|
||||
categoryCards = screen.queryAllByRole("button", { name: /Category [1-2]/ });
|
||||
expect(categoryCards.length).toEqual(numberOfCategoriesInMockQuery);
|
||||
});
|
||||
return categoryCards;
|
||||
};
|
||||
|
||||
describe("The CategoryList", () => {
|
||||
test("displays the existing categories, but not the details of it", async () => {
|
||||
renderCategoryList();
|
||||
|
@ -134,31 +162,3 @@ describe("The CategoryList", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderCategoryList(additionalMocks?: Array<MockedResponse>) {
|
||||
const initialMocks = [...getAllCategoriesMock, ...getCategoryByIdMock];
|
||||
const allMocks = additionalMocks
|
||||
? [...initialMocks, ...additionalMocks]
|
||||
: initialMocks;
|
||||
return render(
|
||||
<MockedProvider mocks={allMocks}>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<CategoryList />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const waitForInitialCategoriesToRender = async (): Promise<
|
||||
Array<HTMLElement>
|
||||
> => {
|
||||
const numberOfCategoriesInMockQuery = categoryNodesMock.length;
|
||||
let categoryCards: Array<HTMLElement> = [];
|
||||
await waitFor(() => {
|
||||
categoryCards = screen.queryAllByRole("button", { name: /Category [1-2]/ });
|
||||
expect(categoryCards.length).toEqual(numberOfCategoriesInMockQuery);
|
||||
});
|
||||
return categoryCards;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
CandidatePosition,
|
||||
getIconForPosition,
|
||||
} from "../components/CandidatePositionLegend";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
|
||||
const memoizedGetIconPath = (icon: JSX.Element) => {
|
||||
const cache: { path?: string } = {};
|
||||
|
@ -32,6 +33,7 @@ const memoizedGetIconPath = (icon: JSX.Element) => {
|
|||
};
|
||||
|
||||
const getEditIconPath = memoizedGetIconPath(<EditIcon />);
|
||||
const getMenuIconPath = memoizedGetIconPath(<MenuIcon />);
|
||||
const getDeleteIconPath = memoizedGetIconPath(<DeleteIcon />);
|
||||
const getAddIconPath = memoizedGetIconPath(<AddIcon />);
|
||||
export const getPositivePositionPath = memoizedGetIconPath(
|
||||
|
@ -47,6 +49,7 @@ export const getSkippedPositionPath = memoizedGetIconPath(
|
|||
getIconForPosition(CandidatePosition.skipped)
|
||||
);
|
||||
|
||||
// sorry, I found no better way to find a specific icon button...
|
||||
export const queryAllIconButtons = (
|
||||
iconPath: string,
|
||||
container?: HTMLElement
|
||||
|
@ -60,47 +63,21 @@ export const queryAllIconButtons = (
|
|||
);
|
||||
};
|
||||
|
||||
// sorry, I found no better way to find a specific icon button...
|
||||
export const queryAllEditIconButtons = (
|
||||
container?: HTMLElement
|
||||
): Array<HTMLElement> => {
|
||||
return (container
|
||||
? queryAllByRole(container, "button")
|
||||
: screen.queryAllByRole("button")
|
||||
).filter(
|
||||
(button) =>
|
||||
button.innerHTML.includes("svg") &&
|
||||
button.innerHTML.includes(getEditIconPath())
|
||||
);
|
||||
};
|
||||
): Array<HTMLElement> => queryAllIconButtons(getEditIconPath(), container);
|
||||
|
||||
// sorry, I found no better way to find a specific icon button...
|
||||
const queryAllDeleteIconButtons = (
|
||||
container?: HTMLElement
|
||||
): Array<HTMLElement> => {
|
||||
return (container
|
||||
? queryAllByRole(container, "button")
|
||||
: screen.queryAllByRole("button")
|
||||
).filter(
|
||||
(button) =>
|
||||
button.innerHTML.includes("svg") &&
|
||||
button.innerHTML.includes(getDeleteIconPath())
|
||||
);
|
||||
};
|
||||
): Array<HTMLElement> => queryAllIconButtons(getDeleteIconPath(), container);
|
||||
|
||||
// sorry, I found no better way to find a specific icon button...
|
||||
export const queryAllAddIconButtons = (
|
||||
container?: HTMLElement
|
||||
): Array<HTMLElement> => {
|
||||
return (container
|
||||
? queryAllByRole(container, "button")
|
||||
: screen.queryAllByRole("button")
|
||||
).filter(
|
||||
(button) =>
|
||||
button.innerHTML.includes("svg") &&
|
||||
button.innerHTML.includes(getAddIconPath())
|
||||
);
|
||||
};
|
||||
): Array<HTMLElement> => queryAllIconButtons(getAddIconPath(), container);
|
||||
|
||||
export const queryAllMenuIconButtons = (
|
||||
container?: HTMLElement
|
||||
): Array<HTMLElement> => queryAllIconButtons(getMenuIconPath(), container);
|
||||
|
||||
export const expandAccordionAndGetIconButtons = async (
|
||||
accordion: HTMLElement
|
||||
|
|
Loading…
Reference in a new issue