mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: added category editing
This commit is contained in:
parent
6e2aa84b4a
commit
c3fa9070f1
@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import BaseModal from '@educt/components/Modal';
|
||||||
|
import { ICategory } from '@educt/interfaces';
|
||||||
|
import { MdModeEdit } from 'react-icons/md';
|
||||||
|
import { FormControl, FormLabel, FormHelperText, Stack, Input, Textarea } from '@chakra-ui/react';
|
||||||
|
import { UpdateCategoryParamsType } from '@educt/types';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema
|
||||||
|
*/
|
||||||
|
import EditCategorySchema from './EditCategoryModal.validator';
|
||||||
|
import { useUpdateCategory } from '@educt/hooks/queries';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
type EditCategoryModalPropsType = {
|
||||||
|
category: ICategory;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onEdited?: (updatedCategory: ICategory) => void | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateCategoryInputType = {
|
||||||
|
title?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditCategoryModal: React.FC<EditCategoryModalPropsType> = props => {
|
||||||
|
const { category, onClose, onEdited, isOpen } = props;
|
||||||
|
const defaultValues = {
|
||||||
|
title: category.title,
|
||||||
|
description: category.description,
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors, isDirty },
|
||||||
|
} = useForm<UpdateCategoryInputType>({
|
||||||
|
resolver: yupResolver(EditCategorySchema),
|
||||||
|
defaultValues: useMemo(() => {
|
||||||
|
return defaultValues;
|
||||||
|
}, [props]),
|
||||||
|
});
|
||||||
|
const { updateCategory, isLoading } = useUpdateCategory();
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdateCategoryParamsType) => {
|
||||||
|
try {
|
||||||
|
const updated = await updateCategory(category.id, data);
|
||||||
|
onClose();
|
||||||
|
onEdited && onEdited(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(defaultValues);
|
||||||
|
}, [props.category]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal
|
||||||
|
icon={MdModeEdit}
|
||||||
|
heading='Edit category'
|
||||||
|
confirmText='Save'
|
||||||
|
loadingText='Saving...'
|
||||||
|
isLoading={isLoading}
|
||||||
|
isOpen={isOpen}
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
onClose={onClose}
|
||||||
|
onProceed={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Stack spacing={'4'}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<Input placeholder='e.g Management' {...register('title')} isInvalid={!!errors.title} />
|
||||||
|
{errors.title && <FormHelperText color='red.500'>{errors?.title.message}</FormHelperText>}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<Textarea resize={'none'} placeholder='Write a description...' {...register('description')} />
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCategoryModal;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import yup from '@educt/schema';
|
||||||
|
|
||||||
|
const EditCategorySchema = yup.object().shape({
|
||||||
|
title: yup.string().optional(),
|
||||||
|
desctiption: yup.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EditCategorySchema;
|
||||||
3
src/components/Modals/EditCategoryModal/index.ts
Normal file
3
src/components/Modals/EditCategoryModal/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import EditCategoryModal from './EditCategoryModal';
|
||||||
|
|
||||||
|
export default EditCategoryModal;
|
||||||
@ -1,16 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Box, Text, Table, Tbody, useDisclosure } from '@chakra-ui/react';
|
import { Flex, Box, Text, Table, Tbody } from '@chakra-ui/react';
|
||||||
import { CreateButton } from '@educt/components/Buttons';
|
import { CreateButton } from '@educt/components/Buttons';
|
||||||
import { ICategory } from '@educt/interfaces';
|
import { ICategory } from '@educt/interfaces';
|
||||||
import CategoryTableHead from './CategoryTableHead';
|
import CategoryTableHead from './CategoryTableHead';
|
||||||
import { CategoryTableRowPropsType } from './CategoryTableRow';
|
import { CategoryTableRowPropsType } from './CategoryTableRow';
|
||||||
|
import AddCategoryModal from '@educt/components/Modals/AddCategoryModal';
|
||||||
|
import DeleteCategoryDialog from '@educt/components/Dialogs/DeleteCategoryDialog';
|
||||||
|
import EditCategoryModal from '@educt/components/Modals/EditCategoryModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import AddCategoryModal from '@educt/components/Modals/AddCategoryModal';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
import DeleteCategoryDialog from '@educt/components/Dialogs/DeleteCategoryDialog';
|
|
||||||
|
|
||||||
type CategoryTableListPropsType = {
|
type CategoryTableListPropsType = {
|
||||||
render: React.FC<CategoryTableRowPropsType>;
|
render: React.FC<CategoryTableRowPropsType>;
|
||||||
@ -21,38 +23,56 @@ const CategoryTableList: React.FC<CategoryTableListPropsType> = props => {
|
|||||||
const { render: Row, categories } = props;
|
const { render: Row, categories } = props;
|
||||||
const [rows, setRows] = useState<ICategory[]>(categories);
|
const [rows, setRows] = useState<ICategory[]>(categories);
|
||||||
const [deleting, setDeleting] = useState<ICategory | null>(null);
|
const [deleting, setDeleting] = useState<ICategory | null>(null);
|
||||||
const { onOpen: onOpenCategoryModal, onClose: onCloseCategoryModal, isOpen: isOpenCategoryModal } = useDisclosure();
|
const [editing, setEditing] = useState<ICategory | null>(null);
|
||||||
|
const { onOpen: onOpenCreateModal, onClose: onCloseCreateyModal, isOpen: isOpenCreateModal } = useDisclosure();
|
||||||
|
const { onOpen: onOpenEditModal, onClose: onCloseEditModal, isOpen: isOpenEditModal } = useDisclosure();
|
||||||
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
|
||||||
|
|
||||||
const isEmptyRows = rows.length === 0;
|
const isEmptyRows = rows.length === 0;
|
||||||
|
|
||||||
const handleAddCategory = (category: ICategory) => setRows(prev => [...prev, category]);
|
const handleAddRow = (category: ICategory) => setRows(prev => [...prev, category]);
|
||||||
|
|
||||||
|
const handleRemoveRow = (removed: ICategory) => setRows(prev => prev.filter(category => category.id !== removed.id));
|
||||||
|
|
||||||
|
const handleEditRow = (edited: ICategory) => {
|
||||||
|
console.log(edited);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = (category: ICategory) => () => {
|
const handleDelete = (category: ICategory) => () => {
|
||||||
setDeleting(category);
|
setDeleting(category);
|
||||||
onOpenDeleteDialog();
|
onOpenDeleteDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO make edit query
|
const handleEdit = (category: ICategory) => () => {
|
||||||
const handleEdit = () => () => undefined;
|
setEditing(category);
|
||||||
|
onOpenEditModal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<AddCategoryModal
|
<AddCategoryModal
|
||||||
isOpen={isOpenCategoryModal}
|
isOpen={isOpenCreateModal}
|
||||||
onClose={onCloseCategoryModal}
|
onClose={onCloseCreateyModal}
|
||||||
onAdded={category => handleAddCategory(category)}
|
onAdded={category => handleAddRow(category)}
|
||||||
/>
|
/>
|
||||||
{deleting && (
|
{deleting && (
|
||||||
<DeleteCategoryDialog
|
<DeleteCategoryDialog
|
||||||
category={deleting}
|
category={deleting}
|
||||||
isOpen={isOpenDeleteDialog}
|
isOpen={isOpenDeleteDialog}
|
||||||
onClose={onCloseDeleteDialog}
|
onClose={onCloseDeleteDialog}
|
||||||
onDeleted={deleted => setRows(prev => prev.filter(category => category.id !== deleted.id))}
|
onDeleted={category => handleRemoveRow(category)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{editing && (
|
||||||
|
<EditCategoryModal
|
||||||
|
category={editing}
|
||||||
|
isOpen={isOpenEditModal}
|
||||||
|
onClose={onCloseEditModal}
|
||||||
|
onEdited={category => handleEditRow(category)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Flex justifyContent={'flex-end'}>
|
<Flex justifyContent={'flex-end'}>
|
||||||
<CreateButton onClick={onOpenCategoryModal} />
|
<CreateButton onClick={onOpenCreateModal} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Table borderRadius='lg' mt='2'>
|
<Table borderRadius='lg' mt='2'>
|
||||||
<CategoryTableHead />
|
<CategoryTableHead />
|
||||||
|
|||||||
41
src/hooks/queries/category/useUpdateCategory.ts
Normal file
41
src/hooks/queries/category/useUpdateCategory.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { ICategory } from '@educt/interfaces';
|
||||||
|
import { CategoryServiceInstance } from '@educt/services';
|
||||||
|
import { UpdateCategoryParamsType } from '@educt/types';
|
||||||
|
import useAsync from '@educt/hooks/useAsync';
|
||||||
|
import { useToast } from '@chakra-ui/react';
|
||||||
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
|
|
||||||
|
const useUpdateCategory = () => {
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
const update = async (id: string, data: UpdateCategoryParamsType) => {
|
||||||
|
try {
|
||||||
|
const result = await CategoryServiceInstance.update(id, data);
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
default:
|
||||||
|
toast({ title: `${error.message}`, status: 'error' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { execute: updateCategory, ...state } = useAsync<ICategory, Parameters<typeof update>>(update);
|
||||||
|
|
||||||
|
return { updateCategory, ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useUpdateCategory };
|
||||||
@ -35,6 +35,7 @@ import { useUpdateUserEmail } from './user/useUpdateUserEmail';
|
|||||||
import { useFetchCategories } from './category/useFetchCategories';
|
import { useFetchCategories } from './category/useFetchCategories';
|
||||||
import { useCreateCategory } from './category/useCreateCategory';
|
import { useCreateCategory } from './category/useCreateCategory';
|
||||||
import { useDeleteCategory } from './category/useDeleteCategory';
|
import { useDeleteCategory } from './category/useDeleteCategory';
|
||||||
|
import { useUpdateCategory } from './category/useUpdateCategory';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useLogin,
|
useLogin,
|
||||||
@ -54,5 +55,6 @@ export {
|
|||||||
useUpdateUserEmail,
|
useUpdateUserEmail,
|
||||||
useCreateCategory,
|
useCreateCategory,
|
||||||
useDeleteCategory,
|
useDeleteCategory,
|
||||||
|
useUpdateCategory,
|
||||||
useFetchCategories,
|
useFetchCategories,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
import { IApiRespose, ICategory } from '@educt/interfaces';
|
import { IApiRespose, ICategory } from '@educt/interfaces';
|
||||||
import { ApiServiceInstance } from '.';
|
import { ApiServiceInstance } from '.';
|
||||||
import { CreateCategoryParamsType } from '@educt/types';
|
import { CreateCategoryParamsType, UpdateCategoryParamsType } from '@educt/types';
|
||||||
|
|
||||||
class CategoryService {
|
class CategoryService {
|
||||||
public api: AxiosInstance;
|
public api: AxiosInstance;
|
||||||
@ -40,6 +40,18 @@ class CategoryService {
|
|||||||
const result = await this.api.delete(`/v1/categories/${id}`);
|
const result = await this.api.delete(`/v1/categories/${id}`);
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update category by id
|
||||||
|
*
|
||||||
|
* @param id Category id
|
||||||
|
* @param data Field to update
|
||||||
|
* @returns Updated category
|
||||||
|
*/
|
||||||
|
public async update(id: string, data: UpdateCategoryParamsType): Promise<IApiRespose<ICategory>> {
|
||||||
|
const reuslt = await this.api.patch(`/v1/categories/${id}`, data);
|
||||||
|
return reuslt.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryServiceInstance = new CategoryService(ApiServiceInstance.api);
|
export const CategoryServiceInstance = new CategoryService(ApiServiceInstance.api);
|
||||||
|
|||||||
5
src/types/index.d.ts
vendored
5
src/types/index.d.ts
vendored
@ -183,3 +183,8 @@ export type CreateCategoryParamsType = {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateCategoryParamsType = {
|
||||||
|
title?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user