refactor: removed edit course page store

This commit is contained in:
Sergey Yarkov 2021-12-14 01:00:05 +03:00
parent 5d70c629ea
commit 21779419c9
10 changed files with 101 additions and 180 deletions

View File

@ -19,15 +19,17 @@ type DeleteLessonDialogPropsType = {
lesson: Pick<ILesson, 'id' | 'title'>;
isOpen: boolean;
onClose: () => void;
onConfirmed?: (id: string) => void | undefined;
};
const DeleteLessonDialog: React.FC<DeleteLessonDialogPropsType> = ({ lesson, isOpen, onClose }) => {
const DeleteLessonDialog: React.FC<DeleteLessonDialogPropsType> = ({ lesson, isOpen, onConfirmed, onClose }) => {
const { deleteLesson, isLoading } = useDeleteLesson();
const handleDelete = async (isConfirmed: boolean) => {
if (isConfirmed) {
try {
await deleteLesson(lesson.id);
onConfirmed && onConfirmed(lesson.id);
} catch (error) {
console.error(error);
}

View File

@ -22,14 +22,14 @@ import { BeatLoader } from 'react-spinners';
import { Virtuoso } from 'react-virtuoso';
import { FetchUsersParamsType } from '@educt/types';
type SelectUsersInputPropsType = {
type SelectStudentsInputPropsType = {
placeholder?: string | undefined;
searchParams?: FetchUsersParamsType | undefined;
onSelect: (selected: Array<IUser>) => void;
currentStudents: ICourse['students'];
};
const SelectUsersInput: React.FC<SelectUsersInputPropsType> = ({
const SelectStudentsInput: React.FC<SelectStudentsInputPropsType> = ({
placeholder,
searchParams: params,
onSelect,
@ -151,4 +151,4 @@ const SelectUsersInput: React.FC<SelectUsersInputPropsType> = ({
);
};
export default SelectUsersInput;
export default SelectStudentsInput;

View File

@ -2,24 +2,20 @@
* Types
*/
import { ICourse } from '@educt/interfaces';
import { CourseServiceInstance } from '@educt/services';
/**
* Hooks
*/
import { useEffect } from 'react';
import useAsync from '../../useAsync';
import { useRootStore } from '../../useRootStore';
type CourseResultDataType = Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
const useFetchCourse = (id: string) => {
const {
pageStore: { editCourseStore },
} = useRootStore();
const fetch = async (id: string) => {
const result = await editCourseStore.loadCourseById(id);
return result.data;
const { data } = await CourseServiceInstance.fetchById(id);
return data;
};
const { execute: fetchCourseById, ...state } = useAsync<CourseResultDataType, Parameters<typeof fetch>>(fetch);

View File

@ -1,22 +1,19 @@
import { useToast } from '@chakra-ui/toast';
import { ILesson } from '@educt/interfaces';
import { LessonServiceInstance } from '@educt/services';
import { useErrorHandler } from 'react-error-boundary';
import useAsync from '../../useAsync';
import { useRootStore } from '../../useRootStore';
type CreatedLessonDataType = ILesson;
const useDeleteLesson = () => {
const {
pageStore: { editCourseStore },
} = useRootStore();
const toast = useToast();
const handleError = useErrorHandler();
const deleteLesson = async (id: string) => {
try {
const result = await editCourseStore.deleteLessonById(id);
toast({ title: 'Lesson deleted', status: 'info' });
const result = await LessonServiceInstance.deleteLesson(id);
toast({ title: 'Lesson deleted.', status: 'info' });
return result.data;
} catch (error: any) {
if (error.response) {

View File

@ -1,10 +1,11 @@
import React, { useState } from 'react';
import { observer } from 'mobx-react';
import React from 'react';
import { ItemProps, Virtuoso } from 'react-virtuoso';
import { DragDropContext, Draggable, DraggableProvided, Droppable, DropResult } from 'react-beautiful-dnd';
import moment from 'moment';
import * as helpers from '@educt/helpers';
import { DragDropContext, Draggable, DraggableProvided, Droppable, DropResult } from 'react-beautiful-dnd';
import { Flex, Box, Text, IconButton, Icon, Button } from '@chakra-ui/react';
import { DeleteIcon, DragHandleIcon } from '@chakra-ui/icons';
import { MdTimer, MdAttachment } from 'react-icons/md';
/**
* Types
@ -19,12 +20,11 @@ import DeleteLessonDialog from '@educt/components/Dialogs/DeleteLessonDialog';
/**
* Hooks
*/
import { useState } from 'react';
import { useHistory } from 'react-router';
import { useColorMode } from '@chakra-ui/react';
import { MdTimer, MdAttachment } from 'react-icons/md';
import { useRootStore } from '@educt/hooks/useRootStore';
import useDidMountEffect from '@educt/hooks/useDidMountEffect';
import { useDisclosure } from '@chakra-ui/hooks';
import useDidMountEffect from '@educt/hooks/useDidMountEffect';
/**
* Contexts
@ -55,19 +55,16 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => {
const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
const history = useHistory();
const {
pageStore: { editCourseStore },
} = useRootStore();
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
const { onOpen: onOpenDelDialog, onClose: onCloseDelDialog, isOpen: isOpenDelDialog } = useDisclosure();
const [lessons, setLessons] = useState<Array<ILesson>>(course.lessons);
const [deleting, setDeleting] = useState<Pick<ILesson, 'id' | 'title'> | null>(null);
const handleError = useErrorHandler();
const lessons = course.lessons;
const handleCreateLesson = (): void => history.push('/lessons/create');
const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`);
const handleDeleteLesson = (lesson: ILesson): void => {
setDeleting({ id: lesson.id, title: lesson.title });
onOpenDeleteDialog();
onOpenDelDialog();
};
const handleChangeOrder = async (ids: string[]) => {
@ -84,9 +81,11 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
return;
}
editCourseStore.reorderLessons(result.source.index, result.destination.index);
setLessons(helpers.arrayMove(lessons, result.source.index, result.destination.index));
};
const onDeleted = (id: string) => setLessons(prev => prev.filter(l => l.id !== id));
const LessonItem = React.useMemo(() => {
return ({
provided,
@ -199,7 +198,12 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
{lessons.length !== 0 ? (
<>
{deleting && (
<DeleteLessonDialog lesson={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />
<DeleteLessonDialog
lesson={deleting}
isOpen={isOpenDelDialog}
onClose={onCloseDelDialog}
onConfirmed={onDeleted}
/>
)}
<Flex mt='2' mb='3' padding='0 20px' alignItems='center' justifyContent='space-between'>
<Text fontSize='sm' color='gray.500'>
@ -255,4 +259,4 @@ const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
);
};
export default observer(LessonList);
export default LessonList;

View File

@ -1,17 +1,16 @@
import React from 'react';
import { Table, Tbody, Button, Input, InputGroup, InputLeftElement, Flex, Box, Text } from '@chakra-ui/react';
import { Table, Tbody, Input, InputGroup, InputLeftElement, Flex, Box, Text } from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md';
import AddStudentsModal from '@educt/components/Modals/AddStudentsModal';
import { AddButton } from '@educt/components/Buttons';
import BulkActionsMenu from './BulkActionsMenu';
import StudentTableHead from './StudentTableHead';
import { AddIcon } from '@chakra-ui/icons';
/**
* Types
*/
import type { StudentTableRowPropsType } from './StudentTableRow';
import type { ICourse } from '@educt/interfaces';
import type { ICourse, IUser } from '@educt/interfaces';
/**
* Hooks
@ -28,9 +27,8 @@ type StudentTableListPropsType = {
const StudentTableList: React.FC<StudentTableListPropsType> = props => {
const { render: Row, course } = props;
const { students } = course;
const [rows, setRows] = useState<ICourse['students']>(students);
const [rows, setRows] = useState<ICourse['students']>(props.course.students);
const [selected, setSelected] = useState<ICourse['students']>([]);
const [search, setSearch] = useState<string>('');
const {
@ -50,20 +48,21 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
*/
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setRows([
...students.filter(
setRows(
props.course.students.filter(
s => s.fullname.toLowerCase().includes(e.target.value) || s.email.toLowerCase().includes(e.target.value)
),
]);
)
);
};
/**
* Delete student from state after removing
*/
const onRemoved = (removedStudents: ICourse['students']) => {
setSelected(prev => prev.filter(s => removedStudents.every(r => s.id !== r.id)));
};
const onAdded = (users: IUser[]) => {
setRows(prev => [...prev, ...users]);
};
/**
* Select all students in a table
*/
@ -133,7 +132,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
onClose={onCloseAddStudentModal}
course={course}
currentStudents={rows}
onAdded={users => setRows(prev => [...prev, ...users])}
onAdded={onAdded}
/>
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
<Flex mb='2' alignItems='center'>
@ -164,7 +163,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
<AddButton onClick={onOpenAddStudentModal} />
</Flex>
</Flex>
<Table overflow='hidden' borderRadius='lg' mt='2'>
<Table borderRadius='lg' mt='2'>
<StudentTableHead onSelectAll={handleSelectAll} />
<Tbody>
{rows.map(student => (
@ -184,7 +183,9 @@ const StudentTableList: React.FC<StudentTableListPropsType> = props => {
</Table>
{isEmptyRows && (
<Box textAlign='center' m='6'>
<Text color='gray.500'>Cannot find any user.</Text>
<Text color='gray.500' userSelect='none'>
Cannot find any user.
</Text>
</Box>
)}
<Text mt='4' fontSize='sm' color='gray.500'>

View File

@ -21,7 +21,6 @@ import LessonList from './components/LessonList';
* Hooks
*/
import { useParams } from 'react-router-dom';
import { useRootStore } from '@educt/hooks/useRootStore';
import { useFetchCourse } from '@educt/hooks/queries';
import { useDisclosure } from '@chakra-ui/hooks';
import { PageContent, PageHeading, PageWrapper } from '@educt/components/PageElements';
@ -33,12 +32,7 @@ import { StatusButton } from './components';
*/
const EditCoursePage: React.FC<IPageProps> = () => {
const params = useParams<{ id: string }>();
const {
pageStore: {
editCourseStore: { course },
},
} = useRootStore();
const { error, isLoading } = useFetchCourse(params.id);
const { error, data: course, isLoading } = useFetchCourse(params.id);
const { onOpen: onOpenDeleteDialog, onClose: onCloseDeleteDialog, isOpen: isOpenDeleteDialog } = useDisclosure();
/**
@ -68,13 +62,12 @@ const EditCoursePage: React.FC<IPageProps> = () => {
</PageHeading>
<PageContent>
<Box>
<Tabs isLazy mt='8'>
<Tabs mt='8'>
<TabList>
<Tab>Info</Tab>
<Tab>Lessons</Tab>
<Tab>Students</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Box>

View File

@ -76,8 +76,7 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item, limit }) => {
<Box>
{editing && <EditUserForm user={editing} isOpen={isOpenEditModal} onClose={onCloseEditModal} />}
{deleting && <DeleteUserDialog user={deleting} isOpen={isOpenDeleteDialog} onClose={onCloseDeleteDialog} />}
{users.length !== 0 ? (
<>
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
<UserSearch />
<CreateUserForm />
@ -102,6 +101,11 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item, limit }) => {
<LoadingList />
)}
</Box>
{users.length === 0 && (
<Box textAlign='center' userSelect='none'>
<Text color='gray.500'>Cannot find any users.</Text>
</Box>
)}
<Flex margin='2rem 0' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
<Text fontSize='sm'>
@ -127,12 +131,6 @@ const UserList: React.FC<UserListPropsType> = ({ render: Item, limit }) => {
</Button>
</Flex>
</Flex>
</>
) : (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>Cannot find any users.</Text>
</Box>
)}
</Box>
);
};

View File

@ -1,6 +1,5 @@
import { makeAutoObservable } from 'mobx';
import CoursesStore from './pages/CoursesStore';
import EditCourseStore from './pages/EditCourseStore';
import RootStore from './RootStore';
export default class PageStore {
@ -8,8 +7,6 @@ export default class PageStore {
public coursesStore: CoursesStore;
public editCourseStore: EditCourseStore;
constructor(root: RootStore) {
this.root = root;
@ -17,7 +14,6 @@ export default class PageStore {
* Pages
*/
this.coursesStore = new CoursesStore(this);
this.editCourseStore = new EditCourseStore(this);
makeAutoObservable(this);
}

View File

@ -1,66 +0,0 @@
import { makeAutoObservable, runInAction } from 'mobx';
import * as helpers from '@educt/helpers';
import { ICourse } from '@educt/interfaces';
import PageStore from '../PageStore';
import { CourseServiceInstance, LessonServiceInstance } from '@educt/services';
export default class EditCourseStore {
public pageStore: PageStore;
public course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'> | null = null;
constructor(pageStore: PageStore) {
this.pageStore = pageStore;
makeAutoObservable(this);
}
/**
* Load course by id into store
*
* @param id Course id
* @returns Course response
*/
public async loadCourseById(id: string) {
const result = await CourseServiceInstance.fetchById(id);
runInAction(() => {
this.course = result.data;
});
return result;
}
// TODO update stor on detaching, attaching, deleting students
/**
* Remove lesson from store
*
* @param id Lesson id
* @returns Deleted lesson response
*/
public async deleteLessonById(id: string) {
const result = await LessonServiceInstance.deleteLesson(id);
runInAction(() => {
if (this.course) {
this.course.lessons = this.course.lessons.filter(lesson => lesson.id !== result.data.id);
}
});
return result;
}
/**
* Reorder lessons in store list on drag
*
* @param oldIndex
* @param newIndex
*/
public reorderLessons(oldIndex: number, newIndex: number): void {
if (this.course) {
this.course.lessons = helpers.arrayMove(this.course.lessons, oldIndex, newIndex);
}
}
}