diff --git a/src/components/enhanced-table/EnhancedTable.tsx b/src/components/enhanced-table/EnhancedTable.tsx index 8e44465b8..c116b1c4b 100644 --- a/src/components/enhanced-table/EnhancedTable.tsx +++ b/src/components/enhanced-table/EnhancedTable.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' +import { SxProps } from '@mui/material' import ReportIcon from '@mui/icons-material/Report' import Box from '@mui/material/Box' import Paper from '@mui/material/Paper' @@ -12,6 +13,7 @@ import EnhancedTableRow from '~/components/enhanced-table/enhanced-table-row/Enh import FilterRow from '~/components/enhanced-table/filter-row/FilterRow' import Loader from '~/components/loader/Loader' +import { spliceSx } from '~/utils/helper-functions' import { styles } from '~/components/enhanced-table/EnhancedTable.styles' import { TableColumn, @@ -23,7 +25,7 @@ import { TableSort } from '~/types' -export interface EnhancedTableProps extends TableProps { +export interface EnhancedTableProps extends Omit { columns: TableColumn[] isSelection?: boolean rowActions?: TableRowAction[] @@ -35,6 +37,10 @@ export interface EnhancedTableProps extends TableProps { onRowClick?: (item: I) => void emptyTableKey?: string selectedRows?: I[] + style?: { + root?: SxProps + tableContainer?: SxProps + } } const EnhancedTable = ({ @@ -49,6 +55,7 @@ const EnhancedTable = ({ data, emptyTableKey = 'table.noExactMatches', selectedRows = [], + style = {}, ...props }: EnhancedTableProps) => { const { t } = useTranslation() @@ -69,7 +76,10 @@ const EnhancedTable = ({ )) const tableBody = ( - + ({ tableBody return ( - + {tableContent} ) diff --git a/src/components/enhanced-table/filter-row/FilterRow.jsx b/src/components/enhanced-table/filter-row/FilterRow.jsx index 281a7e6a1..1ebc3eafd 100644 --- a/src/components/enhanced-table/filter-row/FilterRow.jsx +++ b/src/components/enhanced-table/filter-row/FilterRow.jsx @@ -18,7 +18,7 @@ const FilterRow = ({ columns, filter = {}, isSelection }) => { /> )) - const emptyCell = isSelection && + const emptyCell = isSelection && filters && return ( diff --git a/src/components/filter-selector/FilterSelector.styles.ts b/src/components/filter-selector/FilterSelector.styles.ts new file mode 100644 index 000000000..c225d3262 --- /dev/null +++ b/src/components/filter-selector/FilterSelector.styles.ts @@ -0,0 +1,76 @@ +import palette from '~/styles/app-theme/app.pallete' +import { commonHoverShadow } from '~/styles/app-theme/custom-shadows' +import { TypographyVariantEnum } from '~/types' + +export const styles = { + root: { + maxWidth: { xs: '200px', md: '300px' }, + backgroundColor: 'primary.50', + display: 'flex', + alignItems: 'center', + px: '12px', + borderRadius: '100px' + }, + openMenuBtn: { p: 0, ml: '5px' }, + text: { typography: TypographyVariantEnum.Subtitle1 }, + chosenFilters: { + typography: TypographyVariantEnum.Subtitle1, + ml: '4px', + fontWeight: 500, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis' + }, + arrowIcon: (open: boolean) => ({ + color: 'primary.900', + transform: `rotate(${open ? 180 : 0}deg)`, + transition: 'transform 0.3s ease' + }), + menu: (loading: boolean) => ({ + '& .simplebar-content': { + ...(loading && { height: '216px', display: 'flex' }) + } + }), + menuPaperProps: { + style: { + width: '300px', + marginTop: '8px', + borderRadius: '8px', + boxShadow: commonHoverShadow + } + }, + inputWrapper: { p: '15px 20px 0px 20px' }, + input: { + m: '0 auto', + width: '100%', + borderRadius: '6px', + p: { xs: 0, sm: 0 }, + border: `1px solid ${palette.primary[400]}`, + '& > div': { pl: '10px' } + }, + clearAll: (isSelected: boolean) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'end', + columnGap: '3px', + typography: TypographyVariantEnum.Subtitle2, + ...(!isSelected && { color: 'primary.200' }), + ...(isSelected && { cursor: 'pointer' }), + m: '15px 20px 0 0' + }), + clearIcon: { height: '18px', width: '18px' }, + divider: { + border: `1px solid ${palette.primary[200]}`, + mt: '8px' + }, + noMatches: { + typography: TypographyVariantEnum.Subtitle1, + p: '15px 25px', + display: 'flex', + alignItems: 'center', + columnGap: 1 + }, + scrollableContent: { maxHeight: '216px' }, + loader: { color: 'primary.700' }, + noItemsIcon: { color: 'primary.400' } +} diff --git a/src/components/filter-selector/FilterSelector.tsx b/src/components/filter-selector/FilterSelector.tsx new file mode 100644 index 000000000..bd531bb88 --- /dev/null +++ b/src/components/filter-selector/FilterSelector.tsx @@ -0,0 +1,160 @@ +import { useState, ChangeEvent, useMemo, Dispatch, SetStateAction } from 'react' +import { useTranslation } from 'react-i18next' +import SimpleBar from 'simplebar-react' +import Box from '@mui/material/Box' +import Menu, { MenuProps } from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import { PopoverOrigin } from '@mui/material/Popover' +import Divider from '@mui/material/Divider' +import Checkbox from '@mui/material/Checkbox' +import Typography from '@mui/material/Typography' +import IconButton from '@mui/material/IconButton' +import ClearIcon from '@mui/icons-material/Clear' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' + +import useAxios from '~/hooks/use-axios' +import Loader from '~/components/loader/Loader' +import InputWithIcon from '~/components/input-with-icon/InputWithIcon' + +import { defaultResponses } from '~/constants' +import { styles } from '~/components/filter-selector/FilterSelector.styles' +import { ServiceFunction } from '~/types' + +interface FilterSelectorProps extends Omit { + title: string + service: ServiceFunction + selectedItems: string[] + setSelectedItems: Dispatch> + position?: PopoverOrigin['horizontal'] +} + +const FilterSelector = ({ + title, + service, + selectedItems, + setSelectedItems, + position = 'left', + ...props +}: FilterSelectorProps) => { + const { t } = useTranslation() + const [menuAnchor, setMenuAnchor] = useState(null) + const [inputValue, setInputValue] = useState('') + + const handleInputChange = (e: ChangeEvent) => { + setInputValue(e.target.value) + } + + const handleInputReset = () => { + setInputValue('') + } + + const handleMenuOpen = () => { + setMenuAnchor(document.getElementById('menu-filter')) + } + + const handleMenuClose = () => { + setMenuAnchor(null) + } + + const onMenuItemClick = (item: string) => { + if (selectedItems.includes(item)) { + setSelectedItems(selectedItems.filter((selected) => selected !== item)) + } else { + setSelectedItems([...selectedItems, item]) + } + } + + const onClearAll = () => { + selectedItems.length && setSelectedItems([]) + } + + const { loading, response } = useAxios({ + service, + defaultResponse: defaultResponses.array + }) + + const filteredItems = useMemo( + () => + response.filter((item) => + item.toLowerCase().includes(inputValue.toLowerCase()) + ), + [response, inputValue] + ) + + const menuItems = filteredItems.map((item) => ( + onMenuItemClick(item)} sx={styles.text}> + + {item} + + )) + + const scrollableContent = filteredItems.length ? ( + menuItems + ) : ( + + + {t('common.noItems')} + + ) + + const itemsLoad = !response.length && loading + const chosenFiltersText = selectedItems.length + ? selectedItems.join(', ') + : t('cooperationsPage.tabs.all') + + return ( + + {title}: + {chosenFiltersText} + + + + + + + + + + + + {t('header.notifications.clearAll')} + + + + + + {itemsLoad ? ( + + ) : ( + scrollableContent + )} + + + + ) +} + +export default FilterSelector diff --git a/src/components/question/Question.tsx b/src/components/question/Question.tsx index 2a43c1773..4b373e3ab 100644 --- a/src/components/question/Question.tsx +++ b/src/components/question/Question.tsx @@ -60,13 +60,14 @@ const Question: FC = ({ question, category, sx = {} }) => { } } ] + const menuItems = rowActions.map(({ label, func }) => ( void onAction(func)}> {label} )) - const answersList = question.items.map((answer, i) => ( + const answersList = question.answers.map((answer, i) => ( {title} - + {description} diff --git a/src/components/user-table/UserTable.jsx b/src/components/user-table/UserTable.jsx index 23f14037a..ea39df9cf 100644 --- a/src/components/user-table/UserTable.jsx +++ b/src/components/user-table/UserTable.jsx @@ -30,7 +30,7 @@ const UserTable = ({ status: 'all', role }) - const select = useSelect() + const select = useSelect({}) const sort = useSort({ initialSort }) const filter = useFilter({ initialFilters }) const pagination = usePagination({ itemsCount }) diff --git a/src/constants/request.ts b/src/constants/request.ts index d10a129bf..ace71319f 100644 --- a/src/constants/request.ts +++ b/src/constants/request.ts @@ -59,6 +59,9 @@ export const URLs = { questions: { get: '/questions', delete: '/questions' + }, + resourcesCategories: { + getNames: '/resources-categories/names' } }, messages: { diff --git a/src/constants/translations/en/common.json b/src/constants/translations/en/common.json index 5bc8cd83b..1a02c9b8a 100644 --- a/src/constants/translations/en/common.json +++ b/src/constants/translations/en/common.json @@ -35,6 +35,7 @@ "typeError": "Wrong file type. Only .pdf/.jpg/.jpeg files are allowed.", "allFilesSizeError": "All files size error", "fileSizeError": "Розмір одного файлу не може перевищувати 10 Мб.", + "noItems": "No items found", "labels": { "email": "Email", "firstName": "First name", diff --git a/src/constants/translations/en/my-resources-page.json b/src/constants/translations/en/my-resources-page.json index 78a0f4134..c7bd300c8 100644 --- a/src/constants/translations/en/my-resources-page.json +++ b/src/constants/translations/en/my-resources-page.json @@ -19,7 +19,7 @@ }, "attachments": { "addBtn": "Add Attachment", - "addFromAttachments": "Add files from your Attachments", + "add": "Add files from your Attachments", "file": "File", "size": "Size", "lastUpdate": "Last update", @@ -44,6 +44,7 @@ }, "questions": { "addBtn": "New question", + "add": "Add Questions", "title": "Title", "category": "Category", "updated": "Last updates", diff --git a/src/constants/translations/ua/common.json b/src/constants/translations/ua/common.json index 195321f46..74d4a2502 100644 --- a/src/constants/translations/ua/common.json +++ b/src/constants/translations/ua/common.json @@ -30,6 +30,7 @@ "typeError": "Неправильний тип файлу. Дозволяються лише файли .pdf/.jpg/.jpeg", "allFilesSizeError": "Помилка розміру всіх файлів", "fileSizeError": "Size for one file cannot be more than 10 MB.", + "noItems": "Елементів не знайдено", "labels": { "sendMessage": "Надіслати повідомлення", "viewDetails": "Переглянути деталі", diff --git a/src/constants/translations/ua/my-resources-page.json b/src/constants/translations/ua/my-resources-page.json index fc7f6848e..401e9e965 100644 --- a/src/constants/translations/ua/my-resources-page.json +++ b/src/constants/translations/ua/my-resources-page.json @@ -19,6 +19,7 @@ }, "attachments": { "addBtn": "Додайте файли з Вкладень", + "add": "Додайте файли зі своїх вкладень", "file": "Файл", "size": "Розмір", "lastUpdate": "Останнє оновлення", @@ -44,6 +45,7 @@ }, "questions": { "addBtn": "Нове запитання", + "add": "Додати Питання", "title": "Заголовок", "category": "Категорія", "updated": "Останнє оновлення", diff --git a/src/containers/add-attachments/AddAttachments.styles.ts b/src/containers/add-attachments/AddAttachments.styles.ts deleted file mode 100644 index f26246b86..000000000 --- a/src/containers/add-attachments/AddAttachments.styles.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { TypographyVariantEnum } from '~/types' - -export const styles = { - root: { - width: '65vw', - p: '20px' - }, - title: { - mb: '32px', - typography: TypographyVariantEnum.H4 - }, - searchIcon: { - color: 'primary.700' - }, - input: { - maxWidth: '480px', - border: '1px solid', - borderColor: 'primary.500', - borderRadius: '6px' - }, - tableWrapper: { - height: '50vh', - overflow: 'auto', - my: '16px' - }, - table: { - '& td,th': { - '&:first-of-type': { - borderTopLeftRadius: '10px', - borderBottomLeftRadius: '10px', - width: '60%' - }, - '&:last-of-type': { - borderTopRightRadius: '10px', - borderBottomRightRadius: '10px' - }, - '&:nth-of-type(2)': { - width: '20%' - } - } - }, - buttonsArea: { - display: 'flex', - justifyContent: 'space-between' - }, - addButton: { - mr: '16px' - } -} diff --git a/src/containers/add-attachments/AddAttachments.tsx b/src/containers/add-attachments/AddAttachments.tsx index ac34e62cf..9c25e4bb3 100644 --- a/src/containers/add-attachments/AddAttachments.tsx +++ b/src/containers/add-attachments/AddAttachments.tsx @@ -1,19 +1,12 @@ -import { ChangeEvent, FC, useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import SearchIcon from '@mui/icons-material/Search' -import Box from '@mui/material/Box' -import Typography from '@mui/material/Typography' - -import AddDocuments from '~/containers/add-documents/AddDocuments' -import AppButton from '~/components/app-button/AppButton' -import EnhancedTable from '~/components/enhanced-table/EnhancedTable' -import InputWithIcon from '~/components/input-with-icon/InputWithIcon' +import { FC, useCallback, useState } from 'react' + import { useModalContext } from '~/context/modal-context' +import { useSnackBarContext } from '~/context/snackbar-context' +import { ResourceService } from '~/services/resource-service' import useSort from '~/hooks/table/use-sort' import useAxios from '~/hooks/use-axios' import useBreakpoints from '~/hooks/use-breakpoints' -import { useSnackBarContext } from '~/context/snackbar-context' -import { ResourceService } from '~/services/resource-service' +import AddResourceModal from '~/containers/my-resources/add-resource-modal/AddResourceModal' import { columns, @@ -22,13 +15,7 @@ import { } from '~/containers/add-attachments/AddAttachments.constants' import { ajustColumns } from '~/utils/helper-functions' import { defaultResponses, snackbarVariants } from '~/constants' -import { styles } from '~/containers/add-attachments/AddAttachments.styles' -import { - Attachment, - ButtonVariantEnum, - ErrorResponse, - ItemsWithCount -} from '~/types' +import { Attachment, ErrorResponse, ItemsWithCount } from '~/types' interface AddAttachmentsProps { attachments: Attachment[] onAddAttachments: (attachments: Attachment[]) => void @@ -38,14 +25,10 @@ const AddAttachments: FC = ({ attachments = [], onAddAttachments }) => { - const [inputValue, setInputValue] = useState('') - const [selectedAttachments, setSelectedAttachments] = - useState(attachments) + const [selectedRows, setSelectedRows] = useState(attachments) - const { t } = useTranslation() const { closeModal } = useModalContext() const { setAlert } = useSnackBarContext() - const formData = new FormData() const breakpoints = useBreakpoints() const sortOptions = useSort({ @@ -76,35 +59,25 @@ const AddAttachments: FC = ({ defaultResponse: defaultResponses.itemsWithCount }) - const handleInputChange = (e: ChangeEvent) => { - setInputValue(e.target.value) - } - - const handleInputReset = () => { - setInputValue('') - } - - const handleRowClick = (item: Attachment) => { - if (selectedAttachments.find((attachment) => attachment._id === item._id)) { - setSelectedAttachments((prevSelectedAttachments) => - prevSelectedAttachments.filter( - (attachment) => attachment._id !== item._id - ) + const onRowClick = (item: Attachment) => { + if (selectedRows.find((attachment) => attachment._id === item._id)) { + setSelectedRows((prevSelectedRows) => + prevSelectedRows.filter((attachment) => attachment._id !== item._id) ) } else { - setSelectedAttachments((prevSelectedAttachments) => [ + setSelectedRows((prevSelectedAttachments) => [ ...prevSelectedAttachments, item ]) } } - const handleAddAttachments = () => { - onAddAttachments(selectedAttachments) + const onAddItems = () => { + onAddAttachments(selectedRows) closeModal() } - const filteredAttachments = useMemo( - () => + const getItems = useCallback( + (inputValue: string) => response.items.filter((item) => { const lowerCaseFileName = item.fileName.toLowerCase() const lowerCaseInputValue = inputValue.toLocaleLowerCase() @@ -115,7 +88,7 @@ const AddAttachments: FC = ({ return fileNameWithoutExtension.includes(lowerCaseInputValue) }), - [response.items, inputValue] + [response.items] ) const createAttachments = useCallback( @@ -137,58 +110,23 @@ const AddAttachments: FC = ({ onResponseError: onCreateAttachmentsError }) - const uploadFile = async (data: FormData) => { + const uploadItem = async (data: FormData) => { await fetchCreateAttachment(data) await fetchDataAttachments() } - return ( - - - {t('myResourcesPage.attachments.addFromAttachments')} - - } - onChange={handleInputChange} - onClear={handleInputReset} - placeholder={t('common.search')} - sx={styles.input} - value={inputValue} - /> - - - - - - - - {t('common.add')} - - - {t('common.cancel')} - - - - - - - ) + const props = { + columns: columnsToShow, + sort: sortOptions, + selectedRows, + onAddItems, + data: { loading, getItems }, + onRowClick, + uploadItem, + resource: 'attachments' + } + + return {...props} /> } export default AddAttachments diff --git a/src/containers/my-quizzes/edit-quiz-container/EditQuizContainer.tsx b/src/containers/my-quizzes/edit-quiz-container/EditQuizContainer.tsx index a4625fe07..5d202e7fd 100644 --- a/src/containers/my-quizzes/edit-quiz-container/EditQuizContainer.tsx +++ b/src/containers/my-quizzes/edit-quiz-container/EditQuizContainer.tsx @@ -6,6 +6,8 @@ import Divider from '@mui/material/Divider' import EditIcon from '@mui/icons-material/Edit' import AddIcon from '@mui/icons-material/Add' +import { useModalContext } from '~/context/modal-context' +import AddQuestions from '~/containers/my-resources/add-questions/AddQuestions' import AppButton from '~/components/app-button/AppButton' import AppTextField from '~/components/app-text-field/AppTextField' import PageWrapper from '~/components/page-wrapper/PageWrapper' @@ -16,14 +18,17 @@ import { ButtonTypeEnum, ButtonVariantEnum, ComponentEnum, + Question, SizeEnum, TextFieldVariantEnum } from '~/types' const EditQuizContainer = () => { const { t } = useTranslation() + const { openModal } = useModalContext() const [title, setTitle] = useState('') const [description, setDescription] = useState('') + const [questions, setQuestions] = useState([]) const handleTitleChange = ( e: ChangeEvent @@ -37,6 +42,18 @@ const EditQuizContainer = () => { setDescription(e.currentTarget.value) } + const onAddQuestions = (attachments: Question[]) => { + setQuestions(attachments) + } + + const onOpenAddQuestionsModal = () => { + openModal({ + component: ( + + ) + }) + } + return ( @@ -68,6 +85,7 @@ const EditQuizContainer = () => { diff --git a/src/containers/my-resources/add-questions/AddQuestions.constants.tsx b/src/containers/my-resources/add-questions/AddQuestions.constants.tsx new file mode 100644 index 000000000..9e54a3140 --- /dev/null +++ b/src/containers/my-resources/add-questions/AddQuestions.constants.tsx @@ -0,0 +1,62 @@ +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import Typography from '@mui/material/Typography' + +import AppChip from '~/components/app-chip/AppChip' +import TitleWithDescription from '~/components/title-with-description/TitleWithDescription' + +import { getFormattedDate } from '~/utils/helper-functions' +import { + ComponentEnum, + Question, + RemoveColumnRules, + SortEnum, + TableColumn +} from '~/types' +import { styles } from '~/containers/my-resources/add-questions/AddQuestions.styles' + +export const columns: TableColumn[] = [ + { + label: 'myResourcesPage.questions.title', + field: 'title', + calculatedCellValue: (item: Question) => { + return ( + + + {item.title} + + } + /> + ) + } + }, + { + label: 'myResourcesPage.questions.category', + calculatedCellValue: (item: Question) => ( + + {item.category.name} + + ) + }, + { + label: 'myResourcesPage.questions.updated', + field: 'updatedAt', + calculatedCellValue: (item: Question) => ( + + {getFormattedDate({ date: item.updatedAt })} + + ) + } +] + +export const removeColumnRules: RemoveColumnRules = { + tablet: ['myResourcesPage.questions.updated'] +} + +export const initialSort = { order: SortEnum.Desc, orderBy: 'updatedAt' } diff --git a/src/containers/my-resources/add-questions/AddQuestions.styles.ts b/src/containers/my-resources/add-questions/AddQuestions.styles.ts new file mode 100644 index 000000000..42153d3e7 --- /dev/null +++ b/src/containers/my-resources/add-questions/AddQuestions.styles.ts @@ -0,0 +1,30 @@ +import palette from '~/styles/app-theme/app.pallete' +import { TypographyVariantEnum } from '~/types' + +export const styles = { + titleWithDescription: { + wrapper: { display: 'flex', flexDirection: 'column', rowGap: '3px' }, + title: { + typography: TypographyVariantEnum.Subtitle2, + color: 'primary.900' + }, + description: { + typography: TypographyVariantEnum.Caption, + color: 'primary.400' + } + }, + questionTitle: { display: 'flex', alignItems: 'center', columnGap: '8px' }, + questionIcon: { width: '16px', height: '16px', color: 'primary.600' }, + categoryChip: { + backgroundColor: 'inherit', + border: `2px solid ${palette.basic.turquoiseDark}`, + borderRadius: '50px', + '& .MuiChip-label': { p: '0px 8px' } + }, + categoryChipLabel: { + typography: TypographyVariantEnum.Caption, + fontWeight: 500, + color: 'basic.turquoiseDark' + }, + date: { color: 'primary.400', typography: TypographyVariantEnum.Caption } +} diff --git a/src/containers/my-resources/add-questions/AddQuestions.tsx b/src/containers/my-resources/add-questions/AddQuestions.tsx new file mode 100644 index 000000000..6958de33f --- /dev/null +++ b/src/containers/my-resources/add-questions/AddQuestions.tsx @@ -0,0 +1,114 @@ +import { FC, useCallback, useState } from 'react' + +import { ResourceService } from '~/services/resource-service' +import { useSnackBarContext } from '~/context/snackbar-context' +import { useModalContext } from '~/context/modal-context' +import useSelect from '~/hooks/table/use-select' +import useSort from '~/hooks/table/use-sort' +import useAxios from '~/hooks/use-axios' +import useBreakpoints from '~/hooks/use-breakpoints' +import AddResourceModal from '~/containers/my-resources/add-resource-modal/AddResourceModal' + +import { + columns, + initialSort, + removeColumnRules +} from '~/containers/my-resources/add-questions/AddQuestions.constants' +import { ajustColumns } from '~/utils/helper-functions' +import { defaultResponses, snackbarVariants } from '~/constants' +import { Question, ErrorResponse, ItemsWithCount } from '~/types' +interface AddQuestionsProps { + questions: Question[] + onAddQuestions: (questions: Question[]) => void +} + +const AddQuestions: FC = ({ + questions = [], + onAddQuestions +}) => { + const { closeModal } = useModalContext() + const { setAlert } = useSnackBarContext() + const breakpoints = useBreakpoints() + const initialSelect = questions.map((question) => question._id) + const select = useSelect({ initialSelect }) + const sortOptions = useSort({ initialSort }) + const [selectedRows, setSelectedRows] = useState(questions) + + const { sort } = sortOptions + const { handleSelectClick } = select + + const columnsToShow = ajustColumns( + breakpoints, + columns, + removeColumnRules + ) + + const onResponseError = useCallback( + (error: ErrorResponse) => { + setAlert({ + severity: snackbarVariants.error, + message: error ? `errors.${error.code}` : '' + }) + }, + [setAlert] + ) + + const getMyQuestions = useCallback( + () => ResourceService.getQuestions({ sort }), + [sort] + ) + + const { loading, response } = useAxios>({ + service: getMyQuestions, + defaultResponse: defaultResponses.itemsWithCount, + onResponseError + }) + + const onRowClick = (item: Question) => { + if (selectedRows.find((question) => question._id === item._id)) { + setSelectedRows((selectedRows) => + selectedRows.filter((question) => question._id !== item._id) + ) + } else { + setSelectedRows((selectedRows) => [...selectedRows, item]) + } + handleSelectClick(undefined, item._id) + } + + const onAddItems = () => { + onAddQuestions(selectedRows) + closeModal() + } + + const getItems = useCallback( + (title: string, selectedCategories: string[]) => { + return response.items.filter((item) => { + const titleMatch = item.title + .toLocaleLowerCase() + .includes(title.toLocaleLowerCase()) + const categoryMatch = + selectedCategories.length === 0 || + selectedCategories.includes(item.category.name) + + return titleMatch && categoryMatch + }) + }, + [response.items] + ) + + const props = { + columns: columnsToShow, + sort: sortOptions, + select, + selectedRows, + isSelection: true, + onAddItems, + data: { loading, getItems }, + onRowClick, + resource: 'questions' + } + + return {...props} /> +} + +export default AddQuestions diff --git a/src/containers/my-resources/add-resource-modal/AddResourceModal.styles.ts b/src/containers/my-resources/add-resource-modal/AddResourceModal.styles.ts new file mode 100644 index 000000000..8ff89e318 --- /dev/null +++ b/src/containers/my-resources/add-resource-modal/AddResourceModal.styles.ts @@ -0,0 +1,41 @@ +import palette from '~/styles/app-theme/app.pallete' +import { TypographyVariantEnum, VisibilityEnum } from '~/types' + +const input = { + width: '100%', + borderColor: 'primary.200', + borderRadius: '6px' +} + +export const styles = { + root: { width: '65vw', p: '20px' }, + title: { + mb: '32px', + typography: TypographyVariantEnum.H4 + }, + inputWithFilter: { display: 'flex', gap: '16px' }, + searchIcon: { color: 'primary.700' }, + titleInput: { + maxWidth: '350px', + border: '1px solid', + ...input + }, + categoryInput: { maxWidth: '200px', ...input }, + categoryInputLabel: { color: palette.primary[400] }, + tableWrapper: (hasItems: boolean) => ({ + root: { my: '16px', height: '50vh' }, + tableContainer: { + height: hasItems ? '50vh' : 'auto', + '&::-webkit-scrollbar-track': { visibility: VisibilityEnum.Hidden }, + '&::-webkit-scrollbar-thumb': { borderRadius: 0 } + } + }), + table: { + '& th': { backgroundColor: 'primary.100' } + }, + buttonsArea: { + display: 'flex', + justifyContent: 'space-between' + }, + addButton: { mr: '16px' } +} diff --git a/src/containers/my-resources/add-resource-modal/AddResourceModal.tsx b/src/containers/my-resources/add-resource-modal/AddResourceModal.tsx new file mode 100644 index 000000000..a6afd7325 --- /dev/null +++ b/src/containers/my-resources/add-resource-modal/AddResourceModal.tsx @@ -0,0 +1,116 @@ +import { ChangeEvent, useState } from 'react' +import { useTranslation } from 'react-i18next' +import SearchIcon from '@mui/icons-material/Search' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' + +import { ResourceService } from '~/services/resource-service' +import { useModalContext } from '~/context/modal-context' +import AppButton from '~/components/app-button/AppButton' +import AddDocuments from '~/containers/add-documents/AddDocuments' +import EnhancedTable, { + EnhancedTableProps +} from '~/components/enhanced-table/EnhancedTable' +import InputWithIcon from '~/components/input-with-icon/InputWithIcon' +import FilterSelector from '~/components/filter-selector/FilterSelector' + +import { styles } from '~/containers/my-resources/add-resource-modal/AddResourceModal.styles' +import { ButtonVariantEnum, TableItem } from '~/types' + +interface AddResourceModalProps + extends Omit, 'data'> { + data: { + loading: boolean + getItems: (title: string, selectedItems: string[]) => T[] + } + selectedRows: T[] + onAddItems: () => void + uploadItem?: (data: FormData) => Promise + resource: string +} + +const AddResourceModal = ({ + data, + selectedRows, + onAddItems, + uploadItem, + resource, + ...props +}: AddResourceModalProps) => { + const { t } = useTranslation() + const { closeModal } = useModalContext() + const [inputValue, setInputValue] = useState('') + const [selectedItems, setSelectedItems] = useState([]) + + const formData = new FormData() + const { loading, getItems } = data + const items = getItems(inputValue, selectedItems) + + const handleInputChange = (e: ChangeEvent) => { + setInputValue(e.target.value) + } + + const handleInputReset = () => { + setInputValue('') + } + + return ( + + + {t(`myResourcesPage.${resource}.add`)} + + + + } + onChange={handleInputChange} + onClear={handleInputReset} + placeholder={t('common.search')} + sx={styles.titleInput} + value={inputValue} + /> + + + + + + + + + {t('common.add')} + + + {t('common.cancel')} + + + + {uploadItem && ( + + )} + + + ) +} + +export default AddResourceModal diff --git a/src/containers/my-resources/lessons-container/LessonsContainer.tsx b/src/containers/my-resources/lessons-container/LessonsContainer.tsx index db954f99e..4f3b75d7a 100644 --- a/src/containers/my-resources/lessons-container/LessonsContainer.tsx +++ b/src/containers/my-resources/lessons-container/LessonsContainer.tsx @@ -104,7 +104,7 @@ const LessonsContainer = () => { {loading ? ( diff --git a/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx b/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx index a001b1e0e..31f2b2b1d 100644 --- a/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx +++ b/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx @@ -30,9 +30,9 @@ export const columns: TableColumn[] = [ }, { label: 'myResourcesPage.questions.category', - calculatedCellValue: () => ( + calculatedCellValue: (item: Question) => ( - {'Vocabulary Building'} + {item.category.name} ) }, @@ -48,7 +48,7 @@ export const columns: TableColumn[] = [ ] export const removeColumnRules: RemoveColumnRules = { - tablet: ['myOffersPage.tableHeaders.updated'] + tablet: ['myResourcesPage.questions.updated'] } export const initialSort = { order: SortEnum.Desc, orderBy: 'updatedAt' } diff --git a/src/hooks/table/use-select.jsx b/src/hooks/table/use-select.jsx index c421b9d47..8b058b9b8 100644 --- a/src/hooks/table/use-select.jsx +++ b/src/hooks/table/use-select.jsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' -const useSelect = () => { - const [selected, setSelected] = useState([]) +const useSelect = ({ initialSelect = [] }) => { + const [selected, setSelected] = useState(initialSelect) const clearSelected = useCallback(() => setSelected([]), [setSelected]) diff --git a/src/services/resource-service.ts b/src/services/resource-service.ts index 46d4591a4..df64fd805 100644 --- a/src/services/resource-service.ts +++ b/src/services/resource-service.ts @@ -55,5 +55,9 @@ export const ResourceService = { return axiosClient.get(URLs.resources.questions.get, { params }) }, deleteQuestion: async (id: string): Promise => - await axiosClient.delete(createUrlPath(URLs.resources.questions.delete, id)) + await axiosClient.delete( + createUrlPath(URLs.resources.questions.delete, id) + ), + getResourcesCategoriesNames: (): Promise> => + axiosClient.get(URLs.resources.resourcesCategories.getNames) } diff --git a/src/types/my-resources/enum/myResources.enum.ts b/src/types/my-resources/enum/myResources.enum.ts index 420efac14..996a49f82 100644 --- a/src/types/my-resources/enum/myResources.enum.ts +++ b/src/types/my-resources/enum/myResources.enum.ts @@ -4,3 +4,9 @@ export enum ResourcesTabsEnum { Questions = 'questions', Attachments = 'attachments' } + +export enum QuestionTypesEnum { + MultipleChoice = 'multipleChoice', + OpenAnswer = 'openAnswer', + OneAnswer = 'oneAnswer' +} diff --git a/src/types/questions/interfaces/questions.interface.ts b/src/types/questions/interfaces/questions.interface.ts index 9f1b8ead8..1691ae02a 100644 --- a/src/types/questions/interfaces/questions.interface.ts +++ b/src/types/questions/interfaces/questions.interface.ts @@ -1,4 +1,9 @@ -import { CommonEntityFields, UserResponse } from '~/types' +import { + CategoryInterface, + CommonEntityFields, + QuestionTypesEnum, + UserResponse +} from '~/types' export interface Answer { id: string text: string @@ -8,8 +13,10 @@ export interface Answer { export interface Question extends CommonEntityFields { title: string text: string - items: Omit[] + answers: Omit[] author: Pick + type: QuestionTypesEnum + category: Pick } export interface QuestionCategory { name: string diff --git a/tests/unit/components/question/Question.spec.jsx b/tests/unit/components/question/Question.spec.jsx index 5b37ea82b..868144f41 100644 --- a/tests/unit/components/question/Question.spec.jsx +++ b/tests/unit/components/question/Question.spec.jsx @@ -4,7 +4,7 @@ import Question from '~/components/question/Question' const mockedQuestion = { question: { title: 'About Philosophy', - items: [ + answers: [ { id: '1', text: 'Buddha Shakyamuni', diff --git a/tests/unit/containers/add-attachments/AddAttachments.spec.jsx b/tests/unit/containers/add-attachments/AddAttachments.spec.jsx index abcd418c0..d1310175b 100644 --- a/tests/unit/containers/add-attachments/AddAttachments.spec.jsx +++ b/tests/unit/containers/add-attachments/AddAttachments.spec.jsx @@ -40,9 +40,7 @@ describe('AddAttachments', () => { }) it('should render the component', () => { - const addAttachments = screen.getByText( - 'myResourcesPage.attachments.addFromAttachments' - ) + const addAttachments = screen.getByText('myResourcesPage.attachments.add') expect(addAttachments).toBeInTheDocument() }), diff --git a/tests/unit/containers/my-resources/AddQuestions.spec.jsx b/tests/unit/containers/my-resources/AddQuestions.spec.jsx new file mode 100644 index 000000000..ce5d20ddc --- /dev/null +++ b/tests/unit/containers/my-resources/AddQuestions.spec.jsx @@ -0,0 +1,65 @@ +import { fireEvent, screen } from '@testing-library/react' + +import AddQuestions from '~/containers/my-resources/add-questions/AddQuestions' + +import { mockAxiosClient, renderWithProviders } from '~tests/test-utils' +import { URLs } from '~/constants/request' + +const questionMock = { + _id: '64fb2c33eba89699411d22bb', + title: 'First Question', + answers: [ + { text: 'First answer', isCorrect: true }, + { text: 'Second answer', isCorrect: false } + ], + author: '648afee884936e09a37deaaa', + category: { id: '64fb2c33eba89699411d22bb', name: 'New Category' }, + createdAt: '2023-09-08T14:14:11.373Z', + updatedAt: '2023-09-08T14:14:11.373Z' +} + +const responseItemsMock = Array(10) + .fill() + .map((_, index) => ({ + ...questionMock, + _id: `${index}`, + title: index + questionMock.title + })) + +const questionResponseMock = { + count: 10, + items: responseItemsMock +} + +describe('AddQuestions', () => { + beforeEach(() => { + mockAxiosClient + .onGet(URLs.resources.questions.get) + .reply(200, questionResponseMock) + renderWithProviders() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should render title', () => { + const title = screen.getByText('myResourcesPage.questions.add') + + expect(title).toBeInTheDocument() + }), + it('should filter questions', async () => { + const placeholder = screen.getByPlaceholderText('common.search') + + expect(placeholder).toBeInTheDocument() + + fireEvent.click(placeholder) + fireEvent.change(placeholder, { + target: { value: responseItemsMock[1].title } + }) + + const filteredQuestionsCount = screen.getAllByRole('row').length - 2 + + expect(filteredQuestionsCount).toBe(1) + }) +}) diff --git a/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx b/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx index c475ad670..372169125 100644 --- a/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx +++ b/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx @@ -13,6 +13,7 @@ const questionMock = { { text: 'Second answer', isCorrect: false } ], author: '648afee884936e09a37deaaa', + category: { id: '64fb2c33eba89699411d22bb', name: 'New Category' }, createdAt: '2023-09-08T14:14:11.373Z', updatedAt: '2023-09-08T14:14:11.373Z' } diff --git a/tests/unit/hooks/table/use-select.spec.jsx b/tests/unit/hooks/table/use-select.spec.jsx index 1ba825f2f..5183e2387 100644 --- a/tests/unit/hooks/table/use-select.spec.jsx +++ b/tests/unit/hooks/table/use-select.spec.jsx @@ -13,7 +13,7 @@ describe('Use select custom hook', () => { let result beforeEach(() => { - const { result: renderedHookResult } = renderHook(() => useSelect()) + const { result: renderedHookResult } = renderHook(() => useSelect({})) result = renderedHookResult })