From 5c0599fe0ac936bbbb341bfb73275840b47f25e9 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Fri, 10 Dec 2021 23:04:58 +0300 Subject: [PATCH] feat: attach student list query --- src/components/Modal/BaseModal.tsx | 15 +- .../AddStudentsModal/AddSudentsModal.tsx | 153 ++++-------------- .../SelectUsersInput/SelectUsersInput.tsx | 143 ++++++++++++++++ src/components/SelectUsersInput/index.ts | 3 + src/helpers/index.ts | 9 +- .../StudentTableList/StudentTableList.tsx | 13 +- src/pages/courses/edit/edit.tsx | 4 +- src/services/CourseService.ts | 8 + 8 files changed, 219 insertions(+), 129 deletions(-) create mode 100644 src/components/SelectUsersInput/SelectUsersInput.tsx create mode 100644 src/components/SelectUsersInput/index.ts diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 8dd2701..e56764b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -17,14 +17,18 @@ import { IconType } from 'react-icons'; export interface IBaseModalProps extends ModalProps { isOpen: boolean; onClose: () => void; + onProceed: (e: React.MouseEvent) => void; heading: string; icon?: IconType; description?: string; confirmText?: string; + isLoading?: boolean | undefined; + loadingText?: string | undefined; } const BaseModal: React.FC = props => { - const { heading, description, isOpen, onClose, confirmText, icon, ...modalProps } = props; + const { heading, description, isOpen, onClose, onProceed, confirmText, icon, isLoading, loadingText, ...modalProps } = + props; return ( @@ -54,7 +58,14 @@ const BaseModal: React.FC = props => { - diff --git a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx index e357a24..770f14e 100644 --- a/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx +++ b/src/components/Modals/AddStudentsModal/AddSudentsModal.tsx @@ -1,136 +1,44 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useState } from 'react'; -import { - Tag, - TagLabel, - TagCloseButton, - Input, - Box, - Flex, - IconButton, - BoxProps, - InputGroup, - Text, - Stack, - InputRightElement, - Avatar, -} from '@chakra-ui/react'; import BaseModal from '@educt/components/Modal'; import { MdGroup } from 'react-icons/md'; -import { AddIcon, SearchIcon } from '@chakra-ui/icons'; -import { UserServiceInstance } from '@educt/services'; +import { UserRoleEnum } from '@educt/enums'; +import SelectUsersInput from '@educt/components/SelectUsersInput'; import { IUser } from '@educt/interfaces'; -import { useErrorHandler } from 'react-error-boundary'; -import { BeatLoader } from 'react-spinners'; -import { Virtuoso } from 'react-virtuoso'; -import { FetchUsersParamsType } from '@educt/types'; +import { CourseServiceInstance } from '@educt/services'; +import { useToast } from '@chakra-ui/react'; +import useIsMountedRef from '@educt/hooks/useIsMountedRef'; type AddStudentsModalPropsType = { isOpen: boolean; onClose: () => void; + courseId: string; }; -const SelectUsersInput: React.FC = props => { +const AddStudentsModal: React.FC = ({ isOpen, onClose, courseId }) => { const [isLoading, setIsLoading] = useState(false); - const [search, setSearch] = useState(''); - const [selected, setSelected] = useState>([]); - const [users, setUsers] = useState>([]); - const [limit, setLimit] = useState(5); - const filtered = users.filter(u => selected.every(s => s.id !== u.id)); - const handleError = useErrorHandler(); - const isEmpty = filtered.length === 0; + const isMountedRef = useIsMountedRef(); + const [students, setStudents] = useState>([]); + const toast = useToast(); - const fetch = async (params?: FetchUsersParamsType | undefined) => { - try { - setIsLoading(true); - const { data } = await UserServiceInstance.fetchAll({ search, limit }); - setUsers(data); - } catch (error: any) { - handleError(error); - } finally { - setIsLoading(false); + const handleSelectStudents = (students: Array) => setStudents(students); + const handleAttachStudents = async () => { + if (students.length !== 0) { + try { + setIsLoading(true); + const ids = students.map(s => s.id); + await CourseServiceInstance.attachStudentsList(courseId, ids); + + toast({ title: 'Students successfully attached.', status: 'success' }); + onClose(); + } catch (error: any) { + throw error; + } finally { + if (isMountedRef.current) setIsLoading(false); + } } }; - const handleSearch = (e: React.ChangeEvent) => setSearch(e.target.value); - const handleSelect = (user: IUser) => () => setSelected(prev => [...prev, user]); - const handleRemoveSelected = (id: IUser['id']) => () => setSelected(prev => prev.filter(u => u.id !== id)); - - useEffect(() => { - fetch(); - }, [search]); - - return ( - - {selected.length !== 0 && ( - - {selected.map(selected => ( - - - {selected.fullname} - - - ))} - - )} - - fetch()} - onChange={handleSearch} - type='text' - placeholder='Search for users...' - /> - } /> - - - {isLoading && ( - - Loading...   - - - )} - - {!isLoading && isEmpty && ( - - Cannot find any user - - )} - - {!isLoading && !isEmpty && ( - - ( - - - - - {user.fullname} - - - {user.email} - - - } - ml='auto' - borderRadius='full' - size='sm' - /> - - )} - /> - - )} - - ); -}; - -const AddStudentsModal: React.FC = ({ isOpen, onClose }) => { return ( = ({ isOpen, onClose confirmText='Add students' isOpen={isOpen} onClose={onClose} + onProceed={handleAttachStudents} + isLoading={isLoading} + loadingText='Saving...' > - + handleSelectStudents(selected)} + placeholder='Search for students...' + searchParams={{ role: UserRoleEnum.STUDENT }} + /> ); }; diff --git a/src/components/SelectUsersInput/SelectUsersInput.tsx b/src/components/SelectUsersInput/SelectUsersInput.tsx new file mode 100644 index 0000000..12f66d2 --- /dev/null +++ b/src/components/SelectUsersInput/SelectUsersInput.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from 'react'; + +import { + Tag, + TagLabel, + TagCloseButton, + Input, + Box, + Flex, + IconButton, + InputGroup, + Text, + Stack, + InputRightElement, + Avatar, +} from '@chakra-ui/react'; +import { AddIcon, SearchIcon } from '@chakra-ui/icons'; +import { UserServiceInstance } from '@educt/services'; +import { IUser } from '@educt/interfaces'; +import { useErrorHandler } from 'react-error-boundary'; +import { BeatLoader } from 'react-spinners'; +import { Virtuoso } from 'react-virtuoso'; +import { FetchUsersParamsType } from '@educt/types'; + +type SelectUsersInputPropsType = { + placeholder?: string | undefined; + searchParams?: FetchUsersParamsType | undefined; + onSelect: (selected: Array) => void; +}; + +const SelectUsersInput: React.FC = ({ placeholder, searchParams: params, onSelect }) => { + const [isLoading, setIsLoading] = useState(false); + const [search, setSearch] = useState(''); + const [selected, setSelected] = useState>([]); + const [users, setUsers] = useState>([]); + const filtered = users.filter(u => selected.every(s => s.id !== u.id)); + const handleError = useErrorHandler(); + const isEmpty = filtered.length === 0; + + const fetch = async () => { + try { + setIsLoading(true); + const { data } = await UserServiceInstance.fetchAll({ search, ...params }); + setUsers(data); + } catch (error: any) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + + const handleSearch = (e: React.ChangeEvent) => setSearch(e.target.value); + + const handleSelect = (user: IUser) => () => { + setSelected(prev => { + const updated = [...prev, user]; + onSelect(updated); + return updated; + }); + }; + const handleRemoveSelected = (id: IUser['id']) => () => { + setSelected(prev => { + const updated = prev.filter(u => u.id !== id); + onSelect(updated); + return updated; + }); + }; + + useEffect(() => { + fetch(); + }, [search]); + + return ( + + {selected.length !== 0 && ( + + {selected.map(selected => ( + + + {selected.fullname} + + + ))} + + )} + + + } /> + + + {isLoading && ( + + Loading...   + + + )} + + {!isLoading && isEmpty && ( + + Cannot find any user + + )} + + {!isLoading && !isEmpty && ( + + ( + + + + + {user.fullname} + + + {user.email} + + + } + ml='auto' + borderRadius='full' + size='sm' + /> + + )} + /> + + )} + + ); +}; + +export default SelectUsersInput; diff --git a/src/components/SelectUsersInput/index.ts b/src/components/SelectUsersInput/index.ts new file mode 100644 index 0000000..26a35e0 --- /dev/null +++ b/src/components/SelectUsersInput/index.ts @@ -0,0 +1,3 @@ +import SelectUsersInput from './SelectUsersInput'; + +export default SelectUsersInput; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index f505833..641f21f 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -46,7 +46,14 @@ export function transformToFormData(data: object): FormData { /** * Append value to form-data format */ - Object.entries(data).forEach(([key, value]) => value && formData.append(key, value)); + Object.entries(data).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v, i) => formData.append(`${key}[${i}]`, v)); + return; + } + + formData.append(key, value); + }); return formData; } diff --git a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx index 562b08f..b6faf54 100644 --- a/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx +++ b/src/pages/courses/edit/components/StudentTableList/StudentTableList.tsx @@ -21,10 +21,13 @@ import { useDisclosure } from '@chakra-ui/react'; type StudentTableListPropsType = { render: React.FC; - students: ICourse['students']; + course: Omit; }; -const StudentTableList: React.FC = ({ render: Row, students }) => { +const StudentTableList: React.FC = props => { + const { render: Row, course } = props; + const { students } = course; + const [rows, setRows] = useState(students); const [selected, setSelected] = useState([]); const [search, setSearch] = useState(''); @@ -73,7 +76,7 @@ const StudentTableList: React.FC = ({ render: Row, st if (rows.length === 0) { return ( <> - + There are no students in the course yet