#11 Refactor: Disentangle CategoryList

This commit is contained in:
Christoph Lienhard 2020-12-30 22:13:48 +01:00
parent 0170e850a8
commit ff505226ab
Signed by: christoph.lienhard
GPG Key ID: 6B98870DDC270884
7 changed files with 170 additions and 157 deletions

View File

@ -1,5 +1,11 @@
import {MockedResponse} from "@apollo/client/testing";
import {BasicCategoryResponse, GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "./category";
import {
BasicCategoryResponse,
GET_ALL_CATEGORIES,
GET_CATEGORY_BY_ID,
GetAllCategoriesResponse,
GetCategoryByIdResponse
} from "./category";
export const categoryNodesMock: Array<BasicCategoryResponse> = [
@ -33,3 +39,30 @@ 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,
},
},
result: {
data: {
category: c,
},
},
})),
{
request: {
query: GET_CATEGORY_BY_ID,
variables: {
id: "",
},
},
result: {
data: {
category: null,
},
},
}
]

View File

@ -35,3 +35,19 @@ export interface GetAllCategoriesResponse {
}
}
export const GET_CATEGORY_BY_ID = gql`
query GetCategoryById($id:ID!) {
category(id: $id) {
...BasicCategoryFragment
}
}
${BasicCategoryFragment}
`
export interface GetCategoryByIdResponse {
category: BasicCategoryResponse | null,
}
export interface GetCategoryByIdVariables {
id: string,
}

View File

@ -1,25 +1,11 @@
import {Paper, Typography} from "@material-ui/core";
import React, {useState} from "react";
import React from "react";
import {makeStyles} from "@material-ui/core/styles";
import {useMutation, useQuery} from "@apollo/client";
import {useQuery} from "@apollo/client";
import AddCard from "./AddCard";
import AccordionWithEdit from "./AccordionWithEdit";
import {
BasicCategoryFragment,
BasicCategoryResponse,
GET_ALL_CATEGORIES,
GetAllCategoriesResponse
} from "../backend/queries/category";
import DialogChangeCategory, {ChangeCategoryDialogContent} from "./DialogChangeCategory";
import {useSnackbar} from "notistack";
import {
ADD_CATEGORY,
AddCategoryResponse,
AddCategoryVariables,
EDIT_CATEGORY,
EditCategoryResponse,
EditCategoryVariables
} from "../backend/mutations/category";
import {BasicCategoryResponse, GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "../backend/queries/category";
import DialogChangeCategory, {dialogChangeCategoryId, dialogChangeCategoryOpen} from "./DialogChangeCategory";
import DialogDeleteCategory, {
dialogDeleteCategoryId,
dialogDeleteCategoryOpen,
@ -34,80 +20,18 @@ const useStyles = makeStyles((theme) => ({
},
}));
const emptyChangeCategoryDialog: ChangeCategoryDialogContent = {
id: "",
title: "",
details: "",
}
export default function CategoryList() {
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
const [dialogTitle, setDialogTitle] = useState("");
const [dialogConfirmButtonText, setDialogConfirmButtonText] = useState("");
const [changeDialogContent, setChangeDialogContent] = useState(emptyChangeCategoryDialog);
const { enqueueSnackbar } = useSnackbar();
const categories = useQuery<GetAllCategoriesResponse, null>(GET_ALL_CATEGORIES).data?.allCategories.nodes;
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"})
setChangeDialogOpen(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"})
setChangeDialogOpen(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>({
data: data?.createCategory?.category,
fragment: BasicCategoryFragment,
fragmentName: "BasicCategoryFragment",
});
return {nodes: [...existingCategories.nodes, newCategoryRef]};
}
}
});
}
});
const classes = useStyles();
const loading = editLoading || addLoading;
const handleAddClick = () => {
setDialogTitle("Neue Kategorie erstellen");
setDialogConfirmButtonText("Erstellen");
if (changeDialogContent.id !== "") {
setChangeDialogContent(emptyChangeCategoryDialog);
}
setChangeDialogOpen(true);
dialogChangeCategoryId("")
dialogChangeCategoryOpen(true)
}
const handleEditButtonClick = (category: BasicCategoryResponse) => {
setDialogTitle("Kategorie bearbeiten");
setDialogConfirmButtonText("Speichern")
if (changeDialogContent.id !== category.id) {
setChangeDialogContent({
id: category.id,
title: category.title,
details: category.description,
})
}
setChangeDialogOpen(true);
dialogChangeCategoryId(category.id);
dialogChangeCategoryOpen(true)
};
const handleDeleteButtonClick = (category: BasicCategoryResponse) => {
@ -116,29 +40,6 @@ export default function CategoryList() {
dialogDeleteCategoryOpen(true);
}
const handleDialogContentChange = (content: ChangeCategoryDialogContent) => {
setChangeDialogContent(content)
}
const handleChangeConfirmButtonClick = () => {
if (changeDialogContent.id !== "") {
editCategory({
variables: {
id: changeDialogContent.id,
title: changeDialogContent.title,
description: changeDialogContent.details,
}
})
} else {
addCategory({
variables: {
title: changeDialogContent.title,
description: changeDialogContent.details,
}
})
}
};
return (
<Paper className={classes.root}>
<Typography component={"h2"} variant="h6" color="primary" gutterBottom>Kategorien</Typography>
@ -151,16 +52,7 @@ export default function CategoryList() {
/>
)}
<AddCard handleClick={handleAddClick}/>
<DialogChangeCategory
title={dialogTitle}
confirmButtonText={dialogConfirmButtonText}
open={changeDialogOpen}
content={changeDialogContent}
loading={loading}
handleContentChange={handleDialogContentChange}
handleConfirmButtonClick={handleChangeConfirmButtonClick}
handleClose={() => setChangeDialogOpen(false)}
/>
<DialogChangeCategory/>
<DialogDeleteCategory/>
</Paper>
)

