#11 Implement AddQuestion
Also introduce notistack/snackbar to handle async error/success messages
This commit is contained in:
parent
dd2f414f00
commit
7370a7c493
10
redaktions-app/package-lock.json
generated
10
redaktions-app/package-lock.json
generated
|
@ -9603,6 +9603,16 @@
|
|||
"sort-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"notistack": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-1.0.3.tgz",
|
||||
"integrity": "sha512-bRGF/eg2qNQ8BwagPLkHiqrz+W00PYtGY5Xl33I0Of1BTm7arksZO1JxssPTlti0qw127CxuWxm637ipn0eZ9g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clsx": "^1.1.0",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^3.4.4",
|
||||
"typescript": "^3.8"
|
||||
"typescript": "^3.8",
|
||||
"notistack": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
|
|
|
@ -27,21 +27,17 @@ export const ADD_QUESTION = gql`
|
|||
mutation AddQuestion($text: String!, $description: String, $categoryRowId: Int) {
|
||||
createQuestion(input: {question: {text: $text, categoryRowId: $categoryRowId, description: $description}}) {
|
||||
question {
|
||||
id
|
||||
text
|
||||
description
|
||||
categoryByCategoryRowId {
|
||||
id
|
||||
rowId
|
||||
title
|
||||
}
|
||||
...BasicQuestionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
|
||||
export interface AddQuestionResponse {
|
||||
createQuestion?: BasicQuestionResponse
|
||||
createQuestion?: {
|
||||
question: BasicQuestionResponse
|
||||
}
|
||||
}
|
||||
|
||||
export interface AddQuestionVariables {
|
||||
|
@ -51,24 +47,20 @@ export interface AddQuestionVariables {
|
|||
}
|
||||
|
||||
export const DELETE_QUESTION = gql`
|
||||
mutation AddQuestion($text: String!, $description: String, $categoryRowId: Int) {
|
||||
createQuestion(input: {question: {text: $text, categoryRowId: $categoryRowId, description: $description}}) {
|
||||
mutation DeleteQuestion($id: ID!) {
|
||||
deleteQuestion(input: { id: $id }) {
|
||||
question {
|
||||
id
|
||||
text
|
||||
description
|
||||
categoryByCategoryRowId {
|
||||
id
|
||||
rowId
|
||||
title
|
||||
}
|
||||
...BasicQuestionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${BasicQuestionFragment}
|
||||
`
|
||||
|
||||
export interface DeleteQuestionResponse {
|
||||
deleteQuestion?: BasicQuestionResponse
|
||||
deleteQuestion?: {
|
||||
question: BasicQuestionResponse
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeleteQuestionVariables {
|
||||
|
|
|
@ -49,5 +49,3 @@ export interface GetAllQuestionsResponse {
|
|||
nodes: Array<BasicQuestionResponse>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import {Paper, Snackbar, Typography} from "@material-ui/core";
|
||||
import {Paper, Typography} from "@material-ui/core";
|
||||
import React, {useState} from "react";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import {useMutation, useQuery} from "@apollo/client";
|
||||
import AddCard from "./AddCard";
|
||||
import AccordionWithEdit from "./AccordionWithEdit";
|
||||
import {GET_ALL_QUESTIONS, GetAllQuestionsResponse, BasicQuestionResponse} from "../backend/queries/question";
|
||||
import {
|
||||
BasicQuestionFragment,
|
||||
BasicQuestionResponse,
|
||||
GET_ALL_QUESTIONS,
|
||||
GetAllQuestionsResponse
|
||||
} from "../backend/queries/question";
|
||||
import ChangeQuestionDialog, {ChangeQuestionDialogContent} from "./ChangeQuestionDialog";
|
||||
import {GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "../backend/queries/category";
|
||||
import {EDIT_QUESTION, EditQuestionResponse, EditQuestionVariables} from "../backend/mutations/question";
|
||||
import {Alert, Color} from "@material-ui/lab";
|
||||
import {
|
||||
ADD_QUESTION,
|
||||
AddQuestionResponse,
|
||||
AddQuestionVariables,
|
||||
EDIT_QUESTION,
|
||||
EditQuestionResponse,
|
||||
EditQuestionVariables
|
||||
} from "../backend/mutations/question";
|
||||
import {useSnackbar} from 'notistack';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
|
@ -25,22 +37,53 @@ const emptyChangeQuestionDialog: ChangeQuestionDialogContent = {
|
|||
categoryId: null,
|
||||
}
|
||||
|
||||
interface SnackbarProps {
|
||||
open: boolean,
|
||||
message?: string,
|
||||
severity?: Color,
|
||||
}
|
||||
|
||||
export default function QuestionList() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogTitle, setDialogTitle] = useState("");
|
||||
const [dialogConfirmButtonText, setDialogConfirmButtonText] = useState("");
|
||||
const [dialogContent, setDialogContent] = useState(emptyChangeQuestionDialog);
|
||||
const [snackbarProps, setSnackbarProps] = useState<SnackbarProps>({open: false});
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const questions = useQuery<GetAllQuestionsResponse, null>(GET_ALL_QUESTIONS).data?.allQuestions.nodes;
|
||||
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"}),
|
||||
onCompleted: (response) => {
|
||||
if (response.updateQuestion) {
|
||||
enqueueSnackbar("Frage erfolgreich geändert.", { variant: "success"})
|
||||
setDialogOpen(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"})
|
||||
setDialogOpen(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>({
|
||||
data: data?.createQuestion?.question,
|
||||
fragment: BasicQuestionFragment,
|
||||
fragmentName: "BasicQuestionFragment",
|
||||
});
|
||||
return {nodes: [...existingQuestions.nodes, newQuestionRef]};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
const classes = useStyles();
|
||||
const [editQuestion, {loading}] = useMutation<EditQuestionResponse, EditQuestionVariables>(EDIT_QUESTION);
|
||||
|
||||
const loading = editLoading || addLoading;
|
||||
|
||||
const handleAddClick = () => {
|
||||
setDialogTitle("Neue Frage erstellen");
|
||||
|
@ -69,29 +112,27 @@ export default function QuestionList() {
|
|||
setDialogContent(content)
|
||||
}
|
||||
|
||||
const handleConfirmButtonClick = async () => {
|
||||
const handleConfirmButtonClick = () => {
|
||||
if (dialogContent.id !== "") {
|
||||
const response = await editQuestion({
|
||||
editQuestion({
|
||||
variables: {
|
||||
id: dialogContent.id,
|
||||
text: dialogContent.title,
|
||||
description: dialogContent.details,
|
||||
categoryRowId: dialogContent.categoryId
|
||||
categoryRowId: dialogContent.categoryId,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
addQuestion({
|
||||
variables: {
|
||||
text: dialogContent.title,
|
||||
description: dialogContent.details,
|
||||
categoryRowId: dialogContent.categoryId,
|
||||
}
|
||||
})
|
||||
if (response.data?.updateQuestion) {
|
||||
setSnackbarProps({
|
||||
message: "Frage erfolgreich geändert",
|
||||
open: true,
|
||||
severity: 'success',
|
||||
})
|
||||
setDialogOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSnackbarClose = () => setSnackbarProps({...snackbarProps, open: false})
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>Fragen</Typography>
|
||||
|
@ -117,14 +158,6 @@ export default function QuestionList() {
|
|||
handleConfirmButtonClick={handleConfirmButtonClick}
|
||||
handleClose={() => setDialogOpen(false)}
|
||||
/>
|
||||
<Snackbar
|
||||
anchorOrigin={{vertical: 'bottom', horizontal: "right"}}
|
||||
open={snackbarProps.open}
|
||||
onClose={handleSnackbarClose}
|
||||
autoHideDuration={6000}
|
||||
>
|
||||
<Alert onClose={handleSnackbarClose} severity={snackbarProps.severity}>{snackbarProps.message}</Alert>
|
||||
</Snackbar>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,11 +6,14 @@ 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>
|
||||
<App/>
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<App/>
|
||||
</SnackbarProvider>
|
||||
</Router>
|
||||
</ApolloProvider>,
|
||||
document.getElementById('root')
|
||||
|
|
|
@ -3,20 +3,31 @@ 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";
|
||||
|
||||
const renderAppAtUrl = (path: string) => render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<SnackbarProvider>
|
||||
<App/>
|
||||
</SnackbarProvider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => localStorage.clear())
|
||||
|
||||
describe('The root path /', () => {
|
||||
|
||||
test('renders user\'s home page if they are logged in',() => {
|
||||
test('renders user\'s home page if they are logged in', () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf")
|
||||
render(<MockedProvider><MemoryRouter initialEntries={['/']}><App/></MemoryRouter></MockedProvider>);
|
||||
renderAppAtUrl("/");
|
||||
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow()
|
||||
});
|
||||
|
||||
test('redirects to login page if user not logged in', () => {
|
||||
render(<MockedProvider><MemoryRouter initialEntries={['/']}><App/></MemoryRouter></MockedProvider>);
|
||||
renderAppAtUrl("/");
|
||||
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
|
@ -27,7 +38,7 @@ describe('The root path /', () => {
|
|||
|
||||
describe('The /login path', () => {
|
||||
test('renders the signin page if the user is not logged in', () => {
|
||||
render(<MockedProvider><MemoryRouter initialEntries={['/login']}><App/></MemoryRouter></MockedProvider>);
|
||||
renderAppAtUrl("/login");
|
||||
|
||||
const emailField = screen.getByRole('textbox', {name: 'Email Address'});
|
||||
const passwordField = screen.getByLabelText(/Password/);
|
||||
|
@ -37,7 +48,7 @@ describe('The /login path', () => {
|
|||
|
||||
test('redirects to root / and the user\'s home page if the user is logged in', () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf")
|
||||
render(<MockedProvider><MemoryRouter initialEntries={['/login']}><App/></MemoryRouter></MockedProvider>);
|
||||
renderAppAtUrl("/login");
|
||||
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow()
|
||||
});
|
||||
|
@ -45,7 +56,7 @@ describe('The /login path', () => {
|
|||
|
||||
describe('The /signup path', () => {
|
||||
test('renders the signup page if the user is not logged in', () => {
|
||||
render(<MockedProvider><MemoryRouter initialEntries={['/signup']}><App/></MemoryRouter></MockedProvider>);
|
||||
renderAppAtUrl("/signup");
|
||||
|
||||
expect(() => screen.getByRole('textbox', {name: 'Email Address'})).not.toThrow()
|
||||
expect(() => screen.getByLabelText(/Password/)).not.toThrow()
|
||||
|
@ -55,7 +66,7 @@ describe('The /signup path', () => {
|
|||
|
||||
test('redirects to root / and the user\'s home page if the user is logged in', () => {
|
||||
localStorage.setItem("token", "asdfasdfasdf")
|
||||
render(<MockedProvider><MemoryRouter initialEntries={['/signup']}><App/></MemoryRouter></MockedProvider>);
|
||||
renderAppAtUrl("/signup");
|
||||
|
||||
expect(() => screen.getByLabelText(/current user/)).not.toThrow()
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue