#11 Refactor: Disentangle QuestionList

This commit is contained in:
Christoph Lienhard 2020-12-30 22:46:26 +01:00
parent ff505226ab
commit 0016e12acd
Signed by: christoph.lienhard
GPG key ID: 6B98870DDC270884
6 changed files with 185 additions and 170 deletions

View file

@ -1,5 +1,11 @@
import {MockedResponse} from "@apollo/client/testing"; import {MockedResponse} from "@apollo/client/testing";
import {BasicQuestionResponse, GET_ALL_QUESTIONS, GetAllQuestionsResponse} from "./question"; import {
BasicQuestionResponse,
GET_ALL_QUESTIONS,
GET_QUESTION_BY_ID,
GetAllQuestionsResponse,
GetQuestionByIdResponse
} from "./question";
export const questionNodesMock: Array<BasicQuestionResponse> = [{ export const questionNodesMock: Array<BasicQuestionResponse> = [{
@ -46,3 +52,32 @@ export const getAllQuestionsMock: Array<MockedResponse<GetAllQuestionsResponse>>
}, },
] ]
export const getQuestionByIdMock: Array<MockedResponse<GetQuestionByIdResponse>> = [
...questionNodesMock.map(q => ({
request: {
query: GET_QUESTION_BY_ID,
variables: {
id: q.id,
},
},
result: {
data: {
question: q,
},
},
})),
{
request: {
query: GET_QUESTION_BY_ID,
variables: {
id: "",
},
},
result: {
data: {
question: null,
},
},
}
]

View file

@ -52,3 +52,20 @@ export interface GetAllQuestionsResponse {
__typename: "QuestionsConnection", __typename: "QuestionsConnection",
} }
} }
export const GET_QUESTION_BY_ID = gql`
query GetQuestionById($id:ID!) {
question(id: $id) {
...BasicQuestionFragment
}
}
${BasicQuestionFragment}
`
export interface GetQuestionByIdResponse {
question: BasicQuestionResponse | null,
}
export interface GetQuestionByIdVariables {
id: string,
}

View file

@ -42,7 +42,7 @@ export default function DialogChangeCategory() {
setTitle(data.category?.title || ""); setTitle(data.category?.title || "");
setDetails(data.category?.description || "") setDetails(data.category?.description || "")
}) })
}) });
const [editCategory, {loading: editLoading}] = useMutation<EditCategoryResponse, EditCategoryVariables>(EDIT_CATEGORY, { const [editCategory, {loading: editLoading}] = useMutation<EditCategoryResponse, EditCategoryVariables>(EDIT_CATEGORY, {
onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}), onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
onCompleted: (response) => { onCompleted: (response) => {

View file

@ -1,61 +1,134 @@
import React from 'react'; import React, {useState} from 'react';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import {BasicCategoryResponse} from "../backend/queries/category";
import CategorySelectionMenu from "./CategorySelectionMenu";
import {DialogActionBar} from "./DialogActionBar"; import {DialogActionBar} from "./DialogActionBar";
import {DialogTitleAndDetails} from "./DialogTitleAndDetails"; 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
} from "../backend/mutations/question";
import {
BasicQuestionFragment,
BasicQuestionResponse,
GET_QUESTION_BY_ID,
GetQuestionByIdResponse,
GetQuestionByIdVariables
} from "../backend/queries/question";
import CategorySelectionMenu from "./CategorySelectionMenu";
import {GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "../backend/queries/category";
export const dialogChangeQuestionId = makeVar<string>("");
export const dialogChangeQuestionOpen = makeVar<boolean>(false);
export interface ChangeQuestionDialogContent { export default function DialogChangeQuestion() {
id: string const [addMode, setAddMode] = useState(true);
title: string, const [title, setTitle] = useState("");
details: string | null, const [details, setDetails] = useState("");
categoryId: number | null, 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;
interface DialogChangeQuestionProps { const [editQuestion, {loading: editLoading}] = useMutation<EditQuestionResponse, EditQuestionVariables>(EDIT_QUESTION, {
title: string, onError: (e) => enqueueSnackbar(`Ein Fehler ist aufgetreten: ${e.message}`, {variant: "error"}),
confirmButtonText: string, onCompleted: (response) => {
open: boolean, if (response.updateQuestion) {
content: ChangeQuestionDialogContent, enqueueSnackbar("Frage erfolgreich geändert.", {variant: "success"})
loading: boolean, dialogChangeQuestionOpen(false);
categories?: Array<BasicCategoryResponse>, } 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"})
}
},
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]};
}
}
});
}
});
handleContentChange(content: ChangeQuestionDialogContent): void const handleConfirmButtonClick = () => {
if (addMode) {
addQuestion({
variables: {
title,
description: details,
categoryRowId: categoryRowId,
}
})
} else {
editQuestion({
variables: {
id: questionId,
title: title,
description: details,
categoryRowId: categoryRowId,
}
})
}
}
handleConfirmButtonClick(): void,
handleClose(): void,
}
export default function DialogChangeQuestion(props: DialogChangeQuestionProps) {
return ( return (
<Dialog open={props.open} onClose={props.handleClose} aria-labelledby="form-dialog-title"> <Dialog open={open} onClose={() => dialogChangeQuestionOpen(false)} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle> <DialogTitle id="form-dialog-title">
{addMode ? "Neue Frage erstellen" : "Frage bearbeiten"}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogTitleAndDetails <DialogTitleAndDetails
title={props.content.title} title={title}
details={props.content.details} details={details}
onTitleChange={newTitle => props.handleContentChange({...props.content, title: newTitle})} onTitleChange={newTitle => setTitle(newTitle)}
onDetailsChange={newDetails => props.handleContentChange({...props.content, details: newDetails})} onDetailsChange={newDetails => setDetails(newDetails)}
/> />
<CategorySelectionMenu <CategorySelectionMenu
selectedCategoryId={props.content.categoryId} selectedCategoryId={categoryRowId}
categories={props.categories} categories={categories}
handleCategoryChange={(categoryId) => props.handleContentChange({ handleCategoryChange={(categoryId) => setCategoryRowId(categoryId)}
...props.content,
categoryId: categoryId,
})}
/> />
</DialogContent> </DialogContent>
<DialogActionBar <DialogActionBar
loading={props.loading} onClose={() => dialogChangeQuestionOpen(false)}
onClose={props.handleClose} onConfirmButtonClick={handleConfirmButtonClick}
onConfirmButtonClick={props.handleConfirmButtonClick} confirmButtonText={addMode ? "Erstellen" : "Speichern"}
confirmButtonText={props.confirmButtonText} loading={addMode ? addLoading : editLoading}
/> />
</Dialog> </Dialog>
); );

View file

@ -1,27 +1,16 @@
import {Paper, Typography} from "@material-ui/core"; import {Paper, Typography} from "@material-ui/core";
import React, {useState} from "react"; import React from "react";
import {makeStyles} from "@material-ui/core/styles"; import {makeStyles} from "@material-ui/core/styles";
import {useMutation, useQuery} from "@apollo/client"; import {useQuery} from "@apollo/client";
import AddCard from "./AddCard"; import AddCard from "./AddCard";
import AccordionWithEdit from "./AccordionWithEdit"; import AccordionWithEdit from "./AccordionWithEdit";
import { import {BasicQuestionResponse, GET_ALL_QUESTIONS, GetAllQuestionsResponse} from "../backend/queries/question";
BasicQuestionFragment, import DialogChangeQuestion, {dialogChangeQuestionId, dialogChangeQuestionOpen} from "./DialogChangeQuestion";
BasicQuestionResponse, import DialogDeleteQuestion, {
GET_ALL_QUESTIONS, dialogDeleteQuestionId,
GetAllQuestionsResponse dialogDeleteQuestionOpen,
} from "../backend/queries/question"; dialogDeleteQuestionTitle
import DialogChangeQuestion, {ChangeQuestionDialogContent} from "./DialogChangeQuestion"; } from "./DialogDeleteQuestion";
import {GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "../backend/queries/category";
import {
ADD_QUESTION,
AddQuestionResponse,
AddQuestionVariables,
EDIT_QUESTION,
EditQuestionResponse,
EditQuestionVariables
} from "../backend/mutations/question";
import {useSnackbar} from 'notistack';
import DialogDeleteQuestion, {dialogDeleteQuestionId, dialogDeleteQuestionOpen, dialogDeleteQuestionTitle} from "./DialogDeleteQuestion";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@ -31,82 +20,18 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
const emptyChangeQuestionDialog: ChangeQuestionDialogContent = {
id: "",
title: "",
details: "",
categoryId: null,
}
export default function QuestionList() { export default function QuestionList() {
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
const [dialogTitle, setDialogTitle] = useState("");
const [dialogConfirmButtonText, setDialogConfirmButtonText] = useState("");
const [changeDialogContent, setChangeDialogContent] = useState(emptyChangeQuestionDialog);
const {enqueueSnackbar} = useSnackbar();
const questions = useQuery<GetAllQuestionsResponse, null>(GET_ALL_QUESTIONS).data?.allQuestions.nodes; 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"})
setChangeDialogOpen(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"})
setChangeDialogOpen(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 classes = useStyles();
const loading = editLoading || addLoading; const handleAddButtonClick = () => {
dialogChangeQuestionId("")
const handleAddClick = () => { dialogChangeQuestionOpen(true)
setDialogTitle("Neue Frage erstellen");
setDialogConfirmButtonText("Erstellen");
if (changeDialogContent.id !== "") {
setChangeDialogContent(emptyChangeQuestionDialog);
}
setChangeDialogOpen(true);
} }
const handleEditButtonClick = (question: BasicQuestionResponse) => { const handleEditButtonClick = (question: BasicQuestionResponse) => {
setDialogTitle("Frage bearbeiten"); dialogChangeQuestionId(question.id)
setDialogConfirmButtonText("Speichern") dialogChangeQuestionOpen(true)
if (changeDialogContent.id !== question.id) {
setChangeDialogContent({
id: question.id,
title: question.title,
details: question.description,
categoryId: question.categoryByCategoryRowId ? question.categoryByCategoryRowId.rowId : null,
})
}
setChangeDialogOpen(true);
}; };
const handleDeleteButtonClick = (question: BasicQuestionResponse) => { const handleDeleteButtonClick = (question: BasicQuestionResponse) => {
@ -115,31 +40,6 @@ export default function QuestionList() {
dialogDeleteQuestionOpen(true); dialogDeleteQuestionOpen(true);
} }
const handleDialogContentChange = (content: ChangeQuestionDialogContent) => {
setChangeDialogContent(content)
}
const handleChangeConfirmButtonClick = () => {
if (changeDialogContent.id !== "") {
editQuestion({
variables: {
id: changeDialogContent.id,
title: changeDialogContent.title,
description: changeDialogContent.details,
categoryRowId: changeDialogContent.categoryId,
}
})
} else {
addQuestion({
variables: {
title: changeDialogContent.title,
description: changeDialogContent.details,
categoryRowId: changeDialogContent.categoryId,
}
})
}
};
return ( return (
<Paper className={classes.root}> <Paper className={classes.root}>
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>Fragen</Typography> <Typography component={"h2"} variant="h6" color="primary" gutterBottom>Fragen</Typography>
@ -152,18 +52,8 @@ export default function QuestionList() {
onDeleteButtonClick={() => handleDeleteButtonClick(question)} onDeleteButtonClick={() => handleDeleteButtonClick(question)}
/> />
)} )}
<AddCard handleClick={handleAddClick}/> <AddCard handleClick={handleAddButtonClick}/>
<DialogChangeQuestion <DialogChangeQuestion/>
title={dialogTitle}
confirmButtonText={dialogConfirmButtonText}
open={changeDialogOpen}
content={changeDialogContent}
categories={categories}
loading={loading}
handleContentChange={handleDialogContentChange}
handleConfirmButtonClick={handleChangeConfirmButtonClick}
handleClose={() => setChangeDialogOpen(false)}
/>
<DialogDeleteQuestion/> <DialogDeleteQuestion/>
</Paper> </Paper>
) )

View file

@ -4,7 +4,7 @@ import {MockedProvider, MockedResponse} from '@apollo/client/testing';
import {MemoryRouter} from 'react-router-dom'; import {MemoryRouter} from 'react-router-dom';
import QuestionList from "../components/QuestionList"; import QuestionList from "../components/QuestionList";
import {SnackbarProvider} from "notistack"; import {SnackbarProvider} from "notistack";
import {getAllQuestionsMock, questionNodesMock} from "../backend/queries/question.mock"; import {getAllQuestionsMock, getQuestionByIdMock, questionNodesMock} from "../backend/queries/question.mock";
import {getAllCategoriesMock} from "../backend/queries/category.mock"; import {getAllCategoriesMock} from "../backend/queries/category.mock";
import {addQuestionMock, deleteQuestionMock, editQuestionMock} from "../backend/mutations/question.mock"; import {addQuestionMock, deleteQuestionMock, editQuestionMock} from "../backend/mutations/question.mock";
import { import {
@ -125,7 +125,7 @@ describe('The QuestionList', () => {
}); });
function renderQuestionList(additionalMocks?: Array<MockedResponse>) { function renderQuestionList(additionalMocks?: Array<MockedResponse>) {
const initialMocks = [...getAllQuestionsMock, ...getAllCategoriesMock]; const initialMocks = [...getAllQuestionsMock, ...getQuestionByIdMock, ...getAllCategoriesMock];
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
return render( return render(
<MockedProvider mocks={allMocks}> <MockedProvider mocks={allMocks}>