View File

@ -1,50 +1,122 @@
import React from 'react';
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} from "@apollo/client";
import {makeVar, useMutation, useQuery, useReactiveVar} from "@apollo/client";
import {useSnackbar} from "notistack";
import {
BasicCategoryFragment,
BasicCategoryResponse,
GET_CATEGORY_BY_ID,
GetCategoryByIdResponse,
GetCategoryByIdVariables
} from "../backend/queries/category";
import {
ADD_CATEGORY,
AddCategoryResponse,
AddCategoryVariables,
EDIT_CATEGORY,
EditCategoryResponse,
EditCategoryVariables
} from "../backend/mutations/category";
export interface ChangeCategoryDialogContent {
id: string
title: string,
details: string | null,
}
export const dialogChangeCategoryId = makeVar<string>("");
export const dialogChangeCategoryOpen = makeVar<boolean>(false);
interface DialogChangeCategoryProps {
title: string,
confirmButtonText: string,
open: boolean,
content: ChangeCategoryDialogContent,
loading: boolean,
export default function DialogChangeCategory() {
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"}),
onCompleted: (response) => {
if (response.updateCategory) {
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"})
}
},
update: (cache, {data}) => {
cache.modify({
fields: {
allCategories(existingCategories = {nodes: []}) {
const newCategoryRef = cache.writeFragment<BasicCategoryResponse | undefined>({
data: data?.createCategory?.category,
fragment: BasicCategoryFragment,
fragmentName: "BasicCategoryFragment",
});
return {nodes: [...existingCategories.nodes, newCategoryRef]};
}
}
});
}
});
handleContentChange(content: ChangeCategoryDialogContent): void
const handleConfirmButtonClick = () => {
if (addMode) {
addCategory({
variables: {
title,
description: details,
}
})
} else {
editCategory({
variables: {
id: categoryId,
title: title,
description: details,
}
})
}
}
handleConfirmButtonClick(): void,
handleClose(): void,
}
export default function DialogChangeCategory(props: DialogChangeCategoryProps) {
return (
<Dialog open={props.open} onClose={props.handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
<Dialog open={open} onClose={() => dialogChangeCategoryOpen(false)} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">
{addMode ? "Neue Kategorie erstellen" : "Kategorie bearbeiten"}
</DialogTitle>
<DialogContent>
<DialogTitleAndDetails
title={props.content.title}
details={props.content.details}
handleTitleChange={newTitle => props.handleContentChange({...props.content, title: newTitle})}
handleDetailsChange={newDetails => props.handleContentChange({...props.content, details: newDetails})}
title={title}
details={details}
onTitleChange={newTitle => setTitle(newTitle)}
onDetailsChange={newDetails => setDetails(newDetails)}
/>
</DialogContent>
<DialogActionBar
onClose={props.handleClose}
onConfirmButtonClick={props.handleConfirmButtonClick}
confirmButtonText={props.confirmButtonText}
loading={props.loading}
onClose={() => dialogChangeCategoryOpen(false)}
onConfirmButtonClick={handleConfirmButtonClick}
confirmButtonText={addMode ? "Erstellen" : "Speichern"}
loading={addMode ? addLoading : editLoading}
/>
</Dialog>
);

View File

@ -39,8 +39,8 @@ export default function DialogChangeQuestion(props: DialogChangeQuestionProps) {
<DialogTitleAndDetails
title={props.content.title}
details={props.content.details}
handleTitleChange={newTitle => props.handleContentChange({...props.content, title: newTitle})}
handleDetailsChange={newDetails => props.handleContentChange({...props.content, details: newDetails})}
onTitleChange={newTitle => props.handleContentChange({...props.content, title: newTitle})}
onDetailsChange={newDetails => props.handleContentChange({...props.content, details: newDetails})}
/>
<CategorySelectionMenu
selectedCategoryId={props.content.categoryId}

View File

@ -12,9 +12,9 @@ interface DialogTitleAndDetailsProps {
title: string,
details?: string | null,
handleTitleChange(newTitle: string): void,
onTitleChange(newTitle: string): void,
handleDetailsChange(newDetails: string): void,
onDetailsChange(newDetails: string): void,
}
export function DialogTitleAndDetails(props: DialogTitleAndDetailsProps) {
@ -29,7 +29,7 @@ export function DialogTitleAndDetails(props: DialogTitleAndDetailsProps) {
fullWidth
variant="outlined"
value={props.title}
onChange={e => props.handleTitleChange(e.target.value)}
onChange={e => props.onTitleChange(e.target.value)}
/>
<TextField
className={classes.textField}
@ -40,7 +40,7 @@ export function DialogTitleAndDetails(props: DialogTitleAndDetailsProps) {
fullWidth
variant="outlined"
value={props.details}
onChange={e => props.handleDetailsChange(e.target.value)}
onChange={e => props.onDetailsChange(e.target.value)}
/>
</React.Fragment>
)

View File

@ -4,7 +4,7 @@ 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} from "../backend/queries/category.mock";
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";
@ -119,7 +119,7 @@ describe('The CategoryList', () => {
});
function renderCategoryList(additionalMocks?: Array<MockedResponse>) {
const initialMocks = [...getAllCategoriesMock];
const initialMocks = [...getAllCategoriesMock, ...getCategoryByIdMock];
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
return render(
<MockedProvider mocks={allMocks}>