Merge pull request 'feature/#14' (#23) from feature/#14 into develop
Reviewed-on: Netzbegruenung/candymat#23
This commit is contained in:
commit
c12aadef04
14
backend/deep_reset_db.sh
Executable file
14
backend/deep_reset_db.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
docker-compose stop postgres
|
||||
CONTAINER=$(docker image rm candymat-postgres:11.5 2> >(grep -P '[a-f0-9]{12}' -o) | head -1)
|
||||
echo "Going to remove container: $CONTAINER"
|
||||
docker container rm $CONTAINER
|
||||
docker image rm candymat-postgres:11.5
|
||||
echo "Deleting db-data docker volumes ..."
|
||||
VOLUMES=$(docker volume ls -q | grep "db-data")
|
||||
for volume in ${VOLUMES[@]}; do
|
||||
echo "Deleting volume '$volume'"
|
||||
docker volume rm $volume
|
||||
done;
|
||||
docker-compose up --build postgres
|
|
@ -30,7 +30,7 @@ create table candymat_data.answer
|
|||
(
|
||||
question_row_id integer REFERENCES candymat_data.question (row_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
person_row_id integer REFERENCES candymat_data.person (row_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
position integer NOT NULL,
|
||||
position integer NOT NULL check (position between 0 and 3),
|
||||
text character varying(15000),
|
||||
created_at timestamp default now(),
|
||||
primary key (question_row_id, person_row_id)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit f239bec4ffb7327ed88679239c7c918825811040
|
||||
Subproject commit d414b95c1c664adcd5149aee8eac4436b40d7dfb
|
19
redaktions-app/.eslintrc.yml
Normal file
19
redaktions-app/.eslintrc.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
env:
|
||||
browser: true
|
||||
extends:
|
||||
- 'eslint:recommended'
|
||||
- 'plugin:react/recommended'
|
||||
- 'plugin:@typescript-eslint/recommended'
|
||||
parser: '@typescript-eslint/parser'
|
||||
parserOptions:
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
ecmaVersion: 12
|
||||
sourceType: module
|
||||
plugins:
|
||||
- react
|
||||
- '@typescript-eslint'
|
||||
rules: {}
|
||||
settings:
|
||||
react:
|
||||
version: detect
|
3
redaktions-app/.prettierignore
Normal file
3
redaktions-app/.prettierignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
1
redaktions-app/.prettierrc.json
Normal file
1
redaktions-app/.prettierrc.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -9,18 +9,18 @@ The app is written in typescript and react and uses apollo to query the backend
|
|||
|
||||
### Setup
|
||||
|
||||
* Install `npm`
|
||||
* In this directory run `npm ci` to install all dependencies according to the package.json and package-lock.json.
|
||||
- Install `npm`
|
||||
- In this directory run `npm ci` to install all dependencies according to the package.json and package-lock.json.
|
||||
|
||||
### Develop locally
|
||||
|
||||
* In the parent directory run
|
||||
- In the parent directory run
|
||||
```shell script
|
||||
docker-compose up
|
||||
```
|
||||
which will start the whole setup including this app in a dockerfile.
|
||||
However, rebuilding and restarting this image can be cumbersome and is not necessary in the development setup.
|
||||
* Instead run
|
||||
- Instead run
|
||||
```shell script
|
||||
npm start
|
||||
```
|
||||
|
|
3075
redaktions-app/package-lock.json
generated
3075
redaktions-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -24,8 +24,14 @@
|
|||
"@types/react": "^16.9.46",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.12.0",
|
||||
"@typescript-eslint/parser": "^4.12.0",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"husky": "^4.3.6",
|
||||
"jest-environment-jsdom-sixteen": "^1.0.3"
|
||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||
"lint-staged": "^10.5.3",
|
||||
"prettier": "2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -38,9 +44,15 @@
|
|||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm test"
|
||||
"pre-commit": "lint-staged && npm test"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"eslint",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<meta
|
||||
name="description"
|
||||
content="App zum Erstellen von Fragen für den Candymat"
|
||||
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
import './App.css';
|
||||
import "./App.css";
|
||||
import React from "react";
|
||||
import Main from "./components/Main";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
import { Redirect, Route, RouteProps, Switch } from "react-router-dom";
|
||||
import SignIn from "./components/SignIn";
|
||||
import SignUp from "./components/SignUp";
|
||||
|
||||
|
||||
function App() {
|
||||
function App(): React.ReactElement {
|
||||
return (
|
||||
<Switch>
|
||||
<PrivateRoute exact path={"/"}><Main /></PrivateRoute>
|
||||
<NotLoggedInOnlyRoute path={"/login"}><SignIn /></NotLoggedInOnlyRoute>
|
||||
<NotLoggedInOnlyRoute path={"/signup"}><SignUp /></NotLoggedInOnlyRoute>
|
||||
<PrivateRoute exact path={"/"}>
|
||||
<Main />
|
||||
</PrivateRoute>
|
||||
<NotLoggedInOnlyRoute path={"/login"}>
|
||||
<SignIn />
|
||||
</NotLoggedInOnlyRoute>
|
||||
<NotLoggedInOnlyRoute path={"/signup"}>
|
||||
<SignUp />
|
||||
</NotLoggedInOnlyRoute>
|
||||
</Switch>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const isLoggedIn = () => !!localStorage.getItem("token")
|
||||
export const isLoggedIn = (): boolean => !!localStorage.getItem("token");
|
||||
|
||||
// @ts-ignore
|
||||
function PrivateRoute({ children, ...rest }) {
|
||||
function PrivateRoute({ children, ...rest }: RouteProps) {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
|
@ -30,7 +34,7 @@ function PrivateRoute({ children, ...rest }) {
|
|||
<Redirect
|
||||
to={{
|
||||
pathname: "/login",
|
||||
state: { from: location }
|
||||
state: { from: location },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -39,12 +43,11 @@ function PrivateRoute({ children, ...rest }) {
|
|||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
function NotLoggedInOnlyRoute({ children, ...rest }) {
|
||||
function NotLoggedInOnlyRoute({ children, ...rest }: RouteProps) {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ location }) =>
|
||||
render={() =>
|
||||
!isLoggedIn() ? (
|
||||
children
|
||||
) : (
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import {ApolloClient, createHttpLink, InMemoryCache} from "@apollo/client";
|
||||
import {setContext} from "@apollo/client/link/context";
|
||||
import {getRawJsonWebToken} from "../jwt/jwt";
|
||||
|
||||
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
||||
import { setContext } from "@apollo/client/link/context";
|
||||
import { getRawJsonWebToken } from "../jwt/jwt";
|
||||
|
||||
const httpLink = createHttpLink({
|
||||
uri: 'http://localhost:5433/graphql',
|
||||
uri: "http://localhost:5433/graphql",
|
||||
});
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = getRawJsonWebToken();
|
||||
return token ? {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: `Bearer ${token}`,
|
||||
}
|
||||
} : headers
|
||||
return token
|
||||
? {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
: headers;
|
||||
});
|
||||
|
||||
export const client = new ApolloClient({
|
||||
|
|
84
redaktions-app/src/backend/mutations/answer.mock.ts
Normal file
84
redaktions-app/src/backend/mutations/answer.mock.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
ADD_ANSWER,
|
||||
AddAnswerResponse,
|
||||
AddAnswerVariables,
|
||||
EDIT_ANSWER,
|
||||
EditAnswerPayload,
|
||||
EditAnswerResponse,
|
||||
EditAnswerVariables,
|
||||
} from "./answer";
|
||||
import { answersMock } from "../queries/answer.mock";
|
||||
import { CandidatePosition } from "../../components/CandidatePositionLegend";
|
||||
import { FullAnswerResponse } from "../queries/answer";
|
||||
|
||||
const editAnswerVariables: EditAnswerVariables = {
|
||||
id: "a22",
|
||||
text: "New answer",
|
||||
};
|
||||
|
||||
const getEditedAnswerMock = (): EditAnswerPayload | null => {
|
||||
const originalAnswer = answersMock.find(
|
||||
(a) => a.id === editAnswerVariables.id
|
||||
);
|
||||
return originalAnswer
|
||||
? {
|
||||
answer: {
|
||||
...originalAnswer,
|
||||
text:
|
||||
editAnswerVariables.text === undefined
|
||||
? originalAnswer.text
|
||||
: editAnswerVariables.text,
|
||||
position:
|
||||
editAnswerVariables.position === undefined
|
||||
? originalAnswer.position
|
||||
: editAnswerVariables.position,
|
||||
},
|
||||
__typename: "UpdateAnswerPayload",
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const editAnswerMock: Array<MockedResponse<EditAnswerResponse>> = [
|
||||
{
|
||||
request: {
|
||||
query: EDIT_ANSWER,
|
||||
variables: editAnswerVariables,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
updateAnswer: getEditedAnswerMock(),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const addAnswerVariables: AddAnswerVariables = {
|
||||
position: CandidatePosition.positive,
|
||||
questionRowId: 3,
|
||||
personRowId: 2,
|
||||
};
|
||||
|
||||
const addedAnswerMock: FullAnswerResponse = {
|
||||
id: "newA",
|
||||
...addAnswerVariables,
|
||||
text: addAnswerVariables.text !== undefined ? addAnswerVariables.text : null,
|
||||
__typename: "Answer",
|
||||
};
|
||||
|
||||
export const addAnswerMock: Array<MockedResponse<AddAnswerResponse>> = [
|
||||
{
|
||||
request: {
|
||||
query: ADD_ANSWER,
|
||||
variables: addAnswerVariables,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
createAnswer: {
|
||||
answer: addedAnswerMock,
|
||||
__typename: "CreateAnswerPayload",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
163
redaktions-app/src/backend/mutations/answer.ts
Normal file
163
redaktions-app/src/backend/mutations/answer.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
import {
|
||||
ApolloCache,
|
||||
FetchResult,
|
||||
gql,
|
||||
Reference,
|
||||
StoreObject,
|
||||
} from "@apollo/client";
|
||||
import {
|
||||
FullAnswerFragment,
|
||||
FullAnswerResponse,
|
||||
QuestionAnswerResponse,
|
||||
} from "../queries/answer";
|
||||
import { CandidatePosition } from "../../components/CandidatePositionLegend";
|
||||
|
||||
export const EDIT_ANSWER = gql`
|
||||
mutation UpdateAnswer($id: ID!, $position: Int, $text: String) {
|
||||
updateAnswer(
|
||||
input: { id: $id, answerPatch: { position: $position, text: $text } }
|
||||
) {
|
||||
answer {
|
||||
...FullAnswerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${FullAnswerFragment}
|
||||
`;
|
||||
|
||||
export interface EditAnswerResponse {
|
||||
updateAnswer: EditAnswerPayload | null;
|
||||
}
|
||||
|
||||
export interface EditAnswerPayload {
|
||||
answer: FullAnswerResponse;
|
||||
__typename: "UpdateAnswerPayload";
|
||||
}
|
||||
|
||||
export interface EditAnswerVariables {
|
||||
id: string;
|
||||
position?: CandidatePosition;
|
||||
text?: string | null;
|
||||
}
|
||||
|
||||
export const ADD_ANSWER = gql`
|
||||
mutation AddAnswer(
|
||||
$questionRowId: Int!
|
||||
$personRowId: Int!
|
||||
$position: Int!
|
||||
$text: String
|
||||
) {
|
||||
createAnswer(
|
||||
input: {
|
||||
answer: {
|
||||
questionRowId: $questionRowId
|
||||
personRowId: $personRowId
|
||||
position: $position
|
||||
text: $text
|
||||
}
|
||||
}
|
||||
) {
|
||||
answer {
|
||||
...FullAnswerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${FullAnswerFragment}
|
||||
`;
|
||||
|
||||
export interface AddAnswerResponse {
|
||||
createAnswer: AddAnswerPayload | null;
|
||||
}
|
||||
|
||||
export interface AddAnswerPayload {
|
||||
answer: FullAnswerResponse;
|
||||
__typename: "CreateAnswerPayload";
|
||||
}
|
||||
|
||||
export interface AddAnswerVariables {
|
||||
questionRowId: number;
|
||||
personRowId: number;
|
||||
position: CandidatePosition;
|
||||
text?: string | null;
|
||||
}
|
||||
|
||||
const matchesStoreFieldName = (
|
||||
storeFieldName: string,
|
||||
personRowId: number,
|
||||
questionRowId: number
|
||||
): boolean => {
|
||||
const fullName = `answerByQuestionRowIdAndPersonRowId({"personRowId":${personRowId},"questionRowId":${questionRowId}})`;
|
||||
return fullName === storeFieldName;
|
||||
};
|
||||
|
||||
interface NodesCacheRefs {
|
||||
nodes: Array<Reference | StoreObject>;
|
||||
}
|
||||
|
||||
const addAnswerToQuestion = (
|
||||
cache: ApolloCache<AddAnswerResponse>,
|
||||
question: QuestionAnswerResponse,
|
||||
newAnswerRef: Reference
|
||||
) => {
|
||||
cache.modify({
|
||||
id: cache.identify({ ...question }),
|
||||
fields: {
|
||||
answersByQuestionRowId: (
|
||||
answerRefs: NodesCacheRefs = { nodes: [] }
|
||||
): NodesCacheRefs => {
|
||||
return { nodes: [...answerRefs.nodes, newAnswerRef] };
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const addAnswerToRootField = (
|
||||
cache: ApolloCache<AddAnswerResponse>,
|
||||
newAnswerRef: Reference,
|
||||
personRowId: number,
|
||||
questionRowId: number
|
||||
) => {
|
||||
cache.modify({
|
||||
fields: {
|
||||
answerByQuestionRowIdAndPersonRowId: (
|
||||
answerRefs: Reference | StoreObject | null,
|
||||
{ storeFieldName }
|
||||
) => {
|
||||
if (matchesStoreFieldName(storeFieldName, personRowId, questionRowId)) {
|
||||
return newAnswerRef;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const writeAnswerToCache = (
|
||||
cache: ApolloCache<AddAnswerResponse>,
|
||||
answer: FullAnswerResponse
|
||||
): Reference | undefined => {
|
||||
return cache.writeFragment<FullAnswerResponse>({
|
||||
data: answer,
|
||||
fragment: FullAnswerFragment,
|
||||
fragmentName: "FullAnswerFragment",
|
||||
});
|
||||
};
|
||||
|
||||
export const updateCacheAfterAddingAnswer = (
|
||||
cache: ApolloCache<AddAnswerResponse>,
|
||||
{ data }: FetchResult<AddAnswerResponse>,
|
||||
question: QuestionAnswerResponse
|
||||
): void => {
|
||||
const answer = data?.createAnswer?.answer;
|
||||
if (answer) {
|
||||
const newAnswerRef = writeAnswerToCache(cache, answer);
|
||||
if (newAnswerRef) {
|
||||
addAnswerToQuestion(cache, question, newAnswerRef);
|
||||
addAnswerToRootField(
|
||||
cache,
|
||||
newAnswerRef,
|
||||
answer.personRowId,
|
||||
answer.questionRowId
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,32 +1,47 @@
|
|||
import {MockedResponse} from "@apollo/client/testing";
|
||||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
ADD_CATEGORY, AddCategoryResponse,
|
||||
AddCategoryVariables, DELETE_CATEGORY, DeleteCategoryPayload, DeleteCategoryResponse, DeleteCategoryVariables,
|
||||
EDIT_CATEGORY, EditCategoryPayload,
|
||||
ADD_CATEGORY,
|
||||
AddCategoryResponse,
|
||||
AddCategoryVariables,
|
||||
DELETE_CATEGORY,
|
||||
DeleteCategoryPayload,
|
||||
DeleteCategoryResponse,
|
||||
DeleteCategoryVariables,
|
||||
EDIT_CATEGORY,
|
||||
EditCategoryPayload,
|
||||
EditCategoryResponse,
|
||||
EditCategoryVariables
|
||||
EditCategoryVariables,
|
||||
} from "./category";
|
||||
import {BasicCategoryResponse} from "../queries/category";
|
||||
import {categoryNodesMock} from "../queries/category.mock";
|
||||
|
||||
import { BasicCategoryResponse } from "../queries/category";
|
||||
import { categoryNodesMock } from "../queries/category.mock";
|
||||
|
||||
const editCategoryVariables: EditCategoryVariables = {
|
||||
id: 'c1',
|
||||
title: 'New title for Category 1',
|
||||
description: 'Further information for C1',
|
||||
id: "c1",
|
||||
title: "New title for Category 1",
|
||||
description: "Further information for C1",
|
||||
};
|
||||
|
||||
const getEditedCategoryMock = (): EditCategoryPayload | null => {
|
||||
const originalCategory = categoryNodesMock.find(c => c.id === editCategoryVariables.id)
|
||||
return originalCategory ? {
|
||||
category: {
|
||||
...originalCategory,
|
||||
title: editCategoryVariables.title === undefined ? originalCategory.title : editCategoryVariables.title,
|
||||
description: editCategoryVariables.description === undefined ? originalCategory.description : null,
|
||||
},
|
||||
__typename: "UpdateCategoryPayload",
|
||||
} : null
|
||||
}
|
||||
const originalCategory = categoryNodesMock.find(
|
||||
(c) => c.id === editCategoryVariables.id
|
||||
);
|
||||
return originalCategory
|
||||
? {
|
||||
category: {
|
||||
...originalCategory,
|
||||
title:
|
||||
editCategoryVariables.title === undefined
|
||||
? originalCategory.title
|
||||
: editCategoryVariables.title,
|
||||
description:
|
||||
editCategoryVariables.description === undefined
|
||||
? originalCategory.description
|
||||
: null,
|
||||
},
|
||||
__typename: "UpdateCategoryPayload",
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const editCategoryMock: Array<MockedResponse<EditCategoryResponse>> = [
|
||||
{
|
||||
|
@ -37,13 +52,13 @@ export const editCategoryMock: Array<MockedResponse<EditCategoryResponse>> = [
|
|||
result: {
|
||||
data: {
|
||||
updateCategory: getEditedCategoryMock(),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const addCategoryVariables: AddCategoryVariables = {
|
||||
title: 'New category',
|
||||
title: "New category",
|
||||
description: "",
|
||||
};
|
||||
|
||||
|
@ -52,8 +67,8 @@ const addedCategoryMock: BasicCategoryResponse = {
|
|||
rowId: 3,
|
||||
title: addCategoryVariables.title as string,
|
||||
description: addCategoryVariables.description as string,
|
||||
__typename: "Category"
|
||||
}
|
||||
__typename: "Category",
|
||||
};
|
||||
|
||||
export const addCategoryMock: Array<MockedResponse<AddCategoryResponse>> = [
|
||||
{
|
||||
|
@ -66,25 +81,31 @@ export const addCategoryMock: Array<MockedResponse<AddCategoryResponse>> = [
|
|||
createCategory: {
|
||||
category: addedCategoryMock,
|
||||
__typename: "CreateCategoryPayload",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const deleteCategoryVariables: DeleteCategoryVariables = {
|
||||
id: 'c2'
|
||||
id: "c2",
|
||||
};
|
||||
|
||||
const getDeletedCategoryMock = (): DeleteCategoryPayload | null => {
|
||||
const categoryToBeDeleted = categoryNodesMock.find(q => q.id === deleteCategoryVariables.id)
|
||||
return categoryToBeDeleted ? {
|
||||
category: categoryToBeDeleted,
|
||||
__typename: "DeleteCategoryPayload",
|
||||
} : null
|
||||
}
|
||||
const categoryToBeDeleted = categoryNodesMock.find(
|
||||
(q) => q.id === deleteCategoryVariables.id
|
||||
);
|
||||
return categoryToBeDeleted
|
||||
? {
|
||||
category: categoryToBeDeleted,
|
||||
__typename: "DeleteCategoryPayload",
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const deleteCategoryMock: Array<MockedResponse<DeleteCategoryResponse>> = [
|
||||
export const deleteCategoryMock: Array<
|
||||
MockedResponse<DeleteCategoryResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: DELETE_CATEGORY,
|
||||
|
@ -93,8 +114,7 @@ export const deleteCategoryMock: Array<MockedResponse<DeleteCategoryResponse>> =
|
|||
result: {
|
||||
data: {
|
||||
deleteCategory: getDeletedCategoryMock(),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
];
|
||||
|
|
|
@ -1,55 +1,65 @@
|
|||
import {gql} from "@apollo/client";
|
||||
import {BasicCategoryFragment, BasicCategoryResponse} from "../queries/category";
|
||||
import { gql } from "@apollo/client";
|
||||
import {
|
||||
BasicCategoryFragment,
|
||||
BasicCategoryResponse,
|
||||
} from "../queries/category";
|
||||
|
||||
export const EDIT_CATEGORY = gql`
|
||||
mutation UpdateCategory($id: ID!, $title: String, $description: String) {
|
||||
updateCategory(input: {id: $id, categoryPatch: {description: $description, title: $title}}) {
|
||||
updateCategory(
|
||||
input: {
|
||||
id: $id
|
||||
categoryPatch: { description: $description, title: $title }
|
||||
}
|
||||
) {
|
||||
category {
|
||||
...BasicCategoryFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicCategoryFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface EditCategoryResponse {
|
||||
updateCategory: EditCategoryPayload | null
|
||||
updateCategory: EditCategoryPayload | null;
|
||||
}
|
||||
|
||||
export interface EditCategoryPayload {
|
||||
category: BasicCategoryResponse,
|
||||
__typename: "UpdateCategoryPayload",
|
||||
category: BasicCategoryResponse;
|
||||
__typename: "UpdateCategoryPayload";
|
||||
}
|
||||
|
||||
export interface EditCategoryVariables {
|
||||
id: string,
|
||||
title?: string,
|
||||
description?: string | null,
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export const ADD_CATEGORY = gql`
|
||||
mutation AddCategory($title: String!, $description: String) {
|
||||
createCategory(input: {category: {title: $title, description: $description}}) {
|
||||
createCategory(
|
||||
input: { category: { title: $title, description: $description } }
|
||||
) {
|
||||
category {
|
||||
...BasicCategoryFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicCategoryFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface AddCategoryResponse {
|
||||
createCategory: AddCategoryPayload | null,
|
||||
createCategory: AddCategoryPayload | null;
|
||||
}
|
||||
|
||||
export interface AddCategoryPayload {
|
||||
category: BasicCategoryResponse,
|
||||
__typename: "CreateCategoryPayload",
|
||||
category: BasicCategoryResponse;
|
||||
__typename: "CreateCategoryPayload";
|
||||
}
|
||||
|
||||
export interface AddCategoryVariables {
|
||||
title: string,
|
||||
description?: string | null,
|
||||
title: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export const DELETE_CATEGORY = gql`
|
||||
|
@ -61,17 +71,17 @@ export const DELETE_CATEGORY = gql`
|
|||
}
|
||||
}
|
||||
${BasicCategoryFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface DeleteCategoryResponse {
|
||||
deleteCategory: DeleteCategoryPayload | null
|
||||
deleteCategory: DeleteCategoryPayload | null;
|
||||
}
|
||||
|
||||
export interface DeleteCategoryPayload {
|
||||
category: BasicCategoryResponse,
|
||||
__typename: "DeleteCategoryPayload",
|
||||
category: BasicCategoryResponse;
|
||||
__typename: "DeleteCategoryPayload";
|
||||
}
|
||||
|
||||
export interface DeleteCategoryVariables {
|
||||
id: string,
|
||||
id: string;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {MockedResponse} from "@apollo/client/testing";
|
||||
import {LOGIN, LoginResponse} from "./login";
|
||||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import { LOGIN, LoginResponse } from "./login";
|
||||
|
||||
export const loginMock: Array<MockedResponse<LoginResponse>> = [
|
||||
{
|
||||
|
@ -8,14 +8,14 @@ export const loginMock: Array<MockedResponse<LoginResponse>> = [
|
|||
variables: {
|
||||
email: "test@email.com",
|
||||
password: "password",
|
||||
}
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
authenticate: {
|
||||
jwtToken: "123"
|
||||
}
|
||||
}
|
||||
jwtToken: "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -24,14 +24,14 @@ export const loginMock: Array<MockedResponse<LoginResponse>> = [
|
|||
variables: {
|
||||
email: "test@email.com",
|
||||
password: "wrong-password",
|
||||
}
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
authenticate: {
|
||||
jwtToken: undefined
|
||||
}
|
||||
}
|
||||
jwtToken: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import {gql} from "@apollo/client";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const LOGIN = gql`
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
authenticate(input: {email: $email, password: $password}) {
|
||||
jwtToken
|
||||
}
|
||||
}`
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
authenticate(input: { email: $email, password: $password }) {
|
||||
jwtToken
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface LoginVariables {
|
||||
email: string,
|
||||
password: string
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
authenticate: {
|
||||
jwtToken?: string
|
||||
}
|
||||
authenticate: {
|
||||
jwtToken?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,37 +1,55 @@
|
|||
import {MockedResponse} from "@apollo/client/testing";
|
||||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
ADD_QUESTION, AddQuestionResponse,
|
||||
AddQuestionVariables, DELETE_QUESTION, DeleteQuestionPayload, DeleteQuestionResponse, DeleteQuestionVariables,
|
||||
EDIT_QUESTION, EditQuestionPayload,
|
||||
ADD_QUESTION,
|
||||
AddQuestionResponse,
|
||||
AddQuestionVariables,
|
||||
DELETE_QUESTION,
|
||||
DeleteQuestionPayload,
|
||||
DeleteQuestionResponse,
|
||||
DeleteQuestionVariables,
|
||||
EDIT_QUESTION,
|
||||
EditQuestionPayload,
|
||||
EditQuestionResponse,
|
||||
EditQuestionVariables
|
||||
EditQuestionVariables,
|
||||
} from "./question";
|
||||
import {BasicQuestionResponse} from "../queries/question";
|
||||
import {questionNodesMock} from "../queries/question.mock";
|
||||
import {categoryNodesMock} from "../queries/category.mock";
|
||||
|
||||
import { BasicQuestionResponse } from "../queries/question";
|
||||
import { questionNodesMock } from "../queries/question.mock";
|
||||
import { categoryNodesMock } from "../queries/category.mock";
|
||||
|
||||
const editQuestionVariables: EditQuestionVariables = {
|
||||
id: 'q1',
|
||||
title: 'New title for Question 1?',
|
||||
description: 'Further information for Q1',
|
||||
id: "q1",
|
||||
title: "New title for Question 1?",
|
||||
description: "Further information for Q1",
|
||||
categoryRowId: 1,
|
||||
};
|
||||
|
||||
const getEditedQuestionMock = (): EditQuestionPayload | null => {
|
||||
const originalQuestion = questionNodesMock.find(q => q.id === editQuestionVariables.id)
|
||||
return originalQuestion ? {
|
||||
question: {
|
||||
...originalQuestion,
|
||||
title: editQuestionVariables.title === undefined ? originalQuestion.title : editQuestionVariables.title,
|
||||
description: editQuestionVariables.description === undefined ? originalQuestion.description : null,
|
||||
categoryByCategoryRowId: editQuestionVariables.categoryRowId === undefined
|
||||
? originalQuestion.categoryByCategoryRowId
|
||||
: categoryNodesMock.find(c => c.rowId === editQuestionVariables.categoryRowId) || null,
|
||||
},
|
||||
__typename: "UpdateQuestionPayload",
|
||||
} : null
|
||||
}
|
||||
const originalQuestion = questionNodesMock.find(
|
||||
(q) => q.id === editQuestionVariables.id
|
||||
);
|
||||
return originalQuestion
|
||||
? {
|
||||
question: {
|
||||
...originalQuestion,
|
||||
title:
|
||||
editQuestionVariables.title === undefined
|
||||
? originalQuestion.title
|
||||
: editQuestionVariables.title,
|
||||
description:
|
||||
editQuestionVariables.description === undefined
|
||||
? originalQuestion.description
|
||||
: null,
|
||||
categoryByCategoryRowId:
|
||||
editQuestionVariables.categoryRowId === undefined
|
||||
? originalQuestion.categoryByCategoryRowId
|
||||
: categoryNodesMock.find(
|
||||
(c) => c.rowId === editQuestionVariables.categoryRowId
|
||||
) || null,
|
||||
},
|
||||
__typename: "UpdateQuestionPayload",
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const editQuestionMock: Array<MockedResponse<EditQuestionResponse>> = [
|
||||
{
|
||||
|
@ -42,24 +60,28 @@ export const editQuestionMock: Array<MockedResponse<EditQuestionResponse>> = [
|
|||
result: {
|
||||
data: {
|
||||
updateQuestion: getEditedQuestionMock(),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const addQuestionVariables: AddQuestionVariables = {
|
||||
title: 'New question?',
|
||||
title: "New question?",
|
||||
description: "",
|
||||
categoryRowId: null,
|
||||
};
|
||||
|
||||
const addedQuestionMock: BasicQuestionResponse = {
|
||||
id: `newQ`,
|
||||
rowId: 4,
|
||||
title: addQuestionVariables.title as string,
|
||||
description: addQuestionVariables.description as string,
|
||||
categoryByCategoryRowId: categoryNodesMock.find(c => c.rowId === editQuestionVariables.categoryRowId) || null,
|
||||
__typename: "Question"
|
||||
}
|
||||
categoryByCategoryRowId:
|
||||
categoryNodesMock.find(
|
||||
(c) => c.rowId === editQuestionVariables.categoryRowId
|
||||
) || null,
|
||||
__typename: "Question",
|
||||
};
|
||||
|
||||
export const addQuestionMock: Array<MockedResponse<AddQuestionResponse>> = [
|
||||
{
|
||||
|
@ -72,25 +94,31 @@ export const addQuestionMock: Array<MockedResponse<AddQuestionResponse>> = [
|
|||
createQuestion: {
|
||||
question: addedQuestionMock,
|
||||
__typename: "CreateQuestionPayload",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const deleteQuestionVariables: DeleteQuestionVariables = {
|
||||
id: 'q2'
|
||||
id: "q2",
|
||||
};
|
||||
|
||||
const getDeletedQuestionMock = (): DeleteQuestionPayload | null => {
|
||||
const questionToBeDeleted = questionNodesMock.find(q => q.id === deleteQuestionVariables.id)
|
||||
return questionToBeDeleted ? {
|
||||
question: questionToBeDeleted,
|
||||
__typename: "DeleteQuestionPayload",
|
||||
} : null
|
||||
}
|
||||
const questionToBeDeleted = questionNodesMock.find(
|
||||
(q) => q.id === deleteQuestionVariables.id
|
||||
);
|
||||
return questionToBeDeleted
|
||||
? {
|
||||
question: questionToBeDeleted,
|
||||
__typename: "DeleteQuestionPayload",
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const deleteQuestionMock: Array<MockedResponse<DeleteQuestionResponse>> = [
|
||||
export const deleteQuestionMock: Array<
|
||||
MockedResponse<DeleteQuestionResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: DELETE_QUESTION,
|
||||
|
@ -99,8 +127,7 @@ export const deleteQuestionMock: Array<MockedResponse<DeleteQuestionResponse>> =
|
|||
result: {
|
||||
data: {
|
||||
deleteQuestion: getDeletedQuestionMock(),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
];
|
||||
|
|
|
@ -1,57 +1,86 @@
|
|||
import {gql} from "@apollo/client";
|
||||
import {BasicQuestionFragment, BasicQuestionResponse} from "../queries/question";
|
||||
import { gql } from "@apollo/client";
|
||||
import {
|
||||
BasicQuestionFragment,
|
||||
BasicQuestionResponse,
|
||||
} from "../queries/question";
|
||||
|
||||
export const EDIT_QUESTION = gql`
|
||||
mutation UpdateQuestion($id: ID!, $title: String, $description: String, $categoryRowId: Int) {
|
||||
updateQuestion(input: {id: $id, questionPatch: {categoryRowId: $categoryRowId, description: $description, title: $title}}) {
|
||||
mutation UpdateQuestion(
|
||||
$id: ID!
|
||||
$title: String
|
||||
$description: String
|
||||
$categoryRowId: Int
|
||||
) {
|
||||
updateQuestion(
|
||||
input: {
|
||||
id: $id
|
||||
questionPatch: {
|
||||
categoryRowId: $categoryRowId
|
||||
description: $description
|
||||
title: $title
|
||||
}
|
||||
}
|
||||
) {
|
||||
question {
|
||||
...BasicQuestionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface EditQuestionResponse {
|
||||
updateQuestion: EditQuestionPayload | null,
|
||||
updateQuestion: EditQuestionPayload | null;
|
||||
}
|
||||
|
||||
export interface EditQuestionPayload {
|
||||
question: BasicQuestionResponse,
|
||||
__typename: "UpdateQuestionPayload",
|
||||
question: BasicQuestionResponse;
|
||||
__typename: "UpdateQuestionPayload";
|
||||
}
|
||||
|
||||
export interface EditQuestionVariables {
|
||||
id: string,
|
||||
title?: string,
|
||||
description?: string | null,
|
||||
categoryRowId?: number | null,
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string | null;
|
||||
categoryRowId?: number | null;
|
||||
}
|
||||
|
||||
export const ADD_QUESTION = gql`
|
||||
mutation AddQuestion($title: String!, $description: String, $categoryRowId: Int) {
|
||||
createQuestion(input: {question: {title: $title, categoryRowId: $categoryRowId, description: $description}}) {
|
||||
mutation AddQuestion(
|
||||
$title: String!
|
||||
$description: String
|
||||
$categoryRowId: Int
|
||||
) {
|
||||
createQuestion(
|
||||
input: {
|
||||
question: {
|
||||
title: $title
|
||||
categoryRowId: $categoryRowId
|
||||
description: $description
|
||||
}
|
||||
}
|
||||
) {
|
||||
question {
|
||||
...BasicQuestionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface AddQuestionResponse {
|
||||
createQuestion: AddQuestionPayload | null
|
||||
createQuestion: AddQuestionPayload | null;
|
||||
}
|
||||
|
||||
export interface AddQuestionPayload {
|
||||
question: BasicQuestionResponse,
|
||||
__typename: "CreateQuestionPayload",
|
||||
question: BasicQuestionResponse;
|
||||
__typename: "CreateQuestionPayload";
|
||||
}
|
||||
|
||||
export interface AddQuestionVariables {
|
||||
title: string,
|
||||
description?: string | null,
|
||||
categoryRowId?: number | null
|
||||
title: string;
|
||||
description?: string | null;
|
||||
categoryRowId?: number | null;
|
||||
}
|
||||
|
||||
export const DELETE_QUESTION = gql`
|
||||
|
@ -63,19 +92,17 @@ export const DELETE_QUESTION = gql`
|
|||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface DeleteQuestionResponse {
|
||||
deleteQuestion: DeleteQuestionPayload | null,
|
||||
deleteQuestion: DeleteQuestionPayload | null;
|
||||
}
|
||||
|
||||
export interface DeleteQuestionPayload {
|
||||
question: BasicQuestionResponse,
|
||||
__typename: "DeleteQuestionPayload",
|
||||
question: BasicQuestionResponse;
|
||||
__typename: "DeleteQuestionPayload";
|
||||
}
|
||||
|
||||
export interface DeleteQuestionVariables {
|
||||
id: string,
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,26 +1,38 @@
|
|||
import {gql} from "@apollo/client";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const SIGN_UP = gql`
|
||||
mutation CreateAccount($firstName: String!, $lastName: String!, $email: String!, $password: String!) {
|
||||
registerPerson(input: {firstName: $firstName, lastName: $lastName, email: $email, password: $password}) {
|
||||
person {
|
||||
id
|
||||
}
|
||||
}
|
||||
mutation CreateAccount(
|
||||
$firstName: String!
|
||||
$lastName: String!
|
||||
$email: String!
|
||||
$password: String!
|
||||
) {
|
||||
registerPerson(
|
||||
input: {
|
||||
firstName: $firstName
|
||||
lastName: $lastName
|
||||
email: $email
|
||||
password: $password
|
||||
}
|
||||
) {
|
||||
person {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SignUpVariables {
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
password: string,
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SignUpResponse {
|
||||
registerPerson: {
|
||||
person: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
registerPerson: {
|
||||
person: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
133
redaktions-app/src/backend/queries/answer.mock.ts
Normal file
133
redaktions-app/src/backend/queries/answer.mock.ts
Normal file
|
@ -0,0 +1,133 @@
|
|||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
AnswerPositionResponse,
|
||||
FullAnswerResponse,
|
||||
GET_ALL_QUESTION_ANSWERS,
|
||||
GET_ANSWER_BY_QUESTION_AND_PERSON,
|
||||
GetAllQuestionAnswersResponse,
|
||||
GetAllQuestionAnswersVariables,
|
||||
GetAnswerByQuestionAndPersonResponse,
|
||||
GetAnswerByQuestionAndPersonVariables,
|
||||
QuestionAnswerResponse,
|
||||
} from "./answer";
|
||||
import { CandidatePosition } from "../../components/CandidatePositionLegend";
|
||||
|
||||
export const answersMock: Array<FullAnswerResponse> = [
|
||||
{
|
||||
id: "a12",
|
||||
text: null,
|
||||
position: CandidatePosition.neutral,
|
||||
questionRowId: 1,
|
||||
personRowId: 2,
|
||||
__typename: "Answer",
|
||||
},
|
||||
{
|
||||
id: "a22",
|
||||
text: "Answer 2",
|
||||
position: CandidatePosition.positive,
|
||||
questionRowId: 2,
|
||||
personRowId: 2,
|
||||
__typename: "Answer",
|
||||
},
|
||||
];
|
||||
|
||||
const getAnswersForQuestionRowId = (
|
||||
questionRowId: number
|
||||
): Array<FullAnswerResponse> => {
|
||||
return answersMock.filter((answ) => answ.questionRowId === questionRowId);
|
||||
};
|
||||
|
||||
const getAnswersPositionForQuestionRowId = (
|
||||
questionRowId: number
|
||||
): Array<AnswerPositionResponse> => {
|
||||
return getAnswersForQuestionRowId(questionRowId).map((answer) => ({
|
||||
__typename: answer.__typename,
|
||||
id: answer.id,
|
||||
position: answer.position,
|
||||
}));
|
||||
};
|
||||
|
||||
export const questionAnswersMock: Array<QuestionAnswerResponse> = [
|
||||
{
|
||||
id: "q1",
|
||||
rowId: 1,
|
||||
title: "Question 1?",
|
||||
description: "Further information for Q1",
|
||||
categoryByCategoryRowId: {
|
||||
id: "c1",
|
||||
rowId: 1,
|
||||
title: "Category 1",
|
||||
__typename: "Category",
|
||||
},
|
||||
answersByQuestionRowId: {
|
||||
nodes: getAnswersPositionForQuestionRowId(1),
|
||||
__typename: "AnswersConnection",
|
||||
},
|
||||
__typename: "Question",
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
rowId: 2,
|
||||
title: "Question 2?",
|
||||
description: "Further information for Q2",
|
||||
categoryByCategoryRowId: null,
|
||||
answersByQuestionRowId: {
|
||||
nodes: getAnswersPositionForQuestionRowId(2),
|
||||
__typename: "AnswersConnection",
|
||||
},
|
||||
__typename: "Question",
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
rowId: 3,
|
||||
title: "Question 3?",
|
||||
description: null,
|
||||
categoryByCategoryRowId: null,
|
||||
answersByQuestionRowId: {
|
||||
nodes: getAnswersPositionForQuestionRowId(3),
|
||||
__typename: "AnswersConnection",
|
||||
},
|
||||
__typename: "Question",
|
||||
},
|
||||
];
|
||||
|
||||
export const getAllQuestionAnswersMock: Array<
|
||||
MockedResponse<GetAllQuestionAnswersResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: GET_ALL_QUESTION_ANSWERS,
|
||||
variables: {
|
||||
personRowId: 2,
|
||||
} as GetAllQuestionAnswersVariables,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
allQuestions: {
|
||||
nodes: questionAnswersMock,
|
||||
__typename: "QuestionsConnection",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getAnswerByQuestionAndPersonMock: Array<
|
||||
MockedResponse<GetAnswerByQuestionAndPersonResponse>
|
||||
> = [
|
||||
...questionAnswersMock.map((q, index) => ({
|
||||
request: {
|
||||
query: GET_ANSWER_BY_QUESTION_AND_PERSON,
|
||||
variables: {
|
||||
personRowId: 2,
|
||||
questionRowId: index + 1,
|
||||
} as GetAnswerByQuestionAndPersonVariables,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
answerByQuestionRowIdAndPersonRowId:
|
||||
getAnswersForQuestionRowId(index + 1)[0] || null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
];
|
98
redaktions-app/src/backend/queries/answer.ts
Normal file
98
redaktions-app/src/backend/queries/answer.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { gql } from "@apollo/client";
|
||||
import { BasicQuestionFragment, BasicQuestionResponse } from "./question";
|
||||
import { CandidatePosition } from "../../components/CandidatePositionLegend";
|
||||
|
||||
export const FullAnswerFragment = gql`
|
||||
fragment FullAnswerFragment on Answer {
|
||||
id
|
||||
text
|
||||
position
|
||||
questionRowId
|
||||
personRowId
|
||||
}
|
||||
`;
|
||||
|
||||
export interface FullAnswerResponse {
|
||||
id: string;
|
||||
text: string | null;
|
||||
position: CandidatePosition;
|
||||
questionRowId: number;
|
||||
personRowId: number;
|
||||
__typename: "Answer";
|
||||
}
|
||||
|
||||
export const GET_ANSWER_BY_QUESTION_AND_PERSON = gql`
|
||||
query GetAnswerByQuestionAndPerson($questionRowId: Int!, $personRowId: Int!) {
|
||||
answerByQuestionRowIdAndPersonRowId(
|
||||
personRowId: $personRowId
|
||||
questionRowId: $questionRowId
|
||||
) {
|
||||
...FullAnswerFragment
|
||||
}
|
||||
}
|
||||
${FullAnswerFragment}
|
||||
`;
|
||||
|
||||
export interface GetAnswerByQuestionAndPersonResponse {
|
||||
answerByQuestionRowIdAndPersonRowId: FullAnswerResponse | null;
|
||||
}
|
||||
|
||||
export interface GetAnswerByQuestionAndPersonVariables {
|
||||
personRowId: number;
|
||||
questionRowId: number;
|
||||
}
|
||||
|
||||
export const AnswerPositionFragment = gql`
|
||||
fragment AnswerPositionFragment on Answer {
|
||||
id
|
||||
position
|
||||
}
|
||||
`;
|
||||
|
||||
export interface AnswerPositionResponse {
|
||||
id: string;
|
||||
position: CandidatePosition;
|
||||
__typename: "Answer";
|
||||
}
|
||||
|
||||
export const QuestionAnswerFragment = gql`
|
||||
fragment QuestionAnswerFragment on Question {
|
||||
...BasicQuestionFragment
|
||||
answersByQuestionRowId(condition: { personRowId: $personRowId }) {
|
||||
nodes {
|
||||
...AnswerPositionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
${AnswerPositionFragment}
|
||||
`;
|
||||
|
||||
export interface QuestionAnswerResponse extends BasicQuestionResponse {
|
||||
answersByQuestionRowId: {
|
||||
nodes: Array<AnswerPositionResponse>;
|
||||
__typename: "AnswersConnection";
|
||||
};
|
||||
}
|
||||
|
||||
export const GET_ALL_QUESTION_ANSWERS = gql`
|
||||
query AllQuestionAnswers($personRowId: Int) {
|
||||
allQuestions(orderBy: CATEGORY_ROW_ID_ASC) {
|
||||
nodes {
|
||||
...QuestionAnswerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${QuestionAnswerFragment}
|
||||
`;
|
||||
|
||||
export interface GetAllQuestionAnswersResponse {
|
||||
allQuestions: {
|
||||
nodes: Array<QuestionAnswerResponse>;
|
||||
__typename: "QuestionsConnection";
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetAllQuestionAnswersVariables {
|
||||
personRowId?: number | null;
|
||||
}
|
|
@ -1,29 +1,32 @@
|
|||
import {MockedResponse} from "@apollo/client/testing";
|
||||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
BasicCategoryResponse,
|
||||
GET_ALL_CATEGORIES,
|
||||
GET_CATEGORY_BY_ID,
|
||||
GetAllCategoriesResponse,
|
||||
GetCategoryByIdResponse
|
||||
GetCategoryByIdResponse,
|
||||
} from "./category";
|
||||
|
||||
|
||||
export const categoryNodesMock: Array<BasicCategoryResponse> = [
|
||||
{
|
||||
id: "c1",
|
||||
rowId: 1,
|
||||
title: "Category 1",
|
||||
description: "Further information for C1",
|
||||
__typename: "Category"
|
||||
}, {
|
||||
__typename: "Category",
|
||||
},
|
||||
{
|
||||
id: "c2",
|
||||
rowId: 2,
|
||||
title: "Category 2",
|
||||
description: "Further information for C2",
|
||||
__typename: "Category"
|
||||
}];
|
||||
__typename: "Category",
|
||||
},
|
||||
];
|
||||
|
||||
export const getAllCategoriesMock: Array<MockedResponse<GetAllCategoriesResponse>> = [
|
||||
export const getAllCategoriesMock: Array<
|
||||
MockedResponse<GetAllCategoriesResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: GET_ALL_CATEGORIES,
|
||||
|
@ -37,21 +40,24 @@ export const getAllCategoriesMock: Array<MockedResponse<GetAllCategoriesResponse
|
|||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export const getCategoryByIdMock: Array<MockedResponse<GetCategoryByIdResponse>> = [...categoryNodesMock.map(c => ({
|
||||
request: {
|
||||
query: GET_CATEGORY_BY_ID,
|
||||
variables: {
|
||||
id: c.id,
|
||||
export const getCategoryByIdMock: Array<
|
||||
MockedResponse<GetCategoryByIdResponse>
|
||||
> = [
|
||||
...categoryNodesMock.map((c) => ({
|
||||
request: {
|
||||
query: GET_CATEGORY_BY_ID,
|
||||
variables: {
|
||||
id: c.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
category: c,
|
||||
result: {
|
||||
data: {
|
||||
category: c,
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
})),
|
||||
{
|
||||
request: {
|
||||
query: GET_CATEGORY_BY_ID,
|
||||
|
@ -64,5 +70,5 @@ export const getCategoryByIdMock: Array<MockedResponse<GetCategoryByIdResponse>>
|
|||
category: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {gql} from "@apollo/client";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const BasicCategoryFragment = gql`
|
||||
fragment BasicCategoryFragment on Category {
|
||||
|
@ -7,14 +7,14 @@ export const BasicCategoryFragment = gql`
|
|||
title
|
||||
description
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface BasicCategoryResponse {
|
||||
id: string,
|
||||
rowId: number,
|
||||
title: string,
|
||||
description: string | null,
|
||||
__typename: "Category",
|
||||
id: string;
|
||||
rowId: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
__typename: "Category";
|
||||
}
|
||||
|
||||
export const GET_ALL_CATEGORIES = gql`
|
||||
|
@ -26,28 +26,28 @@ export const GET_ALL_CATEGORIES = gql`
|
|||
}
|
||||
}
|
||||
${BasicCategoryFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface GetAllCategoriesResponse {
|
||||
allCategories: {
|
||||
nodes: Array<BasicCategoryResponse>,
|
||||
__typename: "CategoriesConnection",
|
||||
}
|
||||
nodes: Array<BasicCategoryResponse>;
|
||||
__typename: "CategoriesConnection";
|
||||
};
|
||||
}
|
||||
|
||||
export const GET_CATEGORY_BY_ID = gql`
|
||||
query GetCategoryById($id:ID!) {
|
||||
query GetCategoryById($id: ID!) {
|
||||
category(id: $id) {
|
||||
...BasicCategoryFragment
|
||||
}
|
||||
}
|
||||
${BasicCategoryFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface GetCategoryByIdResponse {
|
||||
category: BasicCategoryResponse | null,
|
||||
category: BasicCategoryResponse | null;
|
||||
}
|
||||
|
||||
export interface GetCategoryByIdVariables {
|
||||
id: string,
|
||||
id: string;
|
||||
}
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import {MockedResponse} from "@apollo/client/testing";
|
||||
import { MockedResponse } from "@apollo/client/testing";
|
||||
import {
|
||||
BasicQuestionResponse,
|
||||
GET_ALL_QUESTIONS,
|
||||
GET_QUESTION_BY_ID,
|
||||
GetAllQuestionsResponse,
|
||||
GetQuestionByIdResponse
|
||||
GetQuestionByIdResponse,
|
||||
} from "./question";
|
||||
|
||||
|
||||
export const questionNodesMock: Array<BasicQuestionResponse> = [{
|
||||
id: "q1",
|
||||
title: "Question 1?",
|
||||
description: "Further information for Q1",
|
||||
categoryByCategoryRowId: {
|
||||
id: "c1",
|
||||
export const questionNodesMock: Array<BasicQuestionResponse> = [
|
||||
{
|
||||
id: "q1",
|
||||
rowId: 1,
|
||||
title: "Category 1",
|
||||
__typename: "Category"
|
||||
title: "Question 1?",
|
||||
description: "Further information for Q1",
|
||||
categoryByCategoryRowId: {
|
||||
id: "c1",
|
||||
rowId: 1,
|
||||
title: "Category 1",
|
||||
__typename: "Category",
|
||||
},
|
||||
__typename: "Question",
|
||||
},
|
||||
__typename: "Question",
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
rowId: 2,
|
||||
title: "Question 2?",
|
||||
description: "Further information for Q2",
|
||||
categoryByCategoryRowId: null,
|
||||
|
@ -29,14 +31,17 @@ export const questionNodesMock: Array<BasicQuestionResponse> = [{
|
|||
},
|
||||
{
|
||||
id: "q3",
|
||||
rowId: 3,
|
||||
title: "Question 3?",
|
||||
description: null,
|
||||
categoryByCategoryRowId: null,
|
||||
__typename: "Question",
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const getAllQuestionsMock: Array<MockedResponse<GetAllQuestionsResponse>> = [
|
||||
export const getAllQuestionsMock: Array<
|
||||
MockedResponse<GetAllQuestionsResponse>
|
||||
> = [
|
||||
{
|
||||
request: {
|
||||
query: GET_ALL_QUESTIONS,
|
||||
|
@ -46,14 +51,16 @@ export const getAllQuestionsMock: Array<MockedResponse<GetAllQuestionsResponse>>
|
|||
allQuestions: {
|
||||
nodes: questionNodesMock,
|
||||
__typename: "QuestionsConnection",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export const getQuestionByIdMock: Array<MockedResponse<GetQuestionByIdResponse>> = [
|
||||
...questionNodesMock.map(q => ({
|
||||
export const getQuestionByIdMock: Array<
|
||||
MockedResponse<GetQuestionByIdResponse>
|
||||
> = [
|
||||
...questionNodesMock.map((q) => ({
|
||||
request: {
|
||||
query: GET_QUESTION_BY_ID,
|
||||
variables: {
|
||||
|
@ -78,6 +85,5 @@ export const getQuestionByIdMock: Array<MockedResponse<GetQuestionByIdResponse>>
|
|||
question: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {gql} from "@apollo/client";
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
const QuestionCategoryFragment = gql`
|
||||
fragment QuestionCategoryFragment on Category {
|
||||
|
@ -6,18 +6,19 @@ const QuestionCategoryFragment = gql`
|
|||
rowId
|
||||
title
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
interface GetQuestionsCategoryResponse {
|
||||
id: string,
|
||||
rowId: number,
|
||||
title: string,
|
||||
__typename: "Category",
|
||||
id: string;
|
||||
rowId: number;
|
||||
title: string;
|
||||
__typename: "Category";
|
||||
}
|
||||
|
||||
export const BasicQuestionFragment = gql`
|
||||
fragment BasicQuestionFragment on Question {
|
||||
id
|
||||
rowId
|
||||
title
|
||||
description
|
||||
categoryByCategoryRowId {
|
||||
|
@ -25,14 +26,15 @@ export const BasicQuestionFragment = gql`
|
|||
}
|
||||
}
|
||||
${QuestionCategoryFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface BasicQuestionResponse {
|
||||
id: string,
|
||||
title: string,
|
||||
description: string | null,
|
||||
categoryByCategoryRowId: GetQuestionsCategoryResponse | null,
|
||||
__typename: "Question",
|
||||
id: string;
|
||||
rowId: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
categoryByCategoryRowId: GetQuestionsCategoryResponse | null;
|
||||
__typename: "Question";
|
||||
}
|
||||
|
||||
export const GET_ALL_QUESTIONS = gql`
|
||||
|
@ -44,28 +46,28 @@ export const GET_ALL_QUESTIONS = gql`
|
|||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface GetAllQuestionsResponse {
|
||||
allQuestions: {
|
||||
nodes: Array<BasicQuestionResponse>,
|
||||
__typename: "QuestionsConnection",
|
||||
}
|
||||
nodes: Array<BasicQuestionResponse>;
|
||||
__typename: "QuestionsConnection";
|
||||
};
|
||||
}
|
||||
|
||||
export const GET_QUESTION_BY_ID = gql`
|
||||
query GetQuestionById($id:ID!) {
|
||||
query GetQuestionById($id: ID!) {
|
||||
question(id: $id) {
|
||||
...BasicQuestionFragment
|
||||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
`;
|
||||
|
||||
export interface GetQuestionByIdResponse {
|
||||
question: BasicQuestionResponse | null,
|
||||
question: BasicQuestionResponse | null;
|
||||
}
|
||||
|
||||
export interface GetQuestionByIdVariables {
|
||||
id: string,
|
||||
id: string;
|
||||
}
|
||||
|
|
96
redaktions-app/src/components/AccordionQuestionAnswer.tsx
Normal file
96
redaktions-app/src/components/AccordionQuestionAnswer.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import React from "react";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Accordion from "@material-ui/core/Accordion";
|
||||
import AccordionDetails from "@material-ui/core/AccordionDetails";
|
||||
import AccordionSummary from "@material-ui/core/AccordionSummary";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import {
|
||||
CandidatePosition,
|
||||
getIconForPosition,
|
||||
} from "./CandidatePositionLegend";
|
||||
import { QuestionAnswerResponse } from "../backend/queries/answer";
|
||||
import EditAnswerSection from "./EditAnswerSection";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: "100%",
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
heading: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
flexGrow: 1,
|
||||
},
|
||||
secondaryHeading: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
details: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
questionDetails: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
positionIcon: {
|
||||
marginLeft: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
interface AccordionQuestionAnswerProps {
|
||||
personRowId: number;
|
||||
question: QuestionAnswerResponse;
|
||||
}
|
||||
|
||||
export default function AccordionQuestionAnswer(
|
||||
props: AccordionQuestionAnswerProps
|
||||
): React.ReactElement {
|
||||
const {
|
||||
rowId: questionRowId,
|
||||
title: questionTitle,
|
||||
description: questionDetails,
|
||||
} = props.question;
|
||||
const position = props.question.answersByQuestionRowId.nodes[0]?.position;
|
||||
const questionCategory = props.question.categoryByCategoryRowId?.title;
|
||||
const classes = useStyles();
|
||||
const answerPosition =
|
||||
position !== undefined ? position : CandidatePosition.skipped;
|
||||
|
||||
return (
|
||||
<div className={classes.root} key={questionRowId}>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1c-content"
|
||||
id="panel1c-header"
|
||||
>
|
||||
<div className={classes.heading}>
|
||||
<Typography>{questionTitle}</Typography>
|
||||
</div>
|
||||
<div className={classes.secondaryHeading}>
|
||||
<Typography>{questionCategory}</Typography>
|
||||
</div>
|
||||
<div className={classes.positionIcon}>
|
||||
{getIconForPosition(answerPosition)}
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={classes.details}>
|
||||
<Typography
|
||||
className={classes.questionDetails}
|
||||
color="textSecondary"
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
>
|
||||
{questionDetails}
|
||||
</Typography>
|
||||
<Divider />
|
||||
<EditAnswerSection
|
||||
personRowId={props.personRowId}
|
||||
question={props.question}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
import React from 'react';
|
||||
import {createStyles, makeStyles, Theme} from '@material-ui/core/styles';
|
||||
import Accordion from '@material-ui/core/Accordion';
|
||||
import AccordionDetails from '@material-ui/core/AccordionDetails';
|
||||
import AccordionSummary from '@material-ui/core/AccordionSummary';
|
||||
import AccordionActions from '@material-ui/core/AccordionActions';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import {IconButton} from '@material-ui/core';
|
||||
import React from "react";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Accordion from "@material-ui/core/Accordion";
|
||||
import AccordionDetails from "@material-ui/core/AccordionDetails";
|
||||
import AccordionSummary from "@material-ui/core/AccordionSummary";
|
||||
import AccordionActions from "@material-ui/core/AccordionActions";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import { IconButton } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(1)
|
||||
width: "100%",
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
heading: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
|
@ -26,28 +26,30 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||
color: theme.palette.text.secondary,
|
||||
},
|
||||
details: {
|
||||
alignItems: 'center',
|
||||
alignItems: "center",
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
interface AccordionWithEditProps {
|
||||
key: string,
|
||||
title: string,
|
||||
description: string | null,
|
||||
subTitle?: string | null,
|
||||
onEditButtonClick?(): void,
|
||||
onDeleteButtonClick?(): void,
|
||||
key: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
subTitle?: string | null;
|
||||
onEditButtonClick?(): void;
|
||||
onDeleteButtonClick?(): void;
|
||||
}
|
||||
|
||||
export default function AccordionWithEdit(props: AccordionWithEditProps) {
|
||||
export default function AccordionWithEdit(
|
||||
props: AccordionWithEditProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon/>}
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1c-content"
|
||||
id="panel1c-header"
|
||||
>
|
||||
|
@ -59,20 +61,30 @@ export default function AccordionWithEdit(props: AccordionWithEditProps) {
|
|||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={classes.details}>
|
||||
<Typography color="textSecondary" style={{whiteSpace: "pre-line"}}>
|
||||
{props.description}
|
||||
<Typography color="textSecondary" style={{ whiteSpace: "pre-line" }}>
|
||||
{props.description}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
<Divider/>
|
||||
<Divider />
|
||||
<AccordionActions>
|
||||
<IconButton data-testid="edit-icon-button" size={"small"} aria-label="edit" onClick={props.onEditButtonClick}>
|
||||
<EditIcon titleAccess="Anpassen"/>
|
||||
<IconButton
|
||||
data-testid="edit-icon-button"
|
||||
size={"small"}
|
||||
aria-label="edit"
|
||||
onClick={props.onEditButtonClick}
|
||||
>
|
||||
<EditIcon titleAccess="Anpassen" />
|
||||
</IconButton>
|
||||
<IconButton data-testid="delete-icon-button" size={"small"} aria-label="delete" onClick={props.onDeleteButtonClick}>
|
||||
<DeleteIcon titleAccess="Löschen"/>
|
||||
<IconButton
|
||||
data-testid="delete-icon-button"
|
||||
size={"small"}
|
||||
aria-label="delete"
|
||||
onClick={props.onDeleteButtonClick}
|
||||
>
|
||||
<DeleteIcon titleAccess="Löschen" />
|
||||
</IconButton>
|
||||
</AccordionActions>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
import {Card, CardActionArea, CardContent} from "@material-ui/core";
|
||||
import { Card, CardActionArea, CardContent } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
addCardContent: {
|
||||
padding: 0,
|
||||
},
|
||||
addCardIcon: {
|
||||
display: 'block',
|
||||
margin: 'auto',
|
||||
display: "block",
|
||||
margin: "auto",
|
||||
padding: 12,
|
||||
},
|
||||
}));
|
||||
|
||||
interface AddCardProps {
|
||||
handleClick?(): void
|
||||
handleClick?(): void;
|
||||
}
|
||||
|
||||
export default function AddCard(props: AddCardProps) {
|
||||
export default function AddCard(props: AddCardProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<CardActionArea onClick={props.handleClick}>
|
||||
<CardContent color={"textSecondary"} className={classes.addCardContent}>
|
||||
<AddIcon className={classes.addCardIcon}/>
|
||||
<AddIcon className={classes.addCardIcon} />
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,57 +1,63 @@
|
|||
import React from 'react';
|
||||
import {createStyles, makeStyles, Theme} from '@material-ui/core/styles';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import {green} from '@material-ui/core/colors';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import React from "react";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import { green } from "@material-ui/core/colors";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import { PropTypes } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
wrapper: {
|
||||
margin: theme.spacing(1),
|
||||
position: 'relative',
|
||||
position: "relative",
|
||||
},
|
||||
buttonProgress: {
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
marginTop: -12,
|
||||
marginLeft: -12,
|
||||
},
|
||||
button: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
interface ButtonWithSpinnerProps {
|
||||
children: string,
|
||||
onClick?: () => void,
|
||||
loading?: boolean
|
||||
type?: "button" | "submit",
|
||||
fullWidth?: boolean,
|
||||
autoFocus?: boolean
|
||||
children: string;
|
||||
onClick?: () => void;
|
||||
loading?: boolean;
|
||||
type?: "button" | "submit";
|
||||
fullWidth?: boolean;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
color?: PropTypes.Color;
|
||||
}
|
||||
|
||||
export default function ButtonWithSpinner(props: ButtonWithSpinnerProps) {
|
||||
export default function ButtonWithSpinner(
|
||||
props: ButtonWithSpinnerProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth={!!props.fullWidth}
|
||||
type={props.type}
|
||||
disabled={props.loading}
|
||||
onClick={props.onClick}
|
||||
autoFocus={props.autoFocus}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
{props.loading && <CircularProgress size={24} className={classes.buttonProgress} />}
|
||||
</div>
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
className={`${classes.button} ${props.className}`}
|
||||
variant="contained"
|
||||
color={props.color || "primary"}
|
||||
fullWidth={!!props.fullWidth}
|
||||
type={props.type}
|
||||
disabled={props.loading}
|
||||
onClick={props.onClick}
|
||||
autoFocus={props.autoFocus}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
{props.loading && (
|
||||
<CircularProgress size={24} className={classes.buttonProgress} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
75
redaktions-app/src/components/CandidatePositionLegend.tsx
Normal file
75
redaktions-app/src/components/CandidatePositionLegend.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Chip, SvgIconProps } from "@material-ui/core";
|
||||
import ThumbUpIcon from "@material-ui/icons/ThumbUp";
|
||||
import RadioButtonUncheckedIcon from "@material-ui/icons/RadioButtonUnchecked";
|
||||
import { ThumbDown } from "@material-ui/icons";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
export enum CandidatePosition {
|
||||
positive,
|
||||
neutral,
|
||||
negative,
|
||||
skipped,
|
||||
}
|
||||
|
||||
export const allPositions = [
|
||||
CandidatePosition.positive,
|
||||
CandidatePosition.neutral,
|
||||
CandidatePosition.negative,
|
||||
CandidatePosition.skipped,
|
||||
];
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
legend: {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
chip: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export function CandidatePositionLegend(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
const getChip = (position: CandidatePosition, legendText: string) => {
|
||||
return (
|
||||
<Chip
|
||||
label={legendText}
|
||||
color="primary"
|
||||
icon={getIconForPosition(position, { fontSize: "inherit" })}
|
||||
variant="outlined"
|
||||
className={classes.chip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.legend}>
|
||||
{getChip(CandidatePosition.positive, "Ich bin dafür")}
|
||||
{getChip(CandidatePosition.neutral, "Neutral")}
|
||||
{getChip(CandidatePosition.negative, "Ich bin dagegen")}
|
||||
{getChip(CandidatePosition.skipped, "Frage überspringen")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const getIconForPosition = (
|
||||
position: CandidatePosition,
|
||||
props?: SvgIconProps
|
||||
): JSX.Element => {
|
||||
switch (position) {
|
||||
case CandidatePosition.positive:
|
||||
return <ThumbUpIcon {...props} />;
|
||||
case CandidatePosition.neutral:
|
||||
return <RadioButtonUncheckedIcon {...props} />;
|
||||
case CandidatePosition.negative:
|
||||
return <ThumbDown {...props} />;
|
||||
case CandidatePosition.skipped:
|
||||
return <CloseIcon {...props} />;
|
||||
}
|
||||
};
|
|
@ -1,60 +1,71 @@
|
|||
import {Paper, Typography} from "@material-ui/core";
|
||||
import { Paper, Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {useQuery} from "@apollo/client";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import AddCard from "./AddCard";
|
||||
import AccordionWithEdit from "./AccordionWithEdit";
|
||||
import {BasicCategoryResponse, GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "../backend/queries/category";
|
||||
import DialogChangeCategory, {dialogChangeCategoryId, dialogChangeCategoryOpen} from "./DialogChangeCategory";
|
||||
import {
|
||||
BasicCategoryResponse,
|
||||
GET_ALL_CATEGORIES,
|
||||
GetAllCategoriesResponse,
|
||||
} from "../backend/queries/category";
|
||||
import DialogChangeCategory, {
|
||||
dialogChangeCategoryId,
|
||||
dialogChangeCategoryOpen,
|
||||
} from "./DialogChangeCategory";
|
||||
import DialogDeleteCategory, {
|
||||
dialogDeleteCategoryId,
|
||||
dialogDeleteCategoryOpen,
|
||||
dialogDeleteCategoryTitle
|
||||
dialogDeleteCategoryTitle,
|
||||
} from "./DialogDeleteCategory";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CategoryList() {
|
||||
const categories = useQuery<GetAllCategoriesResponse, null>(GET_ALL_CATEGORIES).data?.allCategories.nodes;
|
||||
export default function CategoryList(): React.ReactElement {
|
||||
const categories = useQuery<GetAllCategoriesResponse, null>(
|
||||
GET_ALL_CATEGORIES
|
||||
).data?.allCategories.nodes;
|
||||
const classes = useStyles();
|
||||
|
||||
const handleAddClick = () => {
|
||||
dialogChangeCategoryId("")
|
||||
dialogChangeCategoryOpen(true)
|
||||
}
|
||||
dialogChangeCategoryId("");
|
||||
dialogChangeCategoryOpen(true);
|
||||
};
|
||||
|
||||
const handleEditButtonClick = (category: BasicCategoryResponse) => {
|
||||
dialogChangeCategoryId(category.id);
|
||||
dialogChangeCategoryOpen(true)
|
||||
dialogChangeCategoryOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteButtonClick = (category: BasicCategoryResponse) => {
|
||||
dialogDeleteCategoryTitle(category.title);
|
||||
dialogDeleteCategoryId(category.id);
|
||||
dialogDeleteCategoryOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>Kategorien</Typography>
|
||||
{categories?.map(category => <AccordionWithEdit
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
|
||||
Kategorien
|
||||
</Typography>
|
||||
{categories?.map((category) => (
|
||||
<AccordionWithEdit
|
||||
key={category.id}
|
||||
title={category.title}
|
||||
description={category.description}
|
||||
onEditButtonClick={() => handleEditButtonClick(category)}
|
||||
onDeleteButtonClick={() => handleDeleteButtonClick(category)}
|
||||
/>
|
||||
)}
|
||||
<AddCard handleClick={handleAddClick}/>
|
||||
<DialogChangeCategory/>
|
||||
<DialogDeleteCategory/>
|
||||
))}
|
||||
<AddCard handleClick={handleAddClick} />
|
||||
<DialogChangeCategory />
|
||||
<DialogDeleteCategory />
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import React, {ChangeEvent} from 'react';
|
||||
import {FormControl, InputLabel, MenuItem, Select} from "@material-ui/core";
|
||||
import {BasicCategoryResponse} from "../backend/queries/category";
|
||||
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { FormControl, InputLabel, MenuItem, Select } from "@material-ui/core";
|
||||
import { BasicCategoryResponse } from "../backend/queries/category";
|
||||
|
||||
interface CategorySelectionMenuProps {
|
||||
selectedCategoryId: number | null
|
||||
categories?: Array<BasicCategoryResponse>,
|
||||
selectedCategoryId: number | null;
|
||||
categories?: Array<BasicCategoryResponse>;
|
||||
|
||||
handleCategoryChange(categoryId: number | null): void
|
||||
handleCategoryChange(categoryId: number | null): void;
|
||||
}
|
||||
|
||||
export default function CategorySelectionMenu(props: CategorySelectionMenuProps) {
|
||||
const onCategoryIdChange = (e: ChangeEvent<{ name?: string, value: unknown }>) => {
|
||||
const newValue = e.target.value === -1 ? null : e.target.value as number;
|
||||
export default function CategorySelectionMenu(
|
||||
props: CategorySelectionMenuProps
|
||||
): React.ReactElement {
|
||||
const onCategoryIdChange = (
|
||||
e: ChangeEvent<{ name?: string; value: unknown }>
|
||||
) => {
|
||||
const newValue = e.target.value === -1 ? null : (e.target.value as number);
|
||||
props.handleCategoryChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl fullWidth variant="outlined">
|
||||
|
@ -27,9 +30,11 @@ export default function CategorySelectionMenu(props: CategorySelectionMenuProps)
|
|||
<MenuItem value={-1}>
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{props.categories?.map(category => <MenuItem key={category.id} value={category.rowId}>
|
||||
{category.title}
|
||||
</MenuItem>)}
|
||||
{props.categories?.map((category) => (
|
||||
<MenuItem key={category.id} value={category.rowId}>
|
||||
{category.title}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
|
|
|
@ -2,15 +2,15 @@ import Typography from "@material-ui/core/Typography";
|
|||
import Link from "@material-ui/core/Link";
|
||||
import React from "react";
|
||||
|
||||
export function Copyright() {
|
||||
return (
|
||||
<Typography variant="body2" color="textSecondary" align="center">
|
||||
{'Copyright © '}
|
||||
<Link color="inherit" href="https://blog.netzbegruenung.de/">
|
||||
Netzbegruenung e.V.
|
||||
</Link>{' '}
|
||||
{new Date().getFullYear()}
|
||||
{'.'}
|
||||
</Typography>
|
||||
);
|
||||
export function Copyright(): React.ReactElement {
|
||||
return (
|
||||
<Typography variant="body2" color="textSecondary" align="center">
|
||||
{"Copyright © "}
|
||||
<Link color="inherit" href="https://blog.netzbegruenung.de/">
|
||||
Netzbegruenung e.V.
|
||||
</Link>{" "}
|
||||
{new Date().getFullYear()}
|
||||
{"."}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import {IconButton, MenuItem, Toolbar, Typography} from '@material-ui/core';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import React from "react";
|
||||
import AppBar from "@material-ui/core/AppBar";
|
||||
import { IconButton, MenuItem, Toolbar, Typography } from "@material-ui/core";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import Menu from "@material-ui/core/Menu";
|
||||
import AccountCircle from "@material-ui/icons/AccountCircle";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
menuButton: {
|
||||
|
@ -15,10 +14,10 @@ const useStyles = makeStyles({
|
|||
title: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
function CustomAppBar() {
|
||||
const classes = useStyles()
|
||||
function CustomAppBar(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const history = useHistory();
|
||||
|
@ -28,52 +27,57 @@ function CustomAppBar() {
|
|||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
history.push("/login")
|
||||
}
|
||||
localStorage.removeItem("token");
|
||||
history.push("/login");
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
return (
|
||||
<AppBar>
|
||||
<Toolbar>
|
||||
<IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="menu">
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
Candymat
|
||||
</Typography>
|
||||
<IconButton
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleClose}>Profil</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
return (
|
||||
<AppBar>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
className={classes.menuButton}
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
Candymat
|
||||
</Typography>
|
||||
<IconButton
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleClose}>Profil</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomAppBar
|
||||
export default CustomAppBar;
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
import {Button, DialogActions} from "@material-ui/core";
|
||||
import { Button, DialogActions } from "@material-ui/core";
|
||||
import ButtonWithSpinner from "./ButtonWithSpinner";
|
||||
import React from "react";
|
||||
|
||||
interface DialogSimpleActionProps {
|
||||
confirmButtonText?: string,
|
||||
loading?: boolean,
|
||||
confirmButtonText?: string;
|
||||
loading?: boolean;
|
||||
|
||||
onClose(): void,
|
||||
onClose(): void;
|
||||
|
||||
onConfirmButtonClick(): void,
|
||||
onConfirmButtonClick(): void;
|
||||
}
|
||||
|
||||
export function DialogActionBar(props: DialogSimpleActionProps) {
|
||||
return <DialogActions>
|
||||
<Button onClick={props.onClose} color="primary">
|
||||
Abbrechen
|
||||
</Button>
|
||||
<ButtonWithSpinner onClick={props.onConfirmButtonClick} autoFocus loading={props.loading}>
|
||||
{props.confirmButtonText || "Ok"}
|
||||
</ButtonWithSpinner>
|
||||
</DialogActions>;
|
||||
export function DialogActionBar(
|
||||
props: DialogSimpleActionProps
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} color="primary">
|
||||
Abbrechen
|
||||
</Button>
|
||||
<ButtonWithSpinner
|
||||
onClick={props.onConfirmButtonClick}
|
||||
autoFocus
|
||||
loading={props.loading}
|
||||
>
|
||||
{props.confirmButtonText || "Ok"}
|
||||
</ButtonWithSpinner>
|
||||
</DialogActions>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React, {useState} from 'react';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import {DialogActionBar} from "./DialogActionBar";
|
||||
import {DialogTitleAndDetails} from "./DialogTitleAndDetails";
|
||||
import {makeVar, useMutation, useQuery, useReactiveVar} from "@apollo/client";
|
||||
import {useSnackbar} from "notistack";
|
||||
import React, { useState } from "react";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
import { DialogActionBar } from "./DialogActionBar";
|
||||
import { DialogTitleAndDetails } from "./DialogTitleAndDetails";
|
||||
import { makeVar, useMutation, useQuery, useReactiveVar } from "@apollo/client";
|
||||
import { useSnackbar } from "notistack";
|
||||
import {
|
||||
BasicCategoryFragment,
|
||||
BasicCategoryResponse,
|
||||
GET_CATEGORY_BY_ID,
|
||||
GetCategoryByIdResponse,
|
||||
GetCategoryByIdVariables
|
||||
GetCategoryByIdVariables,
|
||||
} from "../backend/queries/category";
|
||||
import {
|
||||
ADD_CATEGORY,
|
||||
|
@ -19,65 +19,89 @@ import {
|
|||
AddCategoryVariables,
|
||||
EDIT_CATEGORY,
|
||||
EditCategoryResponse,
|
||||
EditCategoryVariables
|
||||
EditCategoryVariables,
|
||||
} from "../backend/mutations/category";
|
||||
|
||||
|
||||
export const dialogChangeCategoryId = makeVar<string>("");
|
||||
export const dialogChangeCategoryOpen = makeVar<boolean>(false);
|
||||
|
||||
export default function DialogChangeCategory() {
|
||||
export default function DialogChangeCategory(): React.ReactElement {
|
||||
const [addMode, setAddMode] = useState(true);
|
||||
const [title, setTitle] = useState("");
|
||||
const [details, setDetails] = useState("");
|
||||
const categoryId = useReactiveVar(dialogChangeCategoryId);
|
||||
const open = useReactiveVar(dialogChangeCategoryOpen);
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
useQuery<GetCategoryByIdResponse, GetCategoryByIdVariables>(GET_CATEGORY_BY_ID, {
|
||||
variables: {
|
||||
id: categoryId,
|
||||
},
|
||||
onCompleted: (data => {
|
||||
setAddMode(!data.category && !categoryId)
|
||||
setTitle(data.category?.title || "");
|
||||
setDetails(data.category?.description || "")
|
||||
})
|
||||
});
|
||||
const [editCategory, {loading: editLoading}] = useMutation<EditCategoryResponse, EditCategoryVariables>(EDIT_CATEGORY, {
|
||||
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
useQuery<GetCategoryByIdResponse, GetCategoryByIdVariables>(
|
||||
GET_CATEGORY_BY_ID,
|
||||
{
|
||||
variables: {
|
||||
id: categoryId,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
setAddMode(!data.category && !categoryId);
|
||||
setTitle(data.category?.title || "");
|
||||
setDetails(data.category?.description || "");
|
||||
},
|
||||
}
|
||||
);
|
||||
const [editCategory, { loading: editLoading }] = useMutation<
|
||||
EditCategoryResponse,
|
||||
EditCategoryVariables
|
||||
>(EDIT_CATEGORY, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
onCompleted: (response) => {
|
||||
if (response.updateCategory) {
|
||||
enqueueSnackbar("Kategorie erfolgreich geändert.", {variant: "success"})
|
||||
enqueueSnackbar("Kategorie erfolgreich geändert.", {
|
||||
variant: "success",
|
||||
});
|
||||
dialogChangeCategoryOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {variant: "error"})
|
||||
}
|
||||
}
|
||||
});
|
||||
const [addCategory, {loading: addLoading}] = useMutation<AddCategoryResponse, AddCategoryVariables>(ADD_CATEGORY, {
|
||||
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
|
||||
onCompleted: (response) => {
|
||||
if (response.createCategory) {
|
||||
enqueueSnackbar("Kategorie erfolgreich hinzugefügt.", {variant: "success"})
|
||||
dialogChangeCategoryOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {variant: "error"})
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: (cache, {data}) => {
|
||||
});
|
||||
const [addCategory, { loading: addLoading }] = useMutation<
|
||||
AddCategoryResponse,
|
||||
AddCategoryVariables
|
||||
>(ADD_CATEGORY, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
onCompleted: (response) => {
|
||||
if (response.createCategory) {
|
||||
enqueueSnackbar("Kategorie erfolgreich hinzugefügt.", {
|
||||
variant: "success",
|
||||
});
|
||||
dialogChangeCategoryOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
cache.modify({
|
||||
fields: {
|
||||
allCategories(existingCategories = {nodes: []}) {
|
||||
const newCategoryRef = cache.writeFragment<BasicCategoryResponse | undefined>({
|
||||
allCategories(existingCategories = { nodes: [] }) {
|
||||
const newCategoryRef = cache.writeFragment<
|
||||
BasicCategoryResponse | undefined
|
||||
>({
|
||||
data: data?.createCategory?.category,
|
||||
fragment: BasicCategoryFragment,
|
||||
fragmentName: "BasicCategoryFragment",
|
||||
});
|
||||
return {nodes: [...existingCategories.nodes, newCategoryRef]};
|
||||
}
|
||||
}
|
||||
return { nodes: [...existingCategories.nodes, newCategoryRef] };
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirmButtonClick = () => {
|
||||
|
@ -86,21 +110,25 @@ export default function DialogChangeCategory() {
|
|||
variables: {
|
||||
title,
|
||||
description: details,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
} else {
|
||||
editCategory({
|
||||
variables: {
|
||||
id: categoryId,
|
||||
title: title,
|
||||
description: details,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => dialogChangeCategoryOpen(false)} aria-labelledby="form-dialog-title">
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => dialogChangeCategoryOpen(false)}
|
||||
aria-labelledby="form-dialog-title"
|
||||
>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{addMode ? "Neue Kategorie erstellen" : "Kategorie bearbeiten"}
|
||||
</DialogTitle>
|
||||
|
@ -108,8 +136,8 @@ export default function DialogChangeCategory() {
|
|||
<DialogTitleAndDetails
|
||||
title={title}
|
||||
details={details}
|
||||
onTitleChange={newTitle => setTitle(newTitle)}
|
||||
onDetailsChange={newDetails => setDetails(newDetails)}
|
||||
onTitleChange={(newTitle) => setTitle(newTitle)}
|
||||
onDetailsChange={(newDetails) => setDetails(newDetails)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActionBar
|
||||
|
|
|
@ -1,88 +1,116 @@
|
|||
import React, {useState} from 'react';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import {DialogActionBar} from "./DialogActionBar";
|
||||
import {DialogTitleAndDetails} from "./DialogTitleAndDetails";
|
||||
import {makeVar, useMutation, useQuery, useReactiveVar} from "@apollo/client";
|
||||
import {useSnackbar} from "notistack";
|
||||
import React, { useState } from "react";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
import { DialogActionBar } from "./DialogActionBar";
|
||||
import { DialogTitleAndDetails } from "./DialogTitleAndDetails";
|
||||
import { makeVar, useMutation, useQuery, useReactiveVar } from "@apollo/client";
|
||||
import { useSnackbar } from "notistack";
|
||||
import {
|
||||
ADD_QUESTION,
|
||||
AddQuestionResponse,
|
||||
AddQuestionVariables,
|
||||
EDIT_QUESTION,
|
||||
EditQuestionResponse,
|
||||
EditQuestionVariables
|
||||
EditQuestionVariables,
|
||||
} from "../backend/mutations/question";
|
||||
import {
|
||||
BasicQuestionFragment,
|
||||
BasicQuestionResponse,
|
||||
GET_QUESTION_BY_ID,
|
||||
GetQuestionByIdResponse,
|
||||
GetQuestionByIdVariables
|
||||
GetQuestionByIdVariables,
|
||||
} from "../backend/queries/question";
|
||||
import CategorySelectionMenu from "./CategorySelectionMenu";
|
||||
import {GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "../backend/queries/category";
|
||||
import {
|
||||
GET_ALL_CATEGORIES,
|
||||
GetAllCategoriesResponse,
|
||||
} from "../backend/queries/category";
|
||||
|
||||
export const dialogChangeQuestionId = makeVar<string>("");
|
||||
export const dialogChangeQuestionOpen = makeVar<boolean>(false);
|
||||
|
||||
export default function DialogChangeQuestion() {
|
||||
export default function DialogChangeQuestion(): React.ReactElement {
|
||||
const [addMode, setAddMode] = useState(true);
|
||||
const [title, setTitle] = useState("");
|
||||
const [details, setDetails] = useState("");
|
||||
const [categoryRowId, setCategoryRowId] = useState<number | null>(null);
|
||||
const questionId = useReactiveVar(dialogChangeQuestionId);
|
||||
const open = useReactiveVar(dialogChangeQuestionOpen);
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
useQuery<GetQuestionByIdResponse, GetQuestionByIdVariables>(GET_QUESTION_BY_ID, {
|
||||
variables: {
|
||||
id: questionId,
|
||||
},
|
||||
onCompleted: (data => {
|
||||
setAddMode(!data.question && !questionId)
|
||||
setTitle(data.question?.title || "");
|
||||
setDetails(data.question?.description || "");
|
||||
setCategoryRowId(data.question?.categoryByCategoryRowId?.rowId || null)
|
||||
})
|
||||
})
|
||||
const categories = useQuery<GetAllCategoriesResponse, null>(GET_ALL_CATEGORIES).data?.allCategories.nodes;
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
useQuery<GetQuestionByIdResponse, GetQuestionByIdVariables>(
|
||||
GET_QUESTION_BY_ID,
|
||||
{
|
||||
variables: {
|
||||
id: questionId,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
setAddMode(!data.question && !questionId);
|
||||
setTitle(data.question?.title || "");
|
||||
setDetails(data.question?.description || "");
|
||||
setCategoryRowId(data.question?.categoryByCategoryRowId?.rowId || null);
|
||||
},
|
||||
}
|
||||
);
|
||||
const categories = useQuery<GetAllCategoriesResponse, null>(
|
||||
GET_ALL_CATEGORIES
|
||||
).data?.allCategories.nodes;
|
||||
|
||||
const [editQuestion, {loading: editLoading}] = useMutation<EditQuestionResponse, EditQuestionVariables>(EDIT_QUESTION, {
|
||||
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
|
||||
const [editQuestion, { loading: editLoading }] = useMutation<
|
||||
EditQuestionResponse,
|
||||
EditQuestionVariables
|
||||
>(EDIT_QUESTION, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
onCompleted: (response) => {
|
||||
if (response.updateQuestion) {
|
||||
enqueueSnackbar("Frage erfolgreich geändert.", {variant: "success"})
|
||||
enqueueSnackbar("Frage erfolgreich geändert.", { variant: "success" });
|
||||
dialogChangeQuestionOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {variant: "error"})
|
||||
}
|
||||
}
|
||||
});
|
||||
const [addQuestion, {loading: addLoading}] = useMutation<AddQuestionResponse, AddQuestionVariables>(ADD_QUESTION, {
|
||||
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
|
||||
onCompleted: (response) => {
|
||||
if (response.createQuestion) {
|
||||
enqueueSnackbar("Frage erfolgreich hinzugefügt.", {variant: "success"})
|
||||
dialogChangeQuestionOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {variant: "error"})
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: (cache, {data}) => {
|
||||
});
|
||||
const [addQuestion, { loading: addLoading }] = useMutation<
|
||||
AddQuestionResponse,
|
||||
AddQuestionVariables
|
||||
>(ADD_QUESTION, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
onCompleted: (response) => {
|
||||
if (response.createQuestion) {
|
||||
enqueueSnackbar("Frage erfolgreich hinzugefügt.", {
|
||||
variant: "success",
|
||||
});
|
||||
dialogChangeQuestionOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
cache.modify({
|
||||
fields: {
|
||||
allQuestions(existingQuestions = {nodes: []}) {
|
||||
const newQuestionRef = cache.writeFragment<BasicQuestionResponse | undefined>({
|
||||
allQuestions(existingQuestions = { nodes: [] }) {
|
||||
const newQuestionRef = cache.writeFragment<
|
||||
BasicQuestionResponse | undefined
|
||||
>({
|
||||
data: data?.createQuestion?.question,
|
||||
fragment: BasicQuestionFragment,
|
||||
fragmentName: "BasicQuestionFragment",
|
||||
});
|
||||
return {nodes: [...existingQuestions.nodes, newQuestionRef]};
|
||||
}
|
||||
}
|
||||
return { nodes: [...existingQuestions.nodes, newQuestionRef] };
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirmButtonClick = () => {
|
||||
|
@ -92,8 +120,8 @@ export default function DialogChangeQuestion() {
|
|||
title,
|
||||
description: details,
|
||||
categoryRowId: categoryRowId,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
} else {
|
||||
editQuestion({
|
||||
variables: {
|
||||
|
@ -101,13 +129,17 @@ export default function DialogChangeQuestion() {
|
|||
title: title,
|
||||
description: details,
|
||||
categoryRowId: categoryRowId,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => dialogChangeQuestionOpen(false)} aria-labelledby="form-dialog-title">
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => dialogChangeQuestionOpen(false)}
|
||||
aria-labelledby="form-dialog-title"
|
||||
>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{addMode ? "Neue Frage erstellen" : "Frage bearbeiten"}
|
||||
</DialogTitle>
|
||||
|
@ -115,8 +147,8 @@ export default function DialogChangeQuestion() {
|
|||
<DialogTitleAndDetails
|
||||
title={title}
|
||||
details={details}
|
||||
onTitleChange={newTitle => setTitle(newTitle)}
|
||||
onDetailsChange={newDetails => setDetails(newDetails)}
|
||||
onTitleChange={(newTitle) => setTitle(newTitle)}
|
||||
onDetailsChange={(newDetails) => setDetails(newDetails)}
|
||||
/>
|
||||
<CategorySelectionMenu
|
||||
selectedCategoryId={categoryRowId}
|
||||
|
|
|
@ -1,37 +1,61 @@
|
|||
import React from 'react';
|
||||
import {makeVar, Reference, useMutation, useReactiveVar} from "@apollo/client";
|
||||
import React from "react";
|
||||
import {
|
||||
makeVar,
|
||||
Reference,
|
||||
useMutation,
|
||||
useReactiveVar,
|
||||
} from "@apollo/client";
|
||||
import DialogSimple from "./DialogSimple";
|
||||
import {useSnackbar} from "notistack";
|
||||
import {DELETE_CATEGORY, DeleteCategoryResponse, DeleteCategoryVariables} from "../backend/mutations/category";
|
||||
|
||||
import { useSnackbar } from "notistack";
|
||||
import {
|
||||
DELETE_CATEGORY,
|
||||
DeleteCategoryResponse,
|
||||
DeleteCategoryVariables,
|
||||
} from "../backend/mutations/category";
|
||||
|
||||
export const dialogDeleteCategoryId = makeVar<string>("");
|
||||
export const dialogDeleteCategoryTitle = makeVar<string>("");
|
||||
export const dialogDeleteCategoryOpen = makeVar<boolean>(false);
|
||||
|
||||
export default function DialogDeleteCategory() {
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [deleteCategory, {loading}] = useMutation<DeleteCategoryResponse, DeleteCategoryVariables>(DELETE_CATEGORY, {
|
||||
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
|
||||
export default function DialogDeleteCategory(): React.ReactElement {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [deleteCategory, { loading }] = useMutation<
|
||||
DeleteCategoryResponse,
|
||||
DeleteCategoryVariables
|
||||
>(DELETE_CATEGORY, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
onCompleted: (response) => {
|
||||
if (response.deleteCategory) {
|
||||
enqueueSnackbar("Kategorie erfolgreich gelöscht.", {variant: "success"})
|
||||
enqueueSnackbar("Kategorie erfolgreich gelöscht.", {
|
||||
variant: "success",
|
||||
});
|
||||
dialogDeleteCategoryOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {variant: "error"})
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: (cache, {data}) => {
|
||||
update: (cache, { data }) => {
|
||||
const idToRemove = data?.deleteCategory?.category.id;
|
||||
cache.modify({
|
||||
fields: {
|
||||
allCategories(existingCategoriesRef: { nodes: Array<Reference> } = {nodes: []}, {readField}) {
|
||||
console.log("existingCategory: ", existingCategoriesRef)
|
||||
return {nodes: existingCategoriesRef.nodes.filter(categoryRef => readField('id', categoryRef) !== idToRemove)};
|
||||
}
|
||||
}
|
||||
allCategories(
|
||||
existingCategoriesRef: { nodes: Array<Reference> } = { nodes: [] },
|
||||
{ readField }
|
||||
) {
|
||||
return {
|
||||
nodes: existingCategoriesRef.nodes.filter(
|
||||
(categoryRef) => readField("id", categoryRef) !== idToRemove
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const open = useReactiveVar(dialogDeleteCategoryOpen);
|
||||
|
@ -41,10 +65,10 @@ export default function DialogDeleteCategory() {
|
|||
const handleConfirmButtonClick = () => {
|
||||
deleteCategory({
|
||||
variables: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogSimple
|
||||
|
@ -58,4 +82,3 @@ export default function DialogDeleteCategory() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +1,59 @@
|
|||
import React from 'react';
|
||||
import {makeVar, Reference, useMutation, useReactiveVar} from "@apollo/client";
|
||||
import React from "react";
|
||||
import {
|
||||
makeVar,
|
||||
Reference,
|
||||
useMutation,
|
||||
useReactiveVar,
|
||||
} from "@apollo/client";
|
||||
import DialogSimple from "./DialogSimple";
|
||||
import {DELETE_QUESTION, DeleteQuestionResponse, DeleteQuestionVariables} from "../backend/mutations/question";
|
||||
import {useSnackbar} from "notistack";
|
||||
|
||||
import {
|
||||
DELETE_QUESTION,
|
||||
DeleteQuestionResponse,
|
||||
DeleteQuestionVariables,
|
||||
} from "../backend/mutations/question";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
export const dialogDeleteQuestionId = makeVar<string>("");
|
||||
export const dialogDeleteQuestionTitle = makeVar<string>("");
|
||||
export const dialogDeleteQuestionOpen = makeVar<boolean>(false);
|
||||
|
||||
export default function DialogDeleteQuestion() {
|
||||
const {enqueueSnackbar} = useSnackbar();
|
||||
const [deleteQuestion, {loading}] = useMutation<DeleteQuestionResponse, DeleteQuestionVariables>(DELETE_QUESTION, {
|
||||
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
|
||||
export default function DialogDeleteQuestion(): React.ReactElement {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [deleteQuestion, { loading }] = useMutation<
|
||||
DeleteQuestionResponse,
|
||||
DeleteQuestionVariables
|
||||
>(DELETE_QUESTION, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
onCompleted: (response) => {
|
||||
if (response.deleteQuestion) {
|
||||
enqueueSnackbar("Frage erfolgreich gelöscht.", {variant: "success"})
|
||||
enqueueSnackbar("Frage erfolgreich gelöscht.", { variant: "success" });
|
||||
dialogDeleteQuestionOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {variant: "error"})
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
update: (cache, {data}) => {
|
||||
update: (cache, { data }) => {
|
||||
const idToRemove = data?.deleteQuestion?.question.id;
|
||||
cache.modify({
|
||||
fields: {
|
||||
allQuestions(existingQuestionsRef: { nodes: Array<Reference> } = {nodes: []}, {readField}) {
|
||||
return {nodes: existingQuestionsRef.nodes.filter(questionRef => readField('id', questionRef) !== idToRemove)};
|
||||
}
|
||||
}
|
||||
allQuestions(
|
||||
existingQuestionsRef: { nodes: Array<Reference> } = { nodes: [] },
|
||||
{ readField }
|
||||
) {
|
||||
return {
|
||||
nodes: existingQuestionsRef.nodes.filter(
|
||||
(questionRef) => readField("id", questionRef) !== idToRemove
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const open = useReactiveVar(dialogDeleteQuestionOpen);
|
||||
|
@ -40,10 +63,10 @@ export default function DialogDeleteQuestion() {
|
|||
const handleConfirmButtonClick = () => {
|
||||
deleteQuestion({
|
||||
variables: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogSimple
|
||||
|
@ -57,4 +80,3 @@ export default function DialogDeleteQuestion() {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
import React from 'react';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import {DialogContentText} from "@material-ui/core";
|
||||
import {DialogActionBar} from "./DialogActionBar";
|
||||
|
||||
import React from "react";
|
||||
import Dialog from "@material-ui/core/Dialog";
|
||||
import DialogContent from "@material-ui/core/DialogContent";
|
||||
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||
import { DialogContentText } from "@material-ui/core";
|
||||
import { DialogActionBar } from "./DialogActionBar";
|
||||
|
||||
interface DialogSimpleProps {
|
||||
open: boolean,
|
||||
title: string,
|
||||
confirmButtonText: string,
|
||||
description: string,
|
||||
loading?: boolean,
|
||||
open: boolean;
|
||||
title: string;
|
||||
confirmButtonText: string;
|
||||
description: string;
|
||||
loading?: boolean;
|
||||
|
||||
onConfirmButtonClick(): void,
|
||||
onConfirmButtonClick(): void;
|
||||
|
||||
onClose(): void,
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export default function DialogSimple(props: DialogSimpleProps) {
|
||||
export default function DialogSimple(
|
||||
props: DialogSimpleProps
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
|
@ -40,4 +41,3 @@ export default function DialogSimple(props: DialogSimpleProps) {
|
|||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
textField: {
|
||||
marginBottom: theme.spacing(2),
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
interface DialogTitleAndDetailsProps {
|
||||
title: string,
|
||||
details?: string | null,
|
||||
title: string;
|
||||
details?: string | null;
|
||||
|
||||
onTitleChange(newTitle: string): void,
|
||||
onTitleChange(newTitle: string): void;
|
||||
|
||||
onDetailsChange(newDetails: string): void,
|
||||
onDetailsChange(newDetails: string): void;
|
||||
}
|
||||
|
||||
export function DialogTitleAndDetails(props: DialogTitleAndDetailsProps) {
|
||||
export function DialogTitleAndDetails(
|
||||
props: DialogTitleAndDetailsProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
|
@ -29,7 +31,7 @@ export function DialogTitleAndDetails(props: DialogTitleAndDetailsProps) {
|
|||
fullWidth
|
||||
variant="outlined"
|
||||
value={props.title}
|
||||
onChange={e => props.onTitleChange(e.target.value)}
|
||||
onChange={(e) => props.onTitleChange(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
className={classes.textField}
|
||||
|
@ -40,8 +42,8 @@ export function DialogTitleAndDetails(props: DialogTitleAndDetailsProps) {
|
|||
fullWidth
|
||||
variant="outlined"
|
||||
value={props.details}
|
||||
onChange={e => props.onDetailsChange(e.target.value)}
|
||||
onChange={(e) => props.onDetailsChange(e.target.value)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
159
redaktions-app/src/components/EditAnswerSection.tsx
Normal file
159
redaktions-app/src/components/EditAnswerSection.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import React from "react";
|
||||
import { CandidatePosition } from "./CandidatePositionLegend";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import {
|
||||
FullAnswerResponse,
|
||||
GET_ANSWER_BY_QUESTION_AND_PERSON,
|
||||
GetAnswerByQuestionAndPersonResponse,
|
||||
GetAnswerByQuestionAndPersonVariables,
|
||||
QuestionAnswerResponse,
|
||||
} from "../backend/queries/answer";
|
||||
import { useSnackbar } from "notistack";
|
||||
import {
|
||||
ADD_ANSWER,
|
||||
AddAnswerResponse,
|
||||
AddAnswerVariables,
|
||||
EDIT_ANSWER,
|
||||
EditAnswerResponse,
|
||||
EditAnswerVariables,
|
||||
updateCacheAfterAddingAnswer,
|
||||
} from "../backend/mutations/answer";
|
||||
import ToggleButtonGroupAnswerPosition from "./ToggleButtonGroupAnswerPosition";
|
||||
import EditAnswerText from "./EditAnswerText";
|
||||
|
||||
interface EditAnswerSectionProps {
|
||||
personRowId: number;
|
||||
question: QuestionAnswerResponse;
|
||||
}
|
||||
|
||||
export default function EditAnswerSection(
|
||||
props: EditAnswerSectionProps
|
||||
): React.ReactElement {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { data } = useQuery<
|
||||
GetAnswerByQuestionAndPersonResponse,
|
||||
GetAnswerByQuestionAndPersonVariables
|
||||
>(GET_ANSWER_BY_QUESTION_AND_PERSON, {
|
||||
variables: {
|
||||
personRowId: props.personRowId,
|
||||
questionRowId: props.question.rowId,
|
||||
},
|
||||
});
|
||||
const remoteAnswer = data?.answerByQuestionRowIdAndPersonRowId;
|
||||
const [editAnswer, { loading: editAnswerLoading }] = useMutation<
|
||||
EditAnswerResponse,
|
||||
EditAnswerVariables
|
||||
>(EDIT_ANSWER, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
});
|
||||
const [addAnswer, { loading: addAnswerLoading }] = useMutation<
|
||||
AddAnswerResponse,
|
||||
AddAnswerVariables
|
||||
>(ADD_ANSWER, {
|
||||
onError: (e) =>
|
||||
enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {
|
||||
variant: "error",
|
||||
}),
|
||||
update: (cache, fetchResult) =>
|
||||
updateCacheAfterAddingAnswer(cache, fetchResult, props.question),
|
||||
});
|
||||
|
||||
const parsePosition = (position?: CandidatePosition): CandidatePosition => {
|
||||
return position !== undefined ? position : CandidatePosition.skipped;
|
||||
};
|
||||
const changeAnswer = async (
|
||||
position?: CandidatePosition,
|
||||
text?: string
|
||||
): Promise<FullAnswerResponse | undefined> => {
|
||||
if (remoteAnswer) {
|
||||
const optimisticResponseAnswer = {
|
||||
...remoteAnswer,
|
||||
...(position !== undefined && { position }),
|
||||
...(text !== undefined && { text }),
|
||||
};
|
||||
const response = await editAnswer({
|
||||
variables: {
|
||||
id: remoteAnswer.id,
|
||||
position,
|
||||
text,
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateAnswer: {
|
||||
__typename: "UpdateAnswerPayload",
|
||||
answer: optimisticResponseAnswer,
|
||||
},
|
||||
},
|
||||
});
|
||||
return response.data?.updateAnswer?.answer;
|
||||
} else {
|
||||
const savePosition = parsePosition(position);
|
||||
const response = await addAnswer({
|
||||
variables: {
|
||||
personRowId: props.personRowId,
|
||||
questionRowId: props.question.rowId,
|
||||
position: savePosition,
|
||||
text: text,
|
||||
},
|
||||
optimisticResponse: {
|
||||
createAnswer: {
|
||||
__typename: "CreateAnswerPayload",
|
||||
answer: {
|
||||
id: "somethingIntermediate",
|
||||
position: savePosition,
|
||||
text: text || null,
|
||||
personRowId: props.personRowId,
|
||||
questionRowId: props.question.rowId,
|
||||
__typename: "Answer",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return response.data?.createAnswer?.answer;
|
||||
}
|
||||
};
|
||||
const handleSaveText = async (text: string) => {
|
||||
const newAnswer = await changeAnswer(undefined, text);
|
||||
if (newAnswer) {
|
||||
enqueueSnackbar("Antwort erfolgreich gespeichert.", {
|
||||
variant: "success",
|
||||
});
|
||||
} else {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
const handlePositionChange = async (
|
||||
e: React.MouseEvent<HTMLElement>,
|
||||
newPosition: CandidatePosition
|
||||
) => {
|
||||
const newAnswer = await changeAnswer(newPosition);
|
||||
if (!newAnswer) {
|
||||
enqueueSnackbar("Ein Fehler ist aufgetreten, versuche es erneut.", {
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
const loading = editAnswerLoading || addAnswerLoading;
|
||||
const position = parsePosition(remoteAnswer?.position);
|
||||
|
||||
return remoteAnswer === undefined ? (
|
||||
<div>Antwort laden...</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<ToggleButtonGroupAnswerPosition
|
||||
position={position}
|
||||
onPositionChange={handlePositionChange}
|
||||
loading={loading}
|
||||
/>
|
||||
<EditAnswerText
|
||||
remoteText={remoteAnswer?.text}
|
||||
onSaveClick={handleSaveText}
|
||||
loading={loading}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
75
redaktions-app/src/components/EditAnswerText.tsx
Normal file
75
redaktions-app/src/components/EditAnswerText.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import React, { useState } from "react";
|
||||
import { FormLabel } from "@material-ui/core";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import ButtonWithSpinner from "./ButtonWithSpinner";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
button: {
|
||||
marginLeft: "auto",
|
||||
marginRight: 0,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
},
|
||||
root: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
detailedAnswerActions: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
interface EditAnswerTextSectionProps {
|
||||
remoteText: string | null | undefined;
|
||||
loading?: boolean;
|
||||
|
||||
onSaveClick(text: string): void;
|
||||
}
|
||||
|
||||
export default function EditAnswerText(
|
||||
props: EditAnswerTextSectionProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const initialAnswer = props.remoteText || "Antwort hinzufügen...";
|
||||
const [answerText, setAnswerText] = useState<string>(initialAnswer);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormLabel>Detaillierte Antwort</FormLabel>
|
||||
<TextField
|
||||
multiline
|
||||
rows={4}
|
||||
id="description"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={answerText}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setAnswerText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className={classes.detailedAnswerActions}>
|
||||
<ButtonWithSpinner
|
||||
color="default"
|
||||
className={classes.button}
|
||||
onClick={() => setAnswerText(initialAnswer)}
|
||||
loading={props.loading}
|
||||
>
|
||||
Zurücksetzen
|
||||
</ButtonWithSpinner>
|
||||
<ButtonWithSpinner
|
||||
loading={props.loading}
|
||||
className={classes.button}
|
||||
onClick={() => props.onSaveClick(answerText)}
|
||||
>
|
||||
Speichern
|
||||
</ButtonWithSpinner>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,19 +1,27 @@
|
|||
import React from 'react';
|
||||
import {render, screen} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/client/testing';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import React from "react";
|
||||
import { render, screen } 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 { SnackbarProvider } from "notistack";
|
||||
|
||||
function renderMainPage() {
|
||||
render(<MockedProvider><MemoryRouter><SnackbarProvider><Main/></SnackbarProvider></MemoryRouter></MockedProvider>);
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<Main />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('The main page', () => {
|
||||
test('displays the editors page if an editor is logged in', () => {
|
||||
const editorToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6MSwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.kxdxmDrQw0vzD4tiXPj2fu-Cr8n7aWMikxntZ1ObF6c";
|
||||
localStorage.setItem("token", editorToken)
|
||||
describe("The main page", () => {
|
||||
test("displays the editors page if an editor is logged in", () => {
|
||||
const editorToken =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6MSwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.kxdxmDrQw0vzD4tiXPj2fu-Cr8n7aWMikxntZ1ObF6c";
|
||||
localStorage.setItem("token", editorToken);
|
||||
renderMainPage();
|
||||
|
||||
// it renders question and category lists
|
||||
|
@ -23,30 +31,35 @@ describe('The main page', () => {
|
|||
expect(categoryListHeadline).not.toBeNull();
|
||||
});
|
||||
|
||||
test('displays the candidates page if a candidate is logged in', () => {
|
||||
const candidateToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfY2FuZGlkYXRlIiwicGVyc29uX3Jvd19pZCI6MiwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.i66MDTPVWwfAvOawY25WE9OPb5CQ9hidoUruP91ngcg";
|
||||
localStorage.setItem("token", candidateToken)
|
||||
test("displays the candidates page if a candidate is logged in", () => {
|
||||
const candidateToken =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfY2FuZGlkYXRlIiwicGVyc29uX3Jvd19pZCI6MiwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.i66MDTPVWwfAvOawY25WE9OPb5CQ9hidoUruP91ngcg";
|
||||
localStorage.setItem("token", candidateToken);
|
||||
renderMainPage();
|
||||
|
||||
const placeholder = screen.queryByText(/Under construction/);
|
||||
expect(placeholder).not.toBeNull();
|
||||
const questionListHeadline = screen.queryByText(/Fragen/);
|
||||
const categoryListHeadline = screen.queryByText(/Kategorien/);
|
||||
expect(questionListHeadline).not.toBeNull();
|
||||
expect(categoryListHeadline).toBeNull();
|
||||
});
|
||||
|
||||
test('displays the user page if an normal user is logged in', () => {
|
||||
const userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfcGVyc29uIiwicGVyc29uX3Jvd19pZCI6MywiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.RWo5USCmyn-OYjgYixq0y6qlObU9Rb0KdsxxvrtlW1o";
|
||||
localStorage.setItem("token", userToken)
|
||||
test("displays the user page if an normal user is logged in", () => {
|
||||
const userToken =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfcGVyc29uIiwicGVyc29uX3Jvd19pZCI6MywiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.RWo5USCmyn-OYjgYixq0y6qlObU9Rb0KdsxxvrtlW1o";
|
||||
localStorage.setItem("token", userToken);
|
||||
renderMainPage();
|
||||
|
||||
const placeholder = screen.queryByText(/nichts zu sehen/);
|
||||
expect(placeholder).not.toBeNull();
|
||||
});
|
||||
|
||||
test('displays a link to the loggin page if something is wrong with the token', () => {
|
||||
const invalidToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHOYjgYixq0y6qlObU9Rb0KdsxxvrtlW1o";
|
||||
localStorage.setItem("token", invalidToken)
|
||||
test("displays a link to the loggin page if something is wrong with the token", () => {
|
||||
const invalidToken =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHOYjgYixq0y6qlObU9Rb0KdsxxvrtlW1o";
|
||||
localStorage.setItem("token", invalidToken);
|
||||
renderMainPage();
|
||||
|
||||
const placeholder = screen.queryByRole("link", {name: /Login Seite/});
|
||||
const placeholder = screen.queryByRole("link", { name: /Login Seite/ });
|
||||
expect(placeholder).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,53 +1,60 @@
|
|||
import CustomAppBar from "./CustomAppBar";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {MainPageEditor} from "./MainPageEditor";
|
||||
import {getJsonWebToken} from "../jwt/jwt";
|
||||
import {MainPageCandidate} from "./MainPageCandidate";
|
||||
import {MainPageUser} from "./MainPageUser";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Container} from "@material-ui/core";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { MainPageEditor } from "./MainPageEditor";
|
||||
import { getJsonWebToken } from "../jwt/jwt";
|
||||
import { MainPageCandidate } from "./MainPageCandidate";
|
||||
import { MainPageUser } from "./MainPageUser";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Container } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBarSpacer: theme.mixins.toolbar,
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
height: "100vh",
|
||||
overflow: "auto",
|
||||
},
|
||||
invalidTokenContainer: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
function Main() {
|
||||
function Main(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const getMainPage = () => {
|
||||
switch (getJsonWebToken()?.role) {
|
||||
case "candymat_editor":
|
||||
return <MainPageEditor/>;
|
||||
case "candymat_candidate":
|
||||
return <MainPageCandidate/>;
|
||||
case "candymat_person":
|
||||
return <MainPageUser/>;
|
||||
default:
|
||||
localStorage.removeItem('token');
|
||||
return <Container className={classes.invalidTokenContainer}>
|
||||
Du bist nicht eingelogged oder dein Token ist ungültig. Logge dich erneut ein.<br/>
|
||||
const jwt = getJsonWebToken();
|
||||
if (jwt) {
|
||||
switch (jwt.role) {
|
||||
case "candymat_editor":
|
||||
return <MainPageEditor />;
|
||||
case "candymat_candidate":
|
||||
return <MainPageCandidate personRowId={jwt.person_row_id} />;
|
||||
case "candymat_person":
|
||||
return <MainPageUser />;
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem("token");
|
||||
return (
|
||||
<Container className={classes.invalidTokenContainer}>
|
||||
Du bist nicht eingelogged oder dein Token ist ungültig. Logge dich
|
||||
erneut ein.
|
||||
<br />
|
||||
Zur <Link to={"/login"}>Login Seite</Link>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<CustomAppBar/>
|
||||
<CustomAppBar />
|
||||
<main className={classes.content}>
|
||||
<div className={classes.appBarSpacer}/>
|
||||
<div className={classes.appBarSpacer} />
|
||||
{getMainPage()}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Main;
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import {Container} from "@material-ui/core";
|
||||
import { Container } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import QuestionAnswersList from "./QuestionAnswerList";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
flexDirection: 'column',
|
||||
flexDirection: "column",
|
||||
},
|
||||
}));
|
||||
|
||||
export function MainPageCandidate() {
|
||||
const classes = useStyles();
|
||||
interface MainPageCandidateProps {
|
||||
personRowId: number;
|
||||
}
|
||||
|
||||
export function MainPageCandidate(
|
||||
props: MainPageCandidateProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
Under construction
|
||||
<QuestionAnswersList personRowId={props.personRowId} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import {Container} from "@material-ui/core";
|
||||
import { Container } from "@material-ui/core";
|
||||
import QuestionList from "./QuestionList";
|
||||
import CategoryList from "./CategoryList";
|
||||
import {Copyright} from "./Copyright";
|
||||
import { Copyright } from "./Copyright";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
flexDirection: 'column',
|
||||
flexDirection: "column",
|
||||
},
|
||||
}));
|
||||
|
||||
export function MainPageEditor() {
|
||||
export function MainPageEditor(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<QuestionList/>
|
||||
<CategoryList/>
|
||||
<Copyright/>
|
||||
<QuestionList />
|
||||
<CategoryList />
|
||||
<Copyright />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import {Container} from "@material-ui/core";
|
||||
import { Container } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
flexDirection: 'column',
|
||||
flexDirection: "column",
|
||||
},
|
||||
}));
|
||||
|
||||
export function MainPageUser() {
|
||||
export function MainPageUser(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
|
|
54
redaktions-app/src/components/QuestionAnswerList.tsx
Normal file
54
redaktions-app/src/components/QuestionAnswerList.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import {
|
||||
GET_ALL_QUESTION_ANSWERS,
|
||||
GetAllQuestionAnswersResponse,
|
||||
GetAllQuestionAnswersVariables,
|
||||
} from "../backend/queries/answer";
|
||||
import { Paper, Typography } from "@material-ui/core";
|
||||
import { CandidatePositionLegend } from "./CandidatePositionLegend";
|
||||
import AccordionQuestionAnswer from "./AccordionQuestionAnswer";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: "100%",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: theme.spacing(3),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}));
|
||||
|
||||
interface QuestionAnswerListProps {
|
||||
personRowId: number;
|
||||
}
|
||||
|
||||
export default function QuestionAnswersList(
|
||||
props: QuestionAnswerListProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const questionAnswers = useQuery<
|
||||
GetAllQuestionAnswersResponse,
|
||||
GetAllQuestionAnswersVariables
|
||||
>(GET_ALL_QUESTION_ANSWERS, {
|
||||
variables: {
|
||||
personRowId: props.personRowId,
|
||||
},
|
||||
}).data?.allQuestions.nodes;
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
|
||||
Fragen
|
||||
</Typography>
|
||||
<CandidatePositionLegend />
|
||||
{questionAnswers?.map((question) => (
|
||||
<AccordionQuestionAnswer
|
||||
key={question.rowId}
|
||||
personRowId={props.personRowId}
|
||||
question={question}
|
||||
/>
|
||||
))}
|
||||
</Paper>
|
||||
);
|
||||
}
|
|
@ -1,49 +1,60 @@
|
|||
import {Paper, Typography} from "@material-ui/core";
|
||||
import { Paper, Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {useQuery} from "@apollo/client";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import AddCard from "./AddCard";
|
||||
import AccordionWithEdit from "./AccordionWithEdit";
|
||||
import {BasicQuestionResponse, GET_ALL_QUESTIONS, GetAllQuestionsResponse} from "../backend/queries/question";
|
||||
import DialogChangeQuestion, {dialogChangeQuestionId, dialogChangeQuestionOpen} from "./DialogChangeQuestion";
|
||||
import {
|
||||
BasicQuestionResponse,
|
||||
GET_ALL_QUESTIONS,
|
||||
GetAllQuestionsResponse,
|
||||
} from "../backend/queries/question";
|
||||
import DialogChangeQuestion, {
|
||||
dialogChangeQuestionId,
|
||||
dialogChangeQuestionOpen,
|
||||
} from "./DialogChangeQuestion";
|
||||
import DialogDeleteQuestion, {
|
||||
dialogDeleteQuestionId,
|
||||
dialogDeleteQuestionOpen,
|
||||
dialogDeleteQuestionTitle
|
||||
dialogDeleteQuestionTitle,
|
||||
} from "./DialogDeleteQuestion";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function QuestionList() {
|
||||
const questions = useQuery<GetAllQuestionsResponse, null>(GET_ALL_QUESTIONS).data?.allQuestions.nodes;
|
||||
export default function QuestionList(): React.ReactElement {
|
||||
const questions = useQuery<GetAllQuestionsResponse, null>(GET_ALL_QUESTIONS)
|
||||
.data?.allQuestions.nodes;
|
||||
const classes = useStyles();
|
||||
|
||||
const handleAddButtonClick = () => {
|
||||
dialogChangeQuestionId("")
|
||||
dialogChangeQuestionOpen(true)
|
||||
}
|
||||
dialogChangeQuestionId("");
|
||||
dialogChangeQuestionOpen(true);
|
||||
};
|
||||
|
||||
const handleEditButtonClick = (question: BasicQuestionResponse) => {
|
||||
dialogChangeQuestionId(question.id)
|
||||
dialogChangeQuestionOpen(true)
|
||||
dialogChangeQuestionId(question.id);
|
||||
dialogChangeQuestionOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteButtonClick = (question: BasicQuestionResponse) => {
|
||||
dialogDeleteQuestionTitle(question.title);
|
||||
dialogDeleteQuestionId(question.id);
|
||||
dialogDeleteQuestionOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>Fragen</Typography>
|
||||
{questions?.map(question => <AccordionWithEdit
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>
|
||||
Fragen
|
||||
</Typography>
|
||||
{questions?.map((question) => (
|
||||
<AccordionWithEdit
|
||||
key={question.id}
|
||||
title={question.title}
|
||||
subTitle={question.categoryByCategoryRowId?.title}
|
||||
|
@ -51,11 +62,10 @@ export default function QuestionList() {
|
|||
onEditButtonClick={() => handleEditButtonClick(question)}
|
||||
onDeleteButtonClick={() => handleDeleteButtonClick(question)}
|
||||
/>
|
||||
)}
|
||||
<AddCard handleClick={handleAddButtonClick}/>
|
||||
<DialogChangeQuestion/>
|
||||
<DialogDeleteQuestion/>
|
||||
))}
|
||||
<AddCard handleClick={handleAddButtonClick} />
|
||||
<DialogChangeQuestion />
|
||||
<DialogDeleteQuestion />
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +1,79 @@
|
|||
import React, {useState} from 'react';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import {Alert} from '@material-ui/lab';
|
||||
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
|
||||
import {Link, useHistory, useLocation} from 'react-router-dom';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import {makeStyles} from '@material-ui/core/styles';
|
||||
import Container from '@material-ui/core/Container';
|
||||
import {useMutation} from "@apollo/client";
|
||||
import React, { useState } from "react";
|
||||
import Avatar from "@material-ui/core/Avatar";
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Box from "@material-ui/core/Box";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Container from "@material-ui/core/Container";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import ButtonWithSpinner from "./ButtonWithSpinner";
|
||||
import {Copyright} from "./Copyright";
|
||||
import {LOGIN, LoginResponse, LoginVariables} from "../backend/mutations/login";
|
||||
import { Copyright } from "./Copyright";
|
||||
import {
|
||||
LOGIN,
|
||||
LoginResponse,
|
||||
LoginVariables,
|
||||
} from "../backend/mutations/login";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(1),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
form: {
|
||||
width: '100%', // Fix IE 11 issue.
|
||||
width: "100%", // Fix IE 11 issue.
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
alert: {
|
||||
margin: theme.spacing(1)
|
||||
}
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
export default function SignIn() {
|
||||
export default function SignIn(): React.ReactElement {
|
||||
const history = useHistory();
|
||||
const queryParams = new URLSearchParams(useLocation().search);
|
||||
const classes = useStyles();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [login, {loading}] = useMutation<LoginResponse, LoginVariables>(
|
||||
const [login, { loading }] = useMutation<LoginResponse, LoginVariables>(
|
||||
LOGIN,
|
||||
{
|
||||
onCompleted(data) {
|
||||
if (data.authenticate.jwtToken) {
|
||||
localStorage.setItem("token", data.authenticate.jwtToken)
|
||||
history.replace("/")
|
||||
localStorage.setItem("token", data.authenticate.jwtToken);
|
||||
history.replace("/");
|
||||
} else {
|
||||
setError("Wrong username or password.")
|
||||
setError("Wrong username or password.");
|
||||
}
|
||||
},
|
||||
onError(e) {
|
||||
setError(`Error while trying to log in: ${e.message}`)
|
||||
}
|
||||
setError(`Error while trying to log in: ${e.message}`);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<CssBaseline/>
|
||||
<CssBaseline />
|
||||
<div className={classes.paper}>
|
||||
<Avatar className={classes.avatar}>
|
||||
<LockOutlinedIcon/>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
Sign in
|
||||
|
@ -78,9 +81,12 @@ export default function SignIn() {
|
|||
<form
|
||||
className={classes.form}
|
||||
noValidate
|
||||
onSubmit={event => {
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
login({variables: {email: email, password: password}}).catch(error => console.log(error))
|
||||
// fixme: logging?????
|
||||
login({
|
||||
variables: { email: email, password: password },
|
||||
}).catch((error) => console.log(error));
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
|
@ -117,14 +123,12 @@ export default function SignIn() {
|
|||
/>
|
||||
<FormControlLabel
|
||||
disabled={true}
|
||||
control={<Checkbox value="remember" color="primary"/>}
|
||||
control={<Checkbox value="remember" color="primary" />}
|
||||
label="Remember me"
|
||||
/>
|
||||
<ButtonWithSpinner
|
||||
loading={loading}
|
||||
type="submit"
|
||||
fullWidth
|
||||
>Sign In</ButtonWithSpinner>
|
||||
<ButtonWithSpinner loading={loading} type="submit" fullWidth>
|
||||
Sign In
|
||||
</ButtonWithSpinner>
|
||||
<Grid container>
|
||||
<Grid item xs>
|
||||
{/* todo: see issue #17*/}
|
||||
|
@ -133,27 +137,32 @@ export default function SignIn() {
|
|||
{/*</Link>*/}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Link to="/signup">
|
||||
{"Don't have an account? Sign Up"}
|
||||
</Link>
|
||||
<Link to="/signup">{"Don't have an account? Sign Up"}</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{queryParams.get("recent-sign-up-success")
|
||||
? <Alert className={classes.alert} severity="success" onClose={() => history.push("/login")}>
|
||||
{queryParams.get("recent-sign-up-success") ? (
|
||||
<Alert
|
||||
className={classes.alert}
|
||||
severity="success"
|
||||
onClose={() => history.push("/login")}
|
||||
>
|
||||
Sign-Up was successful. Log in to continue
|
||||
</Alert>
|
||||
: null}
|
||||
{error ? <Alert className={classes.alert} severity="error" onClose={() => setError("")}>{error}</Alert> : null}
|
||||
) : null}
|
||||
{error ? (
|
||||
<Alert
|
||||
className={classes.alert}
|
||||
severity="error"
|
||||
onClose={() => setError("")}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
<Box mt={8}>
|
||||
<Copyright/>
|
||||
<Copyright />
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,110 +1,120 @@
|
|||
import React, {ChangeEvent, useState} from 'react';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import {Link, useHistory} from 'react-router-dom';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import {makeStyles} from '@material-ui/core/styles';
|
||||
import Container from '@material-ui/core/Container';
|
||||
import {Copyright} from "./Copyright";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import React, { ChangeEvent, useState } from "react";
|
||||
import Avatar from "@material-ui/core/Avatar";
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Box from "@material-ui/core/Box";
|
||||
import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Container from "@material-ui/core/Container";
|
||||
import { Copyright } from "./Copyright";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import ButtonWithSpinner from "./ButtonWithSpinner";
|
||||
import {errorHandler, SignUpError} from "./SignUpErrorHandler";
|
||||
import {Alert} from "@material-ui/lab";
|
||||
import {SIGN_UP, SignUpResponse, SignUpVariables} from "../backend/mutations/signUp";
|
||||
|
||||
import { errorHandler, SignUpError } from "./SignUpErrorHandler";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import {
|
||||
SIGN_UP,
|
||||
SignUpResponse,
|
||||
SignUpVariables,
|
||||
} from "../backend/mutations/signUp";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(1),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
form: {
|
||||
width: '100%', // Fix IE 11 issue.
|
||||
width: "100%", // Fix IE 11 issue.
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
error: {
|
||||
color: 'red',
|
||||
color: "red",
|
||||
},
|
||||
alert: {
|
||||
margin: theme.spacing(1)
|
||||
}
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SignUp() {
|
||||
export default function SignUp(): React.ReactElement {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [error, setError] = useState<SignUpError | undefined>(undefined)
|
||||
const [error, setError] = useState<SignUpError | undefined>(undefined);
|
||||
const history = useHistory();
|
||||
const [createAccount, {loading}] = useMutation<SignUpResponse, SignUpVariables>(
|
||||
SIGN_UP,
|
||||
{
|
||||
onCompleted() {
|
||||
history.push("/login?recent-sign-up-success=true")
|
||||
},
|
||||
onError(e) {
|
||||
console.error(e);
|
||||
setPassword("");
|
||||
setError(errorHandler(e))
|
||||
}
|
||||
}
|
||||
);
|
||||
const [createAccount, { loading }] = useMutation<
|
||||
SignUpResponse,
|
||||
SignUpVariables
|
||||
>(SIGN_UP, {
|
||||
onCompleted() {
|
||||
history.push("/login?recent-sign-up-success=true");
|
||||
},
|
||||
onError(e) {
|
||||
console.error(e);
|
||||
setPassword("");
|
||||
setError(errorHandler(e));
|
||||
},
|
||||
});
|
||||
const classes = useStyles();
|
||||
|
||||
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
createAccount({variables: {firstName, lastName, email, password}});
|
||||
}
|
||||
createAccount({ variables: { firstName, lastName, email, password } });
|
||||
};
|
||||
|
||||
const onFirstNameChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
setFirstName(e.target.value)
|
||||
const onFirstNameChange = (
|
||||
e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
setFirstName(e.target.value);
|
||||
if (error?.firstNameInvalid) {
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onLastNameChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
setLastName(e.target.value)
|
||||
const onLastNameChange = (
|
||||
e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
setLastName(e.target.value);
|
||||
if (error?.lastNameInvalid) {
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEmailChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
setEmail(e.target.value)
|
||||
const onEmailChange = (
|
||||
e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
setEmail(e.target.value);
|
||||
if (error?.emailInvalid) {
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
setPassword(e.target.value)
|
||||
const onPasswordChange = (
|
||||
e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
setPassword(e.target.value);
|
||||
if (error?.passwordInvalid) {
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<CssBaseline/>
|
||||
<CssBaseline />
|
||||
<div className={classes.paper}>
|
||||
<Avatar className={classes.avatar}>
|
||||
<LockOutlinedIcon/>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
Sign up
|
||||
|
@ -170,33 +180,28 @@ export default function SignUp() {
|
|||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ButtonWithSpinner
|
||||
loading={loading}
|
||||
type="submit"
|
||||
fullWidth
|
||||
>
|
||||
<ButtonWithSpinner loading={loading} type="submit" fullWidth>
|
||||
Sign Up
|
||||
</ButtonWithSpinner>
|
||||
<Grid container justify="flex-end">
|
||||
<Grid item>
|
||||
<Link to="/login">
|
||||
Already have an account? Sign in
|
||||
</Link>
|
||||
<Link to="/login">Already have an account? Sign in</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{
|
||||
error
|
||||
? <Alert className={classes.alert} severity="error" onClose={() => setError(undefined)}>
|
||||
{error.message}
|
||||
</Alert>
|
||||
: null
|
||||
}
|
||||
{error ? (
|
||||
<Alert
|
||||
className={classes.alert}
|
||||
severity="error"
|
||||
onClose={() => setError(undefined)}
|
||||
>
|
||||
{error.message}
|
||||
</Alert>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
<Box mt={5}>
|
||||
<Copyright/>
|
||||
<Copyright />
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,62 +1,68 @@
|
|||
import {ApolloError} from "@apollo/client";
|
||||
|
||||
import { ApolloError } from "@apollo/client";
|
||||
|
||||
export interface SignUpError {
|
||||
message: string,
|
||||
emailInvalid: boolean,
|
||||
firstNameInvalid: boolean,
|
||||
lastNameInvalid: boolean,
|
||||
passwordInvalid: boolean
|
||||
message: string;
|
||||
emailInvalid: boolean;
|
||||
firstNameInvalid: boolean;
|
||||
lastNameInvalid: boolean;
|
||||
passwordInvalid: boolean;
|
||||
}
|
||||
|
||||
const parseErrorMessage = (error: ApolloError): string => {
|
||||
let result = "Sign-up failed because of the following reason(s): ";
|
||||
if (isEmailAlreadyUsed(error)) {
|
||||
result += "The E-Mail is already in use. "
|
||||
result += "The E-Mail is already in use. ";
|
||||
}
|
||||
if (isFirstNameInvalid(error)) {
|
||||
result += "The provided 'First Name' is invalid. "
|
||||
result += "The provided 'First Name' is invalid. ";
|
||||
}
|
||||
if (isLastNameInvalid(error)) {
|
||||
result += "The provided 'Last Name' is invalid. "
|
||||
result += "The provided 'Last Name' is invalid. ";
|
||||
}
|
||||
if (isPasswordInvalid(error)) {
|
||||
result += "The provided password is invalid. "
|
||||
result += "The provided password is invalid. ";
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const isEmailAlreadyUsed = (error: ApolloError): boolean => {
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
return errorMessage.includes("unique-constraint") && errorMessage.includes("email");
|
||||
}
|
||||
return (
|
||||
errorMessage.includes("unique-constraint") && errorMessage.includes("email")
|
||||
);
|
||||
};
|
||||
|
||||
const isFirstNameInvalid = (error: ApolloError): boolean => {
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
return errorMessage.includes("invalid") && errorMessage.includes("first name");
|
||||
}
|
||||
return (
|
||||
errorMessage.includes("invalid") && errorMessage.includes("first name")
|
||||
);
|
||||
};
|
||||
|
||||
const isLastNameInvalid = (error: ApolloError): boolean => {
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
return errorMessage.includes("invalid") && errorMessage.includes("last name");
|
||||
}
|
||||
};
|
||||
|
||||
const isPasswordInvalid = (error: ApolloError): boolean => {
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
return errorMessage.includes("invalid") && errorMessage.includes("password");
|
||||
}
|
||||
|
||||
export const errorHandler = (error: undefined | ApolloError): undefined | SignUpError => {
|
||||
return error ? {
|
||||
message: parseErrorMessage(error),
|
||||
emailInvalid: isEmailAlreadyUsed(error),
|
||||
firstNameInvalid: isFirstNameInvalid(error),
|
||||
lastNameInvalid: isLastNameInvalid(error),
|
||||
passwordInvalid: isPasswordInvalid(error)
|
||||
} : undefined
|
||||
}
|
||||
};
|
||||
|
||||
export const errorHandler = (
|
||||
error: undefined | ApolloError
|
||||
): undefined | SignUpError => {
|
||||
return error
|
||||
? {
|
||||
message: parseErrorMessage(error),
|
||||
emailInvalid: isEmailAlreadyUsed(error),
|
||||
firstNameInvalid: isFirstNameInvalid(error),
|
||||
lastNameInvalid: isLastNameInvalid(error),
|
||||
passwordInvalid: isPasswordInvalid(error),
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
import { FormLabel } from "@material-ui/core";
|
||||
import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
|
||||
import {
|
||||
allPositions,
|
||||
CandidatePosition,
|
||||
getIconForPosition,
|
||||
} from "./CandidatePositionLegend";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
interface ToggleButtonGroupAnswerPositionProps {
|
||||
position: CandidatePosition;
|
||||
loading?: boolean;
|
||||
|
||||
onPositionChange(
|
||||
e: React.MouseEvent<HTMLElement>,
|
||||
newPosition: CandidatePosition
|
||||
): void;
|
||||
}
|
||||
|
||||
export default function ToggleButtonGroupAnswerPosition(
|
||||
props: ToggleButtonGroupAnswerPositionProps
|
||||
): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<FormLabel component="legend">Deine Position</FormLabel>
|
||||
<ToggleButtonGroup
|
||||
value={props.position}
|
||||
exclusive
|
||||
onChange={props.onPositionChange}
|
||||
>
|
||||
{allPositions.map((position) => (
|
||||
<ToggleButton
|
||||
disabled={props.loading}
|
||||
key={position}
|
||||
value={position}
|
||||
aria-label={position.toString()}
|
||||
>
|
||||
{getIconForPosition(position, {
|
||||
color: "primary",
|
||||
fontSize: "small",
|
||||
})}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import {ApolloProvider} from "@apollo/client";
|
||||
import {client} from "./backend/helper";
|
||||
import {BrowserRouter as Router} from "react-router-dom";
|
||||
import {SnackbarProvider} from "notistack";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import { client } from "./backend/helper";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
|
||||
ReactDOM.render(
|
||||
<ApolloProvider client={client}>
|
||||
<Router>
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<App/>
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</Router>
|
||||
</ApolloProvider>,
|
||||
document.getElementById('root')
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
import React from "react";
|
||||
import {
|
||||
findByRole,
|
||||
findByText,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import {
|
||||
getAllQuestionAnswersMock,
|
||||
getAnswerByQuestionAndPersonMock,
|
||||
questionAnswersMock,
|
||||
} from "../backend/queries/answer.mock";
|
||||
import {
|
||||
addAnswerMock,
|
||||
editAnswerMock,
|
||||
} from "../backend/mutations/answer.mock";
|
||||
import {
|
||||
getNegativePositionPath,
|
||||
getNeutralPositionPath,
|
||||
getPositivePositionPath,
|
||||
getSkippedPositionPath,
|
||||
queryAllIconButtons,
|
||||
} from "./test-helper";
|
||||
import QuestionAnswersList from "../components/QuestionAnswerList";
|
||||
|
||||
describe("The AnswerList", () => {
|
||||
test("displays the existing answers, but not the details of it", async () => {
|
||||
renderQuestionAnswerList();
|
||||
|
||||
const questionAnswerCards = await waitForQuestionsToRender();
|
||||
questionAnswerCards.forEach((card) => {
|
||||
expect(card.innerHTML).toMatch(/Question [1-3]/);
|
||||
});
|
||||
const saveButtons = screen.queryAllByRole("button", { name: /speichern/i });
|
||||
expect(saveButtons).toHaveLength(0);
|
||||
expect(questionAnswerCards[0].innerHTML).toContain(
|
||||
getNeutralPositionPath()
|
||||
);
|
||||
expect(questionAnswerCards[1].innerHTML).toContain(
|
||||
getPositivePositionPath()
|
||||
);
|
||||
expect(questionAnswerCards[2].innerHTML).toContain(
|
||||
getSkippedPositionPath()
|
||||
);
|
||||
});
|
||||
|
||||
test("enables resetting an answer to the last saved state", async () => {
|
||||
renderQuestionAnswerList(editAnswerMock);
|
||||
|
||||
const questionAnswerCards = await waitForQuestionsToRender();
|
||||
let answerSection = await expandAccordionAndGetAnswerSection(
|
||||
questionAnswerCards[1]
|
||||
);
|
||||
expect(answerSection).toBeDefined();
|
||||
answerSection = answerSection as AccordionAnswerSection;
|
||||
|
||||
const { textField, resetButton } = answerSection;
|
||||
|
||||
expect(textField.outerHTML).toContain("Answer 2");
|
||||
// change answer title
|
||||
fireEvent.change(textField, {
|
||||
target: { value: "New answer" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(textField.outerHTML).toContain("New answer");
|
||||
});
|
||||
|
||||
// reset and verify
|
||||
fireEvent.click(resetButton);
|
||||
await waitFor(() => {
|
||||
expect(textField.outerHTML).toContain("Answer 2");
|
||||
});
|
||||
});
|
||||
|
||||
test("enables editing an answer", async () => {
|
||||
renderQuestionAnswerList(editAnswerMock);
|
||||
|
||||
const questionAnswerCards = await waitForQuestionsToRender();
|
||||
let answerSection = await expandAccordionAndGetAnswerSection(
|
||||
questionAnswerCards[1]
|
||||
);
|
||||
expect(answerSection).toBeDefined();
|
||||
answerSection = answerSection as AccordionAnswerSection;
|
||||
|
||||
const { textField, saveButton, resetButton } = answerSection;
|
||||
expect(textField.outerHTML).toContain("Answer 2");
|
||||
// change answer title
|
||||
fireEvent.change(textField, {
|
||||
target: { value: "New answer" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(textField.outerHTML).toContain("New answer");
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update (i.e a subsequent reset should reset to the new answer)
|
||||
fireEvent.click(saveButton);
|
||||
fireEvent.change(textField, {
|
||||
target: { value: "something else" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(textField.outerHTML).toContain("something else");
|
||||
});
|
||||
fireEvent.click(resetButton);
|
||||
await waitFor(() => {
|
||||
expect(textField.outerHTML).toContain("New answer");
|
||||
});
|
||||
});
|
||||
|
||||
test("enables adding an answer via setting the position", async () => {
|
||||
renderQuestionAnswerList(addAnswerMock);
|
||||
|
||||
const questionAnswerCards = await waitForQuestionsToRender();
|
||||
const questionWithoutAnswerCard = questionAnswerCards[2];
|
||||
let answerSection = await expandAccordionAndGetAnswerSection(
|
||||
questionWithoutAnswerCard
|
||||
);
|
||||
expect(answerSection).toBeDefined();
|
||||
answerSection = answerSection as AccordionAnswerSection;
|
||||
|
||||
const { positionIconButtons } = answerSection;
|
||||
expect(positionIconButtons.skipped.outerHTML).toContain(
|
||||
'aria-pressed="true"'
|
||||
);
|
||||
expect(positionIconButtons.positive.outerHTML).toContain(
|
||||
'aria-pressed="false"'
|
||||
);
|
||||
|
||||
// press "positive" icon button and wait for accordion header to change -> apollo cache update successful
|
||||
fireEvent.click(positionIconButtons.positive);
|
||||
await waitFor(() => {
|
||||
expect(questionAnswerCards[2].innerHTML).toContain(
|
||||
getPositivePositionPath()
|
||||
);
|
||||
});
|
||||
expect(positionIconButtons.skipped.outerHTML).toContain(
|
||||
'aria-pressed="false"'
|
||||
);
|
||||
expect(positionIconButtons.positive.outerHTML).toContain(
|
||||
'aria-pressed="true"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function renderQuestionAnswerList(additionalMocks?: Array<MockedResponse>) {
|
||||
const initialMocks = [
|
||||
...getAllQuestionAnswersMock,
|
||||
...getAnswerByQuestionAndPersonMock,
|
||||
];
|
||||
const allMocks = additionalMocks
|
||||
? [...initialMocks, ...additionalMocks]
|
||||
: initialMocks;
|
||||
return render(
|
||||
<MockedProvider mocks={allMocks}>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<QuestionAnswersList personRowId={2} />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const waitForQuestionsToRender = async (): Promise<Array<HTMLElement>> => {
|
||||
const numberOfAnswersInMockQuery = questionAnswersMock.length;
|
||||
let questionAnswerCards: Array<HTMLElement> = [];
|
||||
await waitFor(() => {
|
||||
questionAnswerCards = screen.queryAllByRole("button", {
|
||||
name: /Question [1-3]/,
|
||||
});
|
||||
expect(questionAnswerCards.length).toEqual(numberOfAnswersInMockQuery);
|
||||
});
|
||||
return questionAnswerCards;
|
||||
};
|
||||
|
||||
const getSaveAnswerButton = (parent: HTMLElement) => {
|
||||
return findByRole(parent, "button", { name: /speichern/i });
|
||||
};
|
||||
|
||||
const getResetAnswerButton = (parent: HTMLElement) => {
|
||||
return findByRole(parent, "button", { name: /zurücksetzen/i });
|
||||
};
|
||||
|
||||
interface PositionIconButtons {
|
||||
positive: HTMLElement;
|
||||
neutral: HTMLElement;
|
||||
negative: HTMLElement;
|
||||
skipped: HTMLElement;
|
||||
}
|
||||
|
||||
const getCandidatePositionButtons = async (
|
||||
parent: HTMLElement
|
||||
): Promise<PositionIconButtons> => {
|
||||
const labelAboveButtons = await findByText(parent, /Deine Position/i);
|
||||
const parentElement = labelAboveButtons.parentElement as HTMLElement;
|
||||
const positive = queryAllIconButtons(
|
||||
getPositivePositionPath(),
|
||||
parentElement
|
||||
)[0];
|
||||
const neutral = queryAllIconButtons(
|
||||
getNeutralPositionPath(),
|
||||
parentElement
|
||||
)[0];
|
||||
const negative = queryAllIconButtons(
|
||||
getNegativePositionPath(),
|
||||
parentElement
|
||||
)[0];
|
||||
const skipped = queryAllIconButtons(
|
||||
getSkippedPositionPath(),
|
||||
parentElement
|
||||
)[0];
|
||||
expect(positive).toBeDefined();
|
||||
expect(neutral).toBeDefined();
|
||||
expect(negative).toBeDefined();
|
||||
expect(skipped).toBeDefined();
|
||||
return {
|
||||
positive,
|
||||
neutral,
|
||||
negative,
|
||||
skipped,
|
||||
};
|
||||
};
|
||||
|
||||
interface AccordionAnswerSection {
|
||||
positionIconButtons: PositionIconButtons;
|
||||
textField: HTMLElement;
|
||||
saveButton: HTMLElement;
|
||||
resetButton: HTMLElement;
|
||||
}
|
||||
|
||||
const expandAccordionAndGetAnswerSection = async (
|
||||
accordionExpandArea: HTMLElement
|
||||
): Promise<AccordionAnswerSection | undefined> => {
|
||||
fireEvent.click(accordionExpandArea);
|
||||
let fullAccordion = accordionExpandArea.parentElement;
|
||||
expect(fullAccordion).not.toBeNull();
|
||||
fullAccordion = fullAccordion as HTMLElement;
|
||||
const textField = await findByRole(fullAccordion, "textbox");
|
||||
const saveButton = await getSaveAnswerButton(fullAccordion);
|
||||
const resetButton = await getResetAnswerButton(fullAccordion);
|
||||
const positionIconButtons = await getCandidatePositionButtons(fullAccordion);
|
||||
return {
|
||||
positionIconButtons,
|
||||
textField,
|
||||
saveButton,
|
||||
resetButton,
|
||||
};
|
||||
};
|
|
@ -1,73 +1,75 @@
|
|||
import React from 'react';
|
||||
import {render, screen} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/client/testing';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MockedProvider } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import App from "../App";
|
||||
import {SnackbarProvider} from "notistack";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
|
||||
const renderAppAtUrl = (path: string) => render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<SnackbarProvider>
|
||||
<App/>
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
const renderAppAtUrl = (path: string) =>
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<SnackbarProvider>
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => localStorage.clear())
|
||||
beforeEach(() => localStorage.clear());
|
||||
|
||||
describe('The root path /', () => {
|
||||
|
||||
test('renders user\'s home page if they are logged in', () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf")
|
||||
describe("The root path /", () => {
|
||||
test("renders user's home page if they are logged in", () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf");
|
||||
renderAppAtUrl("/");
|
||||
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow();
|
||||
});
|
||||
|
||||
test('redirects to login page if user not logged in', () => {
|
||||
test("redirects to login page if user not logged in", () => {
|
||||
renderAppAtUrl("/");
|
||||
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
const emailField = screen.getByRole("textbox", { name: "Email Address" });
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
expect(emailField).toHaveValue("");
|
||||
expect(passwordField).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
describe('The /login path', () => {
|
||||
test('renders the signin page if the user is not logged in', () => {
|
||||
describe("The /login path", () => {
|
||||
test("renders the signin page if the user is not logged in", () => {
|
||||
renderAppAtUrl("/login");
|
||||
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
const emailField = screen.getByRole("textbox", { name: "Email Address" });
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
expect(emailField).toHaveValue("");
|
||||
expect(passwordField).toHaveValue("");
|
||||
});
|
||||
|
||||
test('redirects to root / and the user\'s home page if the user is logged in', () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf")
|
||||
test("redirects to root / and the user's home page if the user is logged in", () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf");
|
||||
renderAppAtUrl("/login");
|
||||
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('The /signup path', () => {
|
||||
test('renders the signup page if the user is not logged in', () => {
|
||||
describe("The /signup path", () => {
|
||||
test("renders the signup page if the user is not logged in", () => {
|
||||
renderAppAtUrl("/signup");
|
||||
|
||||
expect(() => screen.getByRole('textbox', {name: 'Email Address'})).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/Password/)).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/First Name/)).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/Last Name/)).not.toThrow()
|
||||
expect(() =>
|
||||
screen.getByRole("textbox", { name: "Email Address" })
|
||||
).not.toThrow();
|
||||
expect(() => screen.getByLabelText(/Password/)).not.toThrow();
|
||||
expect(() => screen.getByLabelText(/First Name/)).not.toThrow();
|
||||
expect(() => screen.getByLabelText(/Last Name/)).not.toThrow();
|
||||
});
|
||||
|
||||
test('redirects to root / and the user\'s home page if the user is logged in', () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf")
|
||||
test("redirects to root / and the user's home page if the user is logged in", () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf");
|
||||
renderAppAtUrl("/signup");
|
||||
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,71 +1,86 @@
|
|||
import React from 'react';
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
import {MockedProvider, MockedResponse} from '@apollo/client/testing';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import CategoryList from "../components/CategoryList";
|
||||
import {SnackbarProvider} from "notistack";
|
||||
import {categoryNodesMock, getAllCategoriesMock, getCategoryByIdMock} from "../backend/queries/category.mock";
|
||||
import {addCategoryMock, deleteCategoryMock, editCategoryMock} from "../backend/mutations/category.mock";
|
||||
import {expandAccordionAndGetIconButtons, queryAllAddIconButtons, queryAllEditIconButtons} from "./test-helper";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import {
|
||||
categoryNodesMock,
|
||||
getAllCategoriesMock,
|
||||
getCategoryByIdMock,
|
||||
} from "../backend/queries/category.mock";
|
||||
import {
|
||||
addCategoryMock,
|
||||
deleteCategoryMock,
|
||||
editCategoryMock,
|
||||
} from "../backend/mutations/category.mock";
|
||||
import {
|
||||
expandAccordionAndGetIconButtons,
|
||||
queryAllAddIconButtons,
|
||||
queryAllEditIconButtons,
|
||||
} from "./test-helper";
|
||||
|
||||
|
||||
describe('The CategoryList', () => {
|
||||
test('displays the existing categories, but not the details of it', async () => {
|
||||
describe("The CategoryList", () => {
|
||||
test("displays the existing categories, but not the details of it", async () => {
|
||||
renderCategoryList();
|
||||
|
||||
const categoryCards = await waitForInitialCategoriesToRender()
|
||||
categoryCards.forEach(card => {
|
||||
expect(card.innerHTML).toMatch(/Category [1-2]/)
|
||||
})
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0)
|
||||
const categoryCards = await waitForInitialCategoriesToRender();
|
||||
categoryCards.forEach((card) => {
|
||||
expect(card.innerHTML).toMatch(/Category [1-2]/);
|
||||
});
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('enables toggling details on each category', async () => {
|
||||
test("enables toggling details on each category", async () => {
|
||||
renderCategoryList();
|
||||
|
||||
// Initial state: Every category card is not expanded
|
||||
const categoryCards = await waitForInitialCategoriesToRender()
|
||||
const categoryCards = await waitForInitialCategoriesToRender();
|
||||
|
||||
// Expand first category card
|
||||
await expandAccordionAndGetIconButtons(categoryCards[0])
|
||||
await expandAccordionAndGetIconButtons(categoryCards[0]);
|
||||
|
||||
// Shrink first category card again
|
||||
fireEvent.click(categoryCards[0])
|
||||
fireEvent.click(categoryCards[0]);
|
||||
await waitFor(() => {
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0)
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('enables editing a category title', async () => {
|
||||
test("enables editing a category title", async () => {
|
||||
renderCategoryList(editCategoryMock);
|
||||
|
||||
const categoryCards = await waitForInitialCategoriesToRender();
|
||||
const {editIconButton} = await expandAccordionAndGetIconButtons(categoryCards[0]);
|
||||
const { editIconButton } = await expandAccordionAndGetIconButtons(
|
||||
categoryCards[0]
|
||||
);
|
||||
|
||||
// open edit dialog
|
||||
expect(screen.queryByText(/Kategorie bearbeiten/)).toBeNull();
|
||||
fireEvent.click(editIconButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Kategorie bearbeiten/)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// change category title
|
||||
const categoryTitleField = screen.getByDisplayValue(/Category 1/);
|
||||
fireEvent.change(categoryTitleField, {target: {value: "New title for Category 1"}});
|
||||
fireEvent.change(categoryTitleField, {
|
||||
target: { value: "New title for Category 1" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByDisplayValue(/New title for /)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update
|
||||
const confirmButton = screen.getByRole("button", {name: /Speichern/});
|
||||
const confirmButton = screen.getByRole("button", { name: /Speichern/ });
|
||||
fireEvent.click(confirmButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Kategorie bearbeiten/)).toBeNull();
|
||||
expect(screen.queryByText(/New title for Category 1/)).not.toBeNull()
|
||||
})
|
||||
expect(screen.queryByText(/New title for Category 1/)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('enables adding a category', async () => {
|
||||
test("enables adding a category", async () => {
|
||||
renderCategoryList(addCategoryMock);
|
||||
await waitForInitialCategoriesToRender();
|
||||
|
||||
|
@ -76,69 +91,74 @@ describe('The CategoryList', () => {
|
|||
fireEvent.click(addButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(dialogIdentifier)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// change category title
|
||||
const categoryTitleField = screen.getByLabelText(/Zusammenfassung/);
|
||||
fireEvent.change(categoryTitleField, {target: {value: "New category"}});
|
||||
fireEvent.change(categoryTitleField, { target: { value: "New category" } });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByDisplayValue(/New category/)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update
|
||||
const confirmButton = screen.getByRole("button", {name: /Erstellen/});
|
||||
const confirmButton = screen.getByRole("button", { name: /Erstellen/ });
|
||||
fireEvent.click(confirmButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(dialogIdentifier)).toBeNull();
|
||||
expect(screen.queryByText(/New category/)).not.toBeNull()
|
||||
})
|
||||
expect(screen.queryByText(/New category/)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('enables deleting a category', async () => {
|
||||
test("enables deleting a category", async () => {
|
||||
renderCategoryList(deleteCategoryMock);
|
||||
|
||||
const categoryCards = await waitForInitialCategoriesToRender();
|
||||
expect(screen.queryByText(/Category 2/)).not.toBeNull();
|
||||
const {deleteIconButton} = await expandAccordionAndGetIconButtons(categoryCards[1]);
|
||||
const { deleteIconButton } = await expandAccordionAndGetIconButtons(
|
||||
categoryCards[1]
|
||||
);
|
||||
|
||||
// open delete confirmation dialog
|
||||
expect(screen.queryByText(/Kategorie löschen/)).toBeNull();
|
||||
fireEvent.click(deleteIconButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Kategorie löschen/)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update
|
||||
const confirmButton = screen.getByRole("button", {name: /Löschen/});
|
||||
const confirmButton = screen.getByRole("button", { name: /Löschen/ });
|
||||
fireEvent.click(confirmButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Kategorie löschen/)).toBeNull();
|
||||
expect(screen.queryByText(/Category 2/)).toBeNull();
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderCategoryList(additionalMocks?: Array<MockedResponse>) {
|
||||
const initialMocks = [...getAllCategoriesMock, ...getCategoryByIdMock];
|
||||
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
|
||||
const allMocks = additionalMocks
|
||||
? [...initialMocks, ...additionalMocks]
|
||||
: initialMocks;
|
||||
return render(
|
||||
<MockedProvider mocks={allMocks}>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<CategoryList/>
|
||||
<CategoryList />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const waitForInitialCategoriesToRender = async (): Promise<Array<HTMLElement>> => {
|
||||
const waitForInitialCategoriesToRender = async (): Promise<
|
||||
Array<HTMLElement>
|
||||
> => {
|
||||
const numberOfCategoriesInMockQuery = categoryNodesMock.length;
|
||||
let categoryCards: Array<HTMLElement> = [];
|
||||
await waitFor(() => {
|
||||
categoryCards = screen.queryAllByRole("button", {name: /Category [1-2]/})
|
||||
categoryCards = screen.queryAllByRole("button", { name: /Category [1-2]/ });
|
||||
expect(categoryCards.length).toEqual(numberOfCategoriesInMockQuery);
|
||||
});
|
||||
return categoryCards;
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -1,77 +1,88 @@
|
|||
import React from 'react';
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
import {MockedProvider, MockedResponse} from '@apollo/client/testing';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import QuestionList from "../components/QuestionList";
|
||||
import {SnackbarProvider} from "notistack";
|
||||
import {getAllQuestionsMock, getQuestionByIdMock, questionNodesMock} from "../backend/queries/question.mock";
|
||||
import {getAllCategoriesMock} from "../backend/queries/category.mock";
|
||||
import {addQuestionMock, deleteQuestionMock, editQuestionMock} from "../backend/mutations/question.mock";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import {
|
||||
getAllQuestionsMock,
|
||||
getQuestionByIdMock,
|
||||
questionNodesMock,
|
||||
} from "../backend/queries/question.mock";
|
||||
import { getAllCategoriesMock } from "../backend/queries/category.mock";
|
||||
import {
|
||||
addQuestionMock,
|
||||
deleteQuestionMock,
|
||||
editQuestionMock,
|
||||
} from "../backend/mutations/question.mock";
|
||||
import {
|
||||
expandAccordionAndGetIconButtons,
|
||||
queryAllAddIconButtons,
|
||||
queryAllEditIconButtons
|
||||
queryAllEditIconButtons,
|
||||
} from "./test-helper";
|
||||
|
||||
|
||||
describe('The QuestionList', () => {
|
||||
test('displays the existing questions, but not the details of it', async () => {
|
||||
describe("The QuestionList", () => {
|
||||
test("displays the existing questions, but not the details of it", async () => {
|
||||
renderQuestionList();
|
||||
|
||||
const questionCards = await waitForInitialQuestionsToRender()
|
||||
questionCards.forEach(card => {
|
||||
expect(card.innerHTML).toMatch(/Question [1-3]\?/)
|
||||
})
|
||||
const questionCards = await waitForInitialQuestionsToRender();
|
||||
questionCards.forEach((card) => {
|
||||
expect(card.innerHTML).toMatch(/Question [1-3]\?/);
|
||||
});
|
||||
expect(questionCards[0].innerHTML).toMatch(/Category 1/);
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0)
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('enables toggling details on each question', async () => {
|
||||
test("enables toggling details on each question", async () => {
|
||||
renderQuestionList();
|
||||
|
||||
// Initial state: Every question card is not expanded
|
||||
const questionCards = await waitForInitialQuestionsToRender()
|
||||
const questionCards = await waitForInitialQuestionsToRender();
|
||||
|
||||
// Expand first question card
|
||||
await expandAccordionAndGetIconButtons(questionCards[0])
|
||||
await expandAccordionAndGetIconButtons(questionCards[0]);
|
||||
|
||||
// Shrink first question card again
|
||||
fireEvent.click(questionCards[0])
|
||||
fireEvent.click(questionCards[0]);
|
||||
await waitFor(() => {
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0)
|
||||
expect(queryAllEditIconButtons()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('enables editing a question title', async () => {
|
||||
test("enables editing a question title", async () => {
|
||||
renderQuestionList(editQuestionMock);
|
||||
|
||||
const questionCards = await waitForInitialQuestionsToRender();
|
||||
const {editIconButton} = await expandAccordionAndGetIconButtons(questionCards[0]);
|
||||
const { editIconButton } = await expandAccordionAndGetIconButtons(
|
||||
questionCards[0]
|
||||
);
|
||||
|
||||
// open edit dialog
|
||||
expect(screen.queryByText(/Frage bearbeiten/)).toBeNull();
|
||||
fireEvent.click(editIconButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Frage bearbeiten/)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// change question title
|
||||
const questionTitleField = screen.getByDisplayValue(/Question 1/);
|
||||
fireEvent.change(questionTitleField, {target: {value: "New title for Question 1?"}});
|
||||
fireEvent.change(questionTitleField, {
|
||||
target: { value: "New title for Question 1?" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByDisplayValue(/New title for /)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update
|
||||
const confirmButton = screen.getByRole("button", {name: /Speichern/});
|
||||
const confirmButton = screen.getByRole("button", { name: /Speichern/ });
|
||||
fireEvent.click(confirmButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Frage bearbeiten/)).toBeNull();
|
||||
expect(screen.queryByText(/New title for Question 1/)).not.toBeNull()
|
||||
})
|
||||
expect(screen.queryByText(/New title for Question 1/)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('enables adding a question', async () => {
|
||||
test("enables adding a question", async () => {
|
||||
renderQuestionList(addQuestionMock);
|
||||
await waitForInitialQuestionsToRender();
|
||||
|
||||
|
@ -82,68 +93,82 @@ describe('The QuestionList', () => {
|
|||
fireEvent.click(addButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(dialogIdentifier)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// change question title
|
||||
const questionTitleField = screen.getByLabelText(/Zusammenfassung/);
|
||||
fireEvent.change(questionTitleField, {target: {value: "New question?"}});
|
||||
fireEvent.change(questionTitleField, {
|
||||
target: { value: "New question?" },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByDisplayValue(/New question/)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update
|
||||
const confirmButton = screen.getByRole("button", {name: /Erstellen/});
|
||||
const confirmButton = screen.getByRole("button", { name: /Erstellen/ });
|
||||
fireEvent.click(confirmButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(dialogIdentifier)).toBeNull();
|
||||
expect(screen.queryByText(/New question/)).not.toBeNull()
|
||||
})
|
||||
expect(screen.queryByText(/New question/)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('enables deleting a question', async () => {
|
||||
test("enables deleting a question", async () => {
|
||||
renderQuestionList(deleteQuestionMock);
|
||||
|
||||
const questionCards = await waitForInitialQuestionsToRender();
|
||||
expect(screen.queryByText(/Question 2/)).not.toBeNull();
|
||||
const {deleteIconButton} = await expandAccordionAndGetIconButtons(questionCards[1]);
|
||||
const { deleteIconButton } = await expandAccordionAndGetIconButtons(
|
||||
questionCards[1]
|
||||
);
|
||||
|
||||
// open delete confirmation dialog
|
||||
expect(screen.queryByText(/Frage löschen/)).toBeNull();
|
||||
fireEvent.click(deleteIconButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Frage löschen/)).not.toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
// call backend and assert apollo cache update
|
||||
const confirmButton = screen.getByRole("button", {name: /Löschen/});
|
||||
const confirmButton = screen.getByRole("button", { name: /Löschen/ });
|
||||
fireEvent.click(confirmButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Frage löschen/)).toBeNull();
|
||||
expect(screen.queryByText(/Question 2/)).toBeNull();
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderQuestionList(additionalMocks?: Array<MockedResponse>) {
|
||||
const initialMocks = [...getAllQuestionsMock, ...getQuestionByIdMock, ...getAllCategoriesMock];
|
||||
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
|
||||
const initialMocks = [
|
||||
...getAllQuestionsMock,
|
||||
...getQuestionByIdMock,
|
||||
...getAllCategoriesMock,
|
||||
];
|
||||
const allMocks = additionalMocks
|
||||
? [...initialMocks, ...additionalMocks]
|
||||
: initialMocks;
|
||||
return render(
|
||||
<MockedProvider mocks={allMocks}>
|
||||
<MemoryRouter>
|
||||
<SnackbarProvider>
|
||||
<QuestionList/>
|
||||
<QuestionList />
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const waitForInitialQuestionsToRender = async (): Promise<Array<HTMLElement>> => {
|
||||
const waitForInitialQuestionsToRender = async (): Promise<
|
||||
Array<HTMLElement>
|
||||
> => {
|
||||
const numberOfQuestionsInMockQuery = questionNodesMock.length;
|
||||
let questionCards: Array<HTMLElement> = [];
|
||||
await waitFor(() => {
|
||||
questionCards = screen.queryAllByRole("button", {name: /Question [1-3]\?/})
|
||||
questionCards = screen.queryAllByRole("button", {
|
||||
name: /Question [1-3]\?/,
|
||||
});
|
||||
expect(questionCards.length).toEqual(numberOfQuestionsInMockQuery);
|
||||
});
|
||||
return questionCards;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,48 +1,60 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import SignIn from "../components/SignIn";
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
import {MockedProvider} from '@apollo/client/testing';
|
||||
import {MemoryRouter} from 'react-router-dom';
|
||||
import {loginMock} from "../backend/mutations/login.mock";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MockedProvider } from "@apollo/client/testing";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { loginMock } from "../backend/mutations/login.mock";
|
||||
|
||||
const mockHistoryReplace = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
jest.mock("react-router-dom", () => ({
|
||||
...jest.requireActual("react-router-dom"),
|
||||
useHistory: () => ({
|
||||
replace: mockHistoryReplace,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('SignIn page', () => {
|
||||
beforeEach(() => mockHistoryReplace.mockReset())
|
||||
describe("SignIn page", () => {
|
||||
beforeEach(() => mockHistoryReplace.mockReset());
|
||||
|
||||
test('initial state', () => {
|
||||
render(<MockedProvider mocks={loginMock}><MemoryRouter><SignIn/></MemoryRouter></MockedProvider>);
|
||||
test("initial state", () => {
|
||||
render(
|
||||
<MockedProvider mocks={loginMock}>
|
||||
<MemoryRouter>
|
||||
<SignIn />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
// it renders empty email and passsword fields
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
expect(emailField).toHaveValue('');
|
||||
const emailField = screen.getByRole("textbox", { name: "Email Address" });
|
||||
expect(emailField).toHaveValue("");
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
expect(passwordField).toHaveValue('');
|
||||
expect(passwordField).toHaveValue("");
|
||||
|
||||
// it renders enabled submit button
|
||||
const button = screen.getByRole('button');
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).not.toBeDisabled();
|
||||
expect(button).toHaveTextContent('Sign In');
|
||||
expect(button).toHaveTextContent("Sign In");
|
||||
expect(mockHistoryReplace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('successful login', async () => {
|
||||
render(<MockedProvider mocks={loginMock}><MemoryRouter><SignIn/></MemoryRouter></MockedProvider>);
|
||||
test("successful login", async () => {
|
||||
render(
|
||||
<MockedProvider mocks={loginMock}>
|
||||
<MemoryRouter>
|
||||
<SignIn />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
const emailField = screen.getByRole("textbox", { name: "Email Address" });
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
const button = screen.getByRole('button', {name: /sign in/i});
|
||||
const button = screen.getByRole("button", { name: /sign in/i });
|
||||
|
||||
// fill out and submit form
|
||||
fireEvent.change(emailField, {target: {value: 'test@email.com'}});
|
||||
fireEvent.change(passwordField, {target: {value: 'password'}});
|
||||
fireEvent.change(emailField, { target: { value: "test@email.com" } });
|
||||
fireEvent.change(passwordField, { target: { value: "password" } });
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
|
@ -51,22 +63,28 @@ describe('SignIn page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('error login', async () => {
|
||||
render(<MockedProvider mocks={loginMock}><MemoryRouter><SignIn/></MemoryRouter></MockedProvider>);
|
||||
test("error login", async () => {
|
||||
render(
|
||||
<MockedProvider mocks={loginMock}>
|
||||
<MemoryRouter>
|
||||
<SignIn />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
const emailField = screen.getByRole("textbox", { name: "Email Address" });
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
const button = screen.getByRole('button');
|
||||
const button = screen.getByRole("button");
|
||||
|
||||
// fill out and submit form
|
||||
fireEvent.change(emailField, {target: {value: 'test@email.com'}});
|
||||
fireEvent.change(passwordField, {target: {value: 'wrong-password'}});
|
||||
fireEvent.change(emailField, { target: { value: "test@email.com" } });
|
||||
fireEvent.change(passwordField, { target: { value: "wrong-password" } });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
// it resets button
|
||||
expect(button).not.toBeDisabled();
|
||||
expect(button).toHaveTextContent('Sign In');
|
||||
expect(button).toHaveTextContent("Sign In");
|
||||
|
||||
// it displays error text
|
||||
const errorText = screen.getByText(/Wrong username or password/);
|
||||
|
|
|
@ -1,50 +1,110 @@
|
|||
import React from 'react';
|
||||
import {fireEvent, queryAllByRole, render, screen, waitFor} from '@testing-library/react'
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
fireEvent,
|
||||
queryAllByRole,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import {
|
||||
CandidatePosition,
|
||||
getIconForPosition,
|
||||
} from "../components/CandidatePositionLegend";
|
||||
|
||||
const memoizedGetIconPath = (icon: JSX.Element) => {
|
||||
let cache: { path?: string } = {};
|
||||
const cache: { path?: string } = {};
|
||||
return (): string => {
|
||||
if (cache?.path) {
|
||||
return cache.path
|
||||
return cache.path;
|
||||
} else {
|
||||
const {container} = render(icon)
|
||||
const path = container.innerHTML.match(/<path d="(.*)">/)?.[1]
|
||||
const { container } = render(icon);
|
||||
const path = container.innerHTML.match(/<path d="(.*)">/)?.[1];
|
||||
if (!path) {
|
||||
throw `Could not get path of MUI ${icon.type.displayName}`
|
||||
throw `Could not get path of MUI ${icon.type.displayName}`;
|
||||
}
|
||||
cache.path = path
|
||||
return path
|
||||
cache.path = path;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getEditIconPath = memoizedGetIconPath(<EditIcon/>)
|
||||
const getDeleteIconPath = memoizedGetIconPath(<DeleteIcon/>)
|
||||
const getAddIconPath = memoizedGetIconPath(<AddIcon/>)
|
||||
const getEditIconPath = memoizedGetIconPath(<EditIcon />);
|
||||
const getDeleteIconPath = memoizedGetIconPath(<DeleteIcon />);
|
||||
const getAddIconPath = memoizedGetIconPath(<AddIcon />);
|
||||
export const getPositivePositionPath = memoizedGetIconPath(
|
||||
getIconForPosition(CandidatePosition.positive)
|
||||
);
|
||||
export const getNeutralPositionPath = memoizedGetIconPath(
|
||||
getIconForPosition(CandidatePosition.neutral)
|
||||
);
|
||||
export const getNegativePositionPath = memoizedGetIconPath(
|
||||
getIconForPosition(CandidatePosition.negative)
|
||||
);
|
||||
export const getSkippedPositionPath = memoizedGetIconPath(
|
||||
getIconForPosition(CandidatePosition.skipped)
|
||||
);
|
||||
|
||||
export const queryAllIconButtons = (
|
||||
iconPath: string,
|
||||
container?: HTMLElement
|
||||
): HTMLElement[] => {
|
||||
return (container
|
||||
? queryAllByRole(container, "button")
|
||||
: screen.queryAllByRole("button")
|
||||
).filter(
|
||||
(button) =>
|
||||
button.innerHTML.includes("svg") && button.innerHTML.includes(iconPath)
|
||||
);
|
||||
};
|
||||
|
||||
// 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()));
|
||||
}
|
||||
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())
|
||||
);
|
||||
};
|
||||
|
||||
// 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()));
|
||||
}
|
||||
const queryAllDeleteIconButtons = (
|
||||
container?: HTMLElement
|
||||
): Array<HTMLElement> => {
|
||||
return (container
|
||||
? queryAllByRole(container, "button")
|
||||
: screen.queryAllByRole("button")
|
||||
).filter(
|
||||
(button) =>
|
||||
button.innerHTML.includes("svg") &&
|
||||
button.innerHTML.includes(getDeleteIconPath())
|
||||
);
|
||||
};
|
||||
|
||||
// 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()));
|
||||
}
|
||||
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())
|
||||
);
|
||||
};
|
||||
|
||||
export const expandAccordionAndGetIconButtons = async (accordion: HTMLElement): Promise<{ editIconButton: HTMLElement, deleteIconButton: HTMLElement }> => {
|
||||
export const expandAccordionAndGetIconButtons = async (
|
||||
accordion: HTMLElement
|
||||
): Promise<{ editIconButton: HTMLElement; deleteIconButton: HTMLElement }> => {
|
||||
let editIconsButtons = queryAllDeleteIconButtons();
|
||||
let deleteIconsButtons = queryAllEditIconButtons();
|
||||
expect(editIconsButtons).toHaveLength(0);
|
||||
|
@ -55,9 +115,9 @@ export const expandAccordionAndGetIconButtons = async (accordion: HTMLElement):
|
|||
deleteIconsButtons = queryAllDeleteIconButtons();
|
||||
expect(editIconsButtons).toHaveLength(1);
|
||||
expect(deleteIconsButtons).toHaveLength(1);
|
||||
})
|
||||
});
|
||||
return {
|
||||
editIconButton: editIconsButtons[0],
|
||||
deleteIconButton: deleteIconsButtons[0]
|
||||
deleteIconButton: deleteIconsButtons[0],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {parseJwt} from "./jwt";
|
||||
import { parseJwt } from "./jwt";
|
||||
|
||||
describe("The parseJwt function", () => {
|
||||
test("parses a valid candymat jwt", () => {
|
||||
const validJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6MSwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
|
||||
const validJwt =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6MSwiZXhwIjoxNjA5NDEyMTY4LCJpYXQiOjE2MDkyMzkzNjgsImF1ZCI6InBvc3RncmFwaGlsZSIsImlzcyI6InBvc3RncmFwaGlsZSJ9.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
|
||||
|
||||
const jwt = parseJwt(validJwt)
|
||||
const jwt = parseJwt(validJwt);
|
||||
|
||||
expect(jwt).not.toBeNull();
|
||||
expect(jwt?.person_row_id).toBe(1);
|
||||
|
@ -12,25 +13,28 @@ describe("The parseJwt function", () => {
|
|||
});
|
||||
|
||||
test("returns null if role claim is invalid", () => {
|
||||
const invalidRoleClaimJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHllZGl0b3IiLCJwZXJzb25fcm93X2lkIjoxLCJleHAiOjE2MDk0MTIxNjgsImlhdCI6MTYwOTIzOTM2OCwiYXVkIjoicG9zdGdyYXBoaWxlIiwiaXNzIjoicG9zdGdyYXBoaWxlIn0._AVFTMqMkIuyrfQGTmWE-Qi-C72KCrZ3s_uVyfuEDco";
|
||||
const invalidRoleClaimJwt =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHllZGl0b3IiLCJwZXJzb25fcm93X2lkIjoxLCJleHAiOjE2MDk0MTIxNjgsImlhdCI6MTYwOTIzOTM2OCwiYXVkIjoicG9zdGdyYXBoaWxlIiwiaXNzIjoicG9zdGdyYXBoaWxlIn0._AVFTMqMkIuyrfQGTmWE-Qi-C72KCrZ3s_uVyfuEDco";
|
||||
|
||||
const jwt = parseJwt(invalidRoleClaimJwt);
|
||||
|
||||
expect(jwt).toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
test("returns null if person_row_id is not a number", () => {
|
||||
const invalidRowIdClaimJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6IjEiLCJleHAiOjE2MDk0MTIxNjgsImlhdCI6MTYwOTIzOTM2OCwiYXVkIjoicG9zdGdyYXBoaWxlIiwiaXNzIjoicG9zdGdyYXBoaWxlIn0.NfXylzN44qrZA5DX0qxxU71vJ1o9gdunscnK6V193Fc";
|
||||
const invalidRowIdClaimJwt =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiY2FuZHltYXRfZWRpdG9yIiwicGVyc29uX3Jvd19pZCI6IjEiLCJleHAiOjE2MDk0MTIxNjgsImlhdCI6MTYwOTIzOTM2OCwiYXVkIjoicG9zdGdyYXBoaWxlIiwiaXNzIjoicG9zdGdyYXBoaWxlIn0.NfXylzN44qrZA5DX0qxxU71vJ1o9gdunscnK6V193Fc";
|
||||
|
||||
const jwt = parseJwt(invalidRowIdClaimJwt);
|
||||
|
||||
expect(jwt).toBeNull();
|
||||
})
|
||||
});
|
||||
|
||||
test("returns null if token is rubish.", () => {
|
||||
const brokenJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eysssssssssssssssssssss.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
|
||||
const brokenJwt =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eysssssssssssssssssssss.6VLDBS5_HwgWIf_MKkMCuj4EVBZkbSGm87aWt5grP2M";
|
||||
const jwt = parseJwt(brokenJwt);
|
||||
|
||||
expect(jwt).toBeNull();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,48 +1,49 @@
|
|||
export const getRawJsonWebToken = (): string | null => {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
return localStorage.getItem("token");
|
||||
};
|
||||
|
||||
export const getJsonWebToken = (): JwtPayload | null => {
|
||||
const rawToken = getRawJsonWebToken();
|
||||
return rawToken ? parseJwt(rawToken) : null
|
||||
}
|
||||
return rawToken ? parseJwt(rawToken) : null;
|
||||
};
|
||||
|
||||
export const parseJwt = (token: string): JwtPayload | null => {
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const base64Url = token.split(".")[1];
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
.split("")
|
||||
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
const jwtPayload = JSON.parse(jsonPayload);
|
||||
return isJwtPayloadValid(jwtPayload) ? jwtPayload : null
|
||||
return isJwtPayloadValid(jwtPayload) ? jwtPayload : null;
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const isJwtPayloadValid = (jwtPayload: JwtPayload): boolean => {
|
||||
return claims.every(claim => Object.keys(jwtPayload).includes(claim))
|
||||
&& userRoles.includes(jwtPayload.role)
|
||||
&& typeof (jwtPayload.person_row_id) === 'number'
|
||||
&& typeof (jwtPayload.exp) === 'number'
|
||||
&& typeof (jwtPayload.iat) === 'number';
|
||||
}
|
||||
return (
|
||||
claims.every((claim) => Object.keys(jwtPayload).includes(claim)) &&
|
||||
userRoles.includes(jwtPayload.role) &&
|
||||
typeof jwtPayload.person_row_id === "number" &&
|
||||
typeof jwtPayload.exp === "number" &&
|
||||
typeof jwtPayload.iat === "number"
|
||||
);
|
||||
};
|
||||
|
||||
const claims = ["role", "person_row_id", "exp", "iat", "aud", "iss"]
|
||||
const userRoles = ["candymat_editor", 'candymat_candidate', 'candymat_person']
|
||||
const claims = ["role", "person_row_id", "exp", "iat", "aud", "iss"];
|
||||
const userRoles = ["candymat_editor", "candymat_candidate", "candymat_person"];
|
||||
|
||||
interface JwtPayload {
|
||||
"role": UserRole,
|
||||
"person_row_id": number,
|
||||
"exp": number,
|
||||
"iat": number,
|
||||
"aud": "postgraphile",
|
||||
"iss": "postgraphile"
|
||||
role: UserRole;
|
||||
person_row_id: number;
|
||||
exp: number;
|
||||
iat: number;
|
||||
aud: "postgraphile";
|
||||
iss: "postgraphile";
|
||||
}
|
||||
|
||||
type UserRole = "candymat_editor" | 'candymat_candidate' | 'candymat_person'
|
||||
|
||||
type UserRole = "candymat_editor" | "candymat_candidate" | "candymat_person";
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === "localhost" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
|
@ -25,13 +25,10 @@ type Config = {
|
|||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
export function register(config?: Config): void {
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(
|
||||
process.env.PUBLIC_URL,
|
||||
window.location.href
|
||||
);
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
|
@ -39,7 +36,7 @@ export function register(config?: Config) {
|
|||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
|
@ -50,8 +47,8 @@ export function register(config?: Config) {
|
|||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
|
@ -65,21 +62,21 @@ export function register(config?: Config) {
|
|||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
"New content is available and will be used when all " +
|
||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
|
@ -90,7 +87,7 @@ function registerValidSW(swUrl: string, config?: Config) {
|
|||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
console.log("Content is cached for offline use.");
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
|
@ -101,25 +98,25 @@ function registerValidSW(swUrl: string, config?: Config) {
|
|||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
headers: { "Service-Worker": "script" },
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
@ -131,18 +128,18 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
|||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
export function unregister(): void {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
@ -19,7 +15,5 @@
|
|||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue