#20 Add UserManagement page
Connects to backend and gets all registered users by role. Enabled editors to see all registered users which wasn't possible.
This commit is contained in:
parent
765443ebc2
commit
e26a154518
|
@ -19,12 +19,14 @@ create table candymat_data_privat.person_account
|
|||
email character varying(320) not null unique check (email ~* '^.+@.+\..+$'),
|
||||
password_hash character varying(256) not null
|
||||
);
|
||||
|
||||
alter table candymat_data.person
|
||||
enable row level security;
|
||||
create policy update_person on candymat_data.person for update to candymat_person
|
||||
with check (row_id = nullif(current_setting('jwt.claims.person_row_id', true), '')::integer);
|
||||
create policy delete_person on candymat_data.person for delete to candymat_person
|
||||
using (row_id = nullif(current_setting('jwt.claims.person_row_id', true), '')::integer);
|
||||
|
||||
-- The following enables viewing candidates and editors information for every person.
|
||||
-- This may be changed to only enable registered (and verified) persons.
|
||||
create policy select_person_public
|
||||
|
@ -32,3 +34,10 @@ create policy select_person_public
|
|||
for select
|
||||
to candymat_anonymous, candymat_person -- maybe change to candymat_person only in the future
|
||||
using (role in ('candymat_editor', 'candymat_candidate'));
|
||||
|
||||
-- Editors can see all registered persons in order to elevate their privileges
|
||||
create policy select_person_editor
|
||||
on candymat_data.person
|
||||
for select
|
||||
to candymat_editor
|
||||
using (true);
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
GET_PERSONS_SORTED_BY_ROLE,
|
||||
GetPersonsSortedByRoleResponse,
|
||||
} from "./person";
|
||||
|
||||
export const getPersonsByRoleAllFilledData: GetPersonsSortedByRoleResponse = {
|
||||
editors: {
|
||||
nodes: [
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDFd",
|
||||
rowId: 1,
|
||||
firstName: "Erika",
|
||||
lastName: "Mustermann",
|
||||
role: "candymat_editor",
|
||||
__typename: "Person",
|
||||
},
|
||||
],
|
||||
__typename: "PeopleConnection",
|
||||
},
|
||||
candidates: {
|
||||
nodes: [
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDJd",
|
||||
rowId: 2,
|
||||
firstName: "Max",
|
||||
lastName: "Mustermann",
|
||||
role: "candymat_candidate",
|
||||
__typename: "Person",
|
||||
},
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDNd",
|
||||
rowId: 3,
|
||||
firstName: "Tricia",
|
||||
lastName: "McMillan",
|
||||
role: "candymat_candidate",
|
||||
__typename: "Person",
|
||||
},
|
||||
],
|
||||
__typename: "PeopleConnection",
|
||||
},
|
||||
users: {
|
||||
nodes: [
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDRd",
|
||||
rowId: 4,
|
||||
firstName: "Happy",
|
||||
lastName: "User",
|
||||
role: "candymat_person",
|
||||
__typename: "Person",
|
||||
},
|
||||
],
|
||||
__typename: "PeopleConnection",
|
||||
},
|
||||
};
|
||||
|
||||
export const getPersonsSortedByRoleAllFilledMock: Array<
|
||||
MockedResponse<GetPersonsSortedByRoleResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: GET_PERSONS_SORTED_BY_ROLE,
|
||||
},
|
||||
result: {
|
||||
data: getPersonsByRoleAllFilledData,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getPersonsByRoleNoCandidatesData: GetPersonsSortedByRoleResponse = {
|
||||
editors: {
|
||||
nodes: [
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDFd",
|
||||
rowId: 1,
|
||||
firstName: "Erika",
|
||||
lastName: "Mustermann",
|
||||
role: "candymat_editor",
|
||||
__typename: "Person",
|
||||
},
|
||||
],
|
||||
__typename: "PeopleConnection",
|
||||
},
|
||||
candidates: {
|
||||
nodes: [],
|
||||
__typename: "PeopleConnection",
|
||||
},
|
||||
users: {
|
||||
nodes: [
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDJd",
|
||||
rowId: 2,
|
||||
firstName: "Max",
|
||||
lastName: "Mustermann",
|
||||
role: "candymat_person",
|
||||
__typename: "Person",
|
||||
},
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDNd",
|
||||
rowId: 3,
|
||||
firstName: "Tricia",
|
||||
lastName: "McMillan",
|
||||
role: "candymat_person",
|
||||
__typename: "Person",
|
||||
},
|
||||
{
|
||||
id: "WyJwZW9wbGUiLDRd",
|
||||
rowId: 4,
|
||||
firstName: "Happy",
|
||||
lastName: "User",
|
||||
role: "candymat_person",
|
||||
__typename: "Person",
|
||||
},
|
||||
],
|
||||
__typename: "PeopleConnection",
|
||||
},
|
||||
};
|
||||
|
||||
export const getPersonsSortedByRoleNoCandidatesMock: Array<
|
||||
MockedResponse<GetPersonsSortedByRoleResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: GET_PERSONS_SORTED_BY_ROLE,
|
||||
},
|
||||
result: {
|
||||
data: getPersonsByRoleNoCandidatesData,
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,57 @@
|
|||
import { gql } from "@apollo/client";
|
||||
import { UserRole } from "../../jwt/jwt";
|
||||
|
||||
const BasicPersonFragment = gql`
|
||||
fragment BasicPersonFragment on Person {
|
||||
id
|
||||
rowId
|
||||
firstName
|
||||
lastName
|
||||
role
|
||||
}
|
||||
`;
|
||||
|
||||
export interface BasicPersonResponse {
|
||||
id: string;
|
||||
rowId: number;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
role: UserRole;
|
||||
__typename: "Person";
|
||||
}
|
||||
|
||||
export const GET_PERSONS_SORTED_BY_ROLE = gql`
|
||||
query AllPeople {
|
||||
editors: allPeople(condition: { role: CANDYMAT_EDITOR }) {
|
||||
nodes {
|
||||
...BasicPersonFragment
|
||||
}
|
||||
}
|
||||
candidates: allPeople(condition: { role: CANDYMAT_CANDIDATE }) {
|
||||
nodes {
|
||||
...BasicPersonFragment
|
||||
}
|
||||
}
|
||||
users: allPeople(condition: { role: CANDYMAT_PERSON }) {
|
||||
nodes {
|
||||
...BasicPersonFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicPersonFragment}
|
||||
`;
|
||||
|
||||
export interface GetPersonsSortedByRoleResponse {
|
||||
editors: {
|
||||
nodes: Array<BasicPersonResponse>;
|
||||
__typename: "PeopleConnection";
|
||||
};
|
||||
candidates: {
|
||||
nodes: Array<BasicPersonResponse>;
|
||||
__typename: "PeopleConnection";
|
||||
};
|
||||
users: {
|
||||
nodes: Array<BasicPersonResponse>;
|
||||
__typename: "PeopleConnection";
|
||||
};
|
||||
}
|
|
@ -9,6 +9,7 @@ import QuestionAnswerIcon from "@material-ui/icons/QuestionAnswer";
|
|||
import PeopleIcon from "@material-ui/icons/People";
|
||||
import { MenuOption } from "./MainMenu";
|
||||
import { PersonRoutes } from "./Main";
|
||||
import { UserManagement } from "./UserManagement";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
|
@ -47,7 +48,7 @@ export function HomePageEditor(): React.ReactElement {
|
|||
<CategoryList />
|
||||
</Route>
|
||||
<Route path={editorRoutes.userManagement.path}>
|
||||
<div />
|
||||
<UserManagement />
|
||||
</Route>
|
||||
</Switch>
|
||||
<Copyright />
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardHeader,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: "100%",
|
||||
marginBottom: theme.spacing(1),
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
title: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
interface PersonCardProps {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
export default function PersonCard(props: PersonCardProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
const getInitials = (): string => {
|
||||
const initials =
|
||||
(props.firstName.charAt(0) || "") + (props.lastName.charAt(0) || "");
|
||||
return initials ? initials.toUpperCase() : "?";
|
||||
};
|
||||
|
||||
const fullName = `${props.firstName} ${props.lastName}`.trim();
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Avatar className={classes.avatar} aria-label={fullName}>
|
||||
{getInitials()}
|
||||
</Avatar>
|
||||
<Typography className={classes.title}>{fullName}</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import React from "react";
|
||||
import { Paper, Typography } from "@material-ui/core";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import PersonCard from "./PersonCard";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import {
|
||||
BasicPersonResponse,
|
||||
GET_PERSONS_SORTED_BY_ROLE,
|
||||
GetPersonsSortedByRoleResponse,
|
||||
} from "../backend/queries/person";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: "100%",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
}));
|
||||
|
||||
export function UserManagement(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const persons = useQuery<GetPersonsSortedByRoleResponse>(
|
||||
GET_PERSONS_SORTED_BY_ROLE
|
||||
).data;
|
||||
|
||||
const convertPersonNodeToPersonCard = (
|
||||
person: BasicPersonResponse
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<PersonCard
|
||||
key={person.id}
|
||||
firstName={person.firstName || ""}
|
||||
lastName={person.lastName || ""}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
|
||||
Redaktion
|
||||
</Typography>
|
||||
{persons?.editors.nodes.map(convertPersonNodeToPersonCard)}
|
||||
</Paper>
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
|
||||
Kandidat:innen
|
||||
</Typography>
|
||||
{persons?.candidates.nodes.map(convertPersonNodeToPersonCard)}
|
||||
</Paper>
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
|
||||
Andere registrierte Personen
|
||||
</Typography>
|
||||
{persons?.users.nodes.map(convertPersonNodeToPersonCard)}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import React from "react";
|
||||
import { findByText, render, screen } from "@testing-library/react";
|
||||
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import { UserManagement } from "../components/UserManagement";
|
||||
import {
|
||||
getPersonsSortedByRoleAllFilledMock,
|
||||
getPersonsSortedByRoleNoCandidatesMock,
|
||||
} from "../backend/queries/person.mock";
|
||||
|
||||
function renderUserManagementPage(mocks: Array<MockedResponse>) {
|
||||
return render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<UserManagement />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe("The UserManagement page", () => {
|
||||
test(" displays all registered persons sorted by role.", async () => {
|
||||
renderUserManagementPage(getPersonsSortedByRoleAllFilledMock);
|
||||
|
||||
const editorsSectionTitle = await screen.findByText(/Redaktion/);
|
||||
const editorsSection = editorsSectionTitle.parentElement as HTMLElement;
|
||||
const editor = await findByText(editorsSection, /Erika Mustermann/);
|
||||
expect(editor).not.toBeNull();
|
||||
|
||||
const candidatesSectionTitle = await screen.findByText(/Kandidat/);
|
||||
const candidatesSection = candidatesSectionTitle.parentElement as HTMLElement;
|
||||
const candidate1 = await findByText(candidatesSection, /Max Mustermann/);
|
||||
const candidate2 = await findByText(candidatesSection, /Tricia McMillan/);
|
||||
expect(candidate1).not.toBeNull();
|
||||
expect(candidate2).not.toBeNull();
|
||||
|
||||
const otherUsersSectionTitle = await screen.findByText(/Andere/);
|
||||
const otherUsersSection = otherUsersSectionTitle.parentElement as HTMLElement;
|
||||
const otherUser = await findByText(otherUsersSection, /Happy User/);
|
||||
expect(otherUser).not.toBeNull();
|
||||
});
|
||||
|
||||
test(" displays all registered persons sorted by role even though there are no candidates.", async () => {
|
||||
renderUserManagementPage(getPersonsSortedByRoleNoCandidatesMock);
|
||||
|
||||
const editorsSectionTitle = await screen.findByText(/Redaktion/);
|
||||
const editorsSection = editorsSectionTitle.parentElement as HTMLElement;
|
||||
const editor = await findByText(editorsSection, /Erika Mustermann/);
|
||||
expect(editor).not.toBeNull();
|
||||
|
||||
const candidatesSectionTitle = await screen.findByText(/Kandidat/);
|
||||
const candidatesSection = candidatesSectionTitle.parentElement as HTMLElement;
|
||||
expect(candidatesSection.childElementCount).toBe(1);
|
||||
|
||||
const otherUsersSectionTitle = await screen.findByText(/Andere/);
|
||||
const otherUsersSection = otherUsersSectionTitle.parentElement as HTMLElement;
|
||||
const otherUser1 = await findByText(otherUsersSection, /Max Mustermann/);
|
||||
const otherUser2 = await findByText(otherUsersSection, /Tricia McMillan/);
|
||||
const otherUser3 = await findByText(otherUsersSection, /Happy User/);
|
||||
expect(otherUser1).not.toBeNull();
|
||||
expect(otherUser2).not.toBeNull();
|
||||
expect(otherUser3).not.toBeNull();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue