mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: attach student list query
This commit is contained in:
parent
7303dd5b43
commit
5c0599fe0a
@ -17,14 +17,18 @@ import { IconType } from 'react-icons';
|
|||||||
export interface IBaseModalProps extends ModalProps {
|
export interface IBaseModalProps extends ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onProceed: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
heading: string;
|
heading: string;
|
||||||
icon?: IconType;
|
icon?: IconType;
|
||||||
description?: string;
|
description?: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
|
isLoading?: boolean | undefined;
|
||||||
|
loadingText?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseModal: React.FC<IBaseModalProps> = props => {
|
const BaseModal: React.FC<IBaseModalProps> = props => {
|
||||||
const { heading, description, isOpen, onClose, confirmText, icon, ...modalProps } = props;
|
const { heading, description, isOpen, onClose, onProceed, confirmText, icon, isLoading, loadingText, ...modalProps } =
|
||||||
|
props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} {...modalProps}>
|
<Modal isOpen={isOpen} onClose={onClose} {...modalProps}>
|
||||||
@ -54,7 +58,14 @@ const BaseModal: React.FC<IBaseModalProps> = props => {
|
|||||||
<Button onClick={onClose} borderRadius='md' mr='2'>
|
<Button onClick={onClose} borderRadius='md' mr='2'>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose} borderRadius='md' variant='outline' colorScheme='blue'>
|
<Button
|
||||||
|
onClick={onProceed}
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingText={loadingText || 'Loading...'}
|
||||||
|
borderRadius='md'
|
||||||
|
variant='outline'
|
||||||
|
colorScheme='blue'
|
||||||
|
>
|
||||||
{confirmText || 'Proceed'}
|
{confirmText || 'Proceed'}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@ -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 BaseModal from '@educt/components/Modal';
|
||||||
import { MdGroup } from 'react-icons/md';
|
import { MdGroup } from 'react-icons/md';
|
||||||
import { AddIcon, SearchIcon } from '@chakra-ui/icons';
|
import { UserRoleEnum } from '@educt/enums';
|
||||||
import { UserServiceInstance } from '@educt/services';
|
import SelectUsersInput from '@educt/components/SelectUsersInput';
|
||||||
import { IUser } from '@educt/interfaces';
|
import { IUser } from '@educt/interfaces';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { CourseServiceInstance } from '@educt/services';
|
||||||
import { BeatLoader } from 'react-spinners';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import useIsMountedRef from '@educt/hooks/useIsMountedRef';
|
||||||
import { FetchUsersParamsType } from '@educt/types';
|
|
||||||
|
|
||||||
type AddStudentsModalPropsType = {
|
type AddStudentsModalPropsType = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
courseId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectUsersInput: React.FC<BoxProps> = props => {
|
const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose, courseId }) => {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [search, setSearch] = useState<string>('');
|
const isMountedRef = useIsMountedRef();
|
||||||
const [selected, setSelected] = useState<Array<IUser>>([]);
|
const [students, setStudents] = useState<Array<IUser>>([]);
|
||||||
const [users, setUsers] = useState<Array<IUser>>([]);
|
const toast = useToast();
|
||||||
const [limit, setLimit] = useState<number>(5);
|
|
||||||
const filtered = users.filter(u => selected.every(s => s.id !== u.id));
|
|
||||||
const handleError = useErrorHandler();
|
|
||||||
const isEmpty = filtered.length === 0;
|
|
||||||
|
|
||||||
const fetch = async (params?: FetchUsersParamsType | undefined) => {
|
const handleSelectStudents = (students: Array<IUser>) => setStudents(students);
|
||||||
|
const handleAttachStudents = async () => {
|
||||||
|
if (students.length !== 0) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const { data } = await UserServiceInstance.fetchAll({ search, limit });
|
const ids = students.map(s => s.id);
|
||||||
setUsers(data);
|
await CourseServiceInstance.attachStudentsList(courseId, ids);
|
||||||
|
|
||||||
|
toast({ title: 'Students successfully attached.', status: 'success' });
|
||||||
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
handleError(error);
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (isMountedRef.current) setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => 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 (
|
|
||||||
<Box {...props}>
|
|
||||||
{selected.length !== 0 && (
|
|
||||||
<Box mt='1'>
|
|
||||||
{selected.map(selected => (
|
|
||||||
<Tag key={selected.id} size='md' colorScheme='blue' borderRadius='full' mr='1' mb='1'>
|
|
||||||
<Avatar size='xs' name={selected.fullname} ml={-1} mr={2} />
|
|
||||||
<TagLabel>{selected.fullname}</TagLabel>
|
|
||||||
<TagCloseButton onClick={handleRemoveSelected(selected.id)} />
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<InputGroup mt='2'>
|
|
||||||
<Input
|
|
||||||
value={search}
|
|
||||||
onFocus={() => fetch()}
|
|
||||||
onChange={handleSearch}
|
|
||||||
type='text'
|
|
||||||
placeholder='Search for users...'
|
|
||||||
/>
|
|
||||||
<InputRightElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<Text as='small' color='gray.500'>
|
|
||||||
Loading...
|
|
||||||
<BeatLoader size={6} color='gray' />
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && isEmpty && (
|
|
||||||
<Text as='small' color='red.500'>
|
|
||||||
Cannot find any user
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && !isEmpty && (
|
|
||||||
<Stack borderWidth='1px' mt='2' borderRadius='md'>
|
|
||||||
<Virtuoso
|
|
||||||
style={{ height: filtered.length * 54, maxHeight: 216 }}
|
|
||||||
data={filtered}
|
|
||||||
itemContent={(index, user) => (
|
|
||||||
<Flex key={user.id} borderBottomWidth='1px' p='7px 16px' alignItems='center'>
|
|
||||||
<Avatar name={user.fullname} size='xs' mr={{ base: '0', sm: '3' }} mb={{ base: '1', sm: '0' }} />
|
|
||||||
<Box textAlign={{ base: 'center', sm: 'left' }}>
|
|
||||||
<Text fontWeight='medium' fontSize='sm'>
|
|
||||||
{user.fullname}
|
|
||||||
</Text>
|
|
||||||
<Text color='gray.500' fontSize='xs' overflowWrap='anywhere'>
|
|
||||||
{user.email}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleSelect(user)}
|
|
||||||
aria-label='Add student'
|
|
||||||
icon={<AddIcon />}
|
|
||||||
ml='auto'
|
|
||||||
borderRadius='full'
|
|
||||||
size='sm'
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose }) => {
|
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
icon={MdGroup}
|
icon={MdGroup}
|
||||||
@ -139,8 +47,15 @@ const AddStudentsModal: React.FC<AddStudentsModalPropsType> = ({ isOpen, onClose
|
|||||||
confirmText='Add students'
|
confirmText='Add students'
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
onProceed={handleAttachStudents}
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingText='Saving...'
|
||||||
>
|
>
|
||||||
<SelectUsersInput />
|
<SelectUsersInput
|
||||||
|
onSelect={selected => handleSelectStudents(selected)}
|
||||||
|
placeholder='Search for students...'
|
||||||
|
searchParams={{ role: UserRoleEnum.STUDENT }}
|
||||||
|
/>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
143
src/components/SelectUsersInput/SelectUsersInput.tsx
Normal file
143
src/components/SelectUsersInput/SelectUsersInput.tsx
Normal file
@ -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<IUser>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectUsersInput: React.FC<SelectUsersInputPropsType> = ({ placeholder, searchParams: params, onSelect }) => {
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [search, setSearch] = useState<string>('');
|
||||||
|
const [selected, setSelected] = useState<Array<IUser>>([]);
|
||||||
|
const [users, setUsers] = useState<Array<IUser>>([]);
|
||||||
|
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<HTMLInputElement>) => 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 (
|
||||||
|
<Box>
|
||||||
|
{selected.length !== 0 && (
|
||||||
|
<Box mt='1'>
|
||||||
|
{selected.map(selected => (
|
||||||
|
<Tag key={selected.id} size='md' colorScheme='blue' borderRadius='full' mr='1' mb='1'>
|
||||||
|
<Avatar size='xs' name={selected.fullname} ml={-1} mr={2} />
|
||||||
|
<TagLabel>{selected.fullname}</TagLabel>
|
||||||
|
<TagCloseButton onClick={handleRemoveSelected(selected.id)} />
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<InputGroup mt='2'>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onFocus={fetch}
|
||||||
|
onChange={handleSearch}
|
||||||
|
type='text'
|
||||||
|
placeholder={placeholder || 'Search for users...'}
|
||||||
|
/>
|
||||||
|
<InputRightElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<Text as='small' color='gray.500'>
|
||||||
|
Loading...
|
||||||
|
<BeatLoader size={6} color='gray' />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && isEmpty && (
|
||||||
|
<Text as='small' color='red.500'>
|
||||||
|
Cannot find any user
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !isEmpty && (
|
||||||
|
<Stack borderWidth='1px' mt='2' borderRadius='md'>
|
||||||
|
<Virtuoso
|
||||||
|
style={{ height: filtered.length * 54, maxHeight: 216 }}
|
||||||
|
data={filtered}
|
||||||
|
itemContent={(i, user) => (
|
||||||
|
<Flex key={user.id} borderBottomWidth='1px' p='7px 16px' alignItems='center'>
|
||||||
|
<Avatar name={user.fullname} size='xs' mr={{ base: '0', sm: '3' }} mb={{ base: '1', sm: '0' }} />
|
||||||
|
<Box textAlign={{ base: 'center', sm: 'left' }}>
|
||||||
|
<Text fontWeight='medium' fontSize='sm'>
|
||||||
|
{user.fullname}
|
||||||
|
</Text>
|
||||||
|
<Text color='gray.500' fontSize='xs' overflowWrap='anywhere'>
|
||||||
|
{user.email}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleSelect(user)}
|
||||||
|
aria-label='Add student'
|
||||||
|
icon={<AddIcon />}
|
||||||
|
ml='auto'
|
||||||
|
borderRadius='full'
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectUsersInput;
|
||||||
3
src/components/SelectUsersInput/index.ts
Normal file
3
src/components/SelectUsersInput/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SelectUsersInput from './SelectUsersInput';
|
||||||
|
|
||||||
|
export default SelectUsersInput;
|
||||||
@ -46,7 +46,14 @@ export function transformToFormData(data: object): FormData {
|
|||||||
/**
|
/**
|
||||||
* Append value to form-data format
|
* 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;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,13 @@ import { useDisclosure } from '@chakra-ui/react';
|
|||||||
|
|
||||||
type StudentTableListPropsType = {
|
type StudentTableListPropsType = {
|
||||||
render: React.FC<StudentTableRowPropsType>;
|
render: React.FC<StudentTableRowPropsType>;
|
||||||
students: ICourse['students'];
|
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, students }) => {
|
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']>(students);
|
||||||
const [selected, setSelected] = useState<ICourse['students']>([]);
|
const [selected, setSelected] = useState<ICourse['students']>([]);
|
||||||
const [search, setSearch] = useState<string>('');
|
const [search, setSearch] = useState<string>('');
|
||||||
@ -73,7 +76,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
|
|||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} />
|
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} courseId={course.id} />
|
||||||
<Box textAlign='center' mt='10'>
|
<Box textAlign='center' mt='10'>
|
||||||
<Text>There are no students in the course yet</Text>
|
<Text>There are no students in the course yet</Text>
|
||||||
<Button mt='2' onClick={onOpenAddStudentModal} size='sm' colorScheme='blue' variant='outline'>
|
<Button mt='2' onClick={onOpenAddStudentModal} size='sm' colorScheme='blue' variant='outline'>
|
||||||
@ -86,7 +89,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} />
|
<AddStudentsModal isOpen={isOpenAddStudentModal} onClose={onCloseAddStudentModal} courseId={course.id} />
|
||||||
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
|
<Flex justifyContent='space-between' flexDir={{ base: 'column', lg: 'row' }}>
|
||||||
<Flex mb='2'>
|
<Flex mb='2'>
|
||||||
<InputGroup mr='2' borderRadius='lg'>
|
<InputGroup mr='2' borderRadius='lg'>
|
||||||
@ -121,7 +124,7 @@ const StudentTableList: React.FC<StudentTableListPropsType> = ({ render: Row, st
|
|||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<Text mt='4' fontSize='sm' color='gray.500'>
|
<Text mt='4' fontSize='sm' color='gray.500'>
|
||||||
{students.length} students
|
{students.length} student(s)
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { Box, Heading, Text, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react';
|
import { Box, Tab, TabList, Flex, Tabs, TabPanels, TabPanel } from '@chakra-ui/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
@ -93,7 +93,7 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
<LessonList course={course} />
|
<LessonList course={course} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<StudentTableList render={StudentTableRow} students={course.students} />
|
<StudentTableList render={StudentTableRow} course={course} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@ -96,6 +96,14 @@ class CourseService {
|
|||||||
const result = await this.api.delete(`/v1/courses/${id}`);
|
const result = await this.api.delete(`/v1/courses/${id}`);
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async attachStudentsList(courseId: string, ids: Array<string>): Promise<IApiRespose<{}>> {
|
||||||
|
const result = await this.api.post(
|
||||||
|
`/v1/courses/${courseId}/attach-student-list`,
|
||||||
|
helpers.transformToFormData({ students: ids })
|
||||||
|
);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CourseServiceInstance = new CourseService(ApiServiceInstance.api);
|
export const CourseServiceInstance = new CourseService(ApiServiceInstance.api);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user