diff --git a/src/constants/translations/en/my-resources-page.json b/src/constants/translations/en/my-resources-page.json index 37517d0ea..a3f74c9f7 100644 --- a/src/constants/translations/en/my-resources-page.json +++ b/src/constants/translations/en/my-resources-page.json @@ -81,7 +81,8 @@ "emptyItems": "You have no categories yet", "successCreation": "«{{category}}» category was created successfully", "successDeletion": "Category was deleted successfully", - "confirmDeletionTitle": "Do you confirm category deletion?" + "confirmDeletionTitle": "Do you confirm category deletion?", + "categoryDropdown": "Category..." }, "confirmDeletionMessage": "This action is permanent and will remove all related content. Please review your decision before proceeding." } diff --git a/src/containers/category-dropdown/CategoryDropdown.styles.tsx b/src/containers/category-dropdown/CategoryDropdown.styles.tsx new file mode 100644 index 000000000..ec89459c6 --- /dev/null +++ b/src/containers/category-dropdown/CategoryDropdown.styles.tsx @@ -0,0 +1,18 @@ +export const styles = { + addButton: { + justifyContent: 'flex-start', + pl: '10px', + gap: '5px' + }, + labelCategory: { + color: 'primary.600', + maxWidth: '464px', + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '4px' + }, + divider: { + color: 'primary.300' + } +} diff --git a/src/containers/category-dropdown/CategoryDropdown.tsx b/src/containers/category-dropdown/CategoryDropdown.tsx new file mode 100644 index 000000000..5a45e46f0 --- /dev/null +++ b/src/containers/category-dropdown/CategoryDropdown.tsx @@ -0,0 +1,152 @@ +import { HTMLAttributes, SyntheticEvent, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import Divider from '@mui/material/Divider' +import AddIcon from '@mui/icons-material/Add' + +import useAxios from '~/hooks/use-axios' +import { useModalContext } from '~/context/modal-context' +import { useSnackBarContext } from '~/context/snackbar-context' +import { ResourceService } from '~/services/resource-service' +import AsyncAutocomplete from '~/components/async-autocomlete/AsyncAutocomplete' +import AppButton from '~/components/app-button/AppButton' +import AddCategoriesModal from '~/containers/my-resources/add-categories-modal/AddCategoriesModal' + +import { snackbarVariants } from '~/constants' +import { + ButtonVariantEnum, + Categories, + CategoryNameInterface, + ComponentEnum, + CreateCategoriesParams, + ErrorResponse, + SizeEnum, + TypographyVariantEnum +} from '~/types' +import { styles } from '~/containers/category-dropdown/CategoryDropdown.styles' + +interface CategoryDropdownInterface { + category: string | null + onCategoryChange: ( + _: SyntheticEvent, + value: CategoryNameInterface | null + ) => void +} + +const CategoryDropdown = ({ + category, + onCategoryChange +}: CategoryDropdownInterface) => { + const { t } = useTranslation() + const { setAlert } = useSnackBarContext() + const { openModal, closeModal } = useModalContext() + const [isFetched, setIsFetched] = useState(false) + const [isFetchedOnFocus, setIsFetchedOnFocus] = useState(false) + + const handleResponseError = (error: ErrorResponse) => { + setAlert({ + severity: snackbarVariants.error, + message: error ? `errors.${error.code}` : '' + }) + } + + const getCategories = useCallback(() => { + setIsFetched(true) + setIsFetchedOnFocus(true) + return ResourceService.getResourcesCategoriesNames() + }, [setIsFetched]) + + const onCreateCategory = () => { + openModal({ + component: ( + + ) + }) + } + + const createCategory = useCallback( + (params?: CreateCategoriesParams) => + ResourceService.createResourceCategory(params), + [] + ) + + const onResponseCategory = useCallback( + (response: Categories | null) => { + const categoryName = response ? response.name : '' + + setAlert({ + severity: snackbarVariants.success, + message: t('myResourcesPage.categories.successCreation', { + category: categoryName + }) + }) + + setIsFetched(false) + }, + [setAlert, t] + ) + + const { fetchData: handleCreateCategory } = useAxios({ + service: createCategory, + defaultResponse: null, + fetchOnMount: false, + onResponse: onResponseCategory, + onResponseError: handleResponseError + }) + + const optionsList = ( + props: HTMLAttributes, + option: string, + index: number + ) => ( + + {index === 0 && ( + + + + {t('myResourcesPage.categories.addBtn')} + + + + )} + + {option} + + + ) + return ( + + + {t('questionPage.chooseCategory')} + + + fetchCondition={!isFetched} + fetchOnFocus={isFetchedOnFocus} + labelField='name' + onChange={onCategoryChange} + renderOption={(props, option, state) => + optionsList(props, option.name, state.index) + } + service={getCategories} + textFieldProps={{ + label: t('myResourcesPage.categories.categoryDropdown') + }} + value={category} + valueField='_id' + /> + + ) +} + +export default CategoryDropdown diff --git a/src/pages/create-or-edit-lesson/CreateOrEditLesson.constants.tsx b/src/pages/create-or-edit-lesson/CreateOrEditLesson.constants.tsx index ac598fdab..5670f1256 100644 --- a/src/pages/create-or-edit-lesson/CreateOrEditLesson.constants.tsx +++ b/src/pages/create-or-edit-lesson/CreateOrEditLesson.constants.tsx @@ -11,7 +11,8 @@ export const initialValues = { title: '', description: '', content: '', - attachments: [] + attachments: [], + category: null } export const defaultResponse = { diff --git a/src/pages/create-or-edit-lesson/CreateOrEditLesson.styles.ts b/src/pages/create-or-edit-lesson/CreateOrEditLesson.styles.ts index 113c992f7..ebc458872 100644 --- a/src/pages/create-or-edit-lesson/CreateOrEditLesson.styles.ts +++ b/src/pages/create-or-edit-lesson/CreateOrEditLesson.styles.ts @@ -43,6 +43,19 @@ export const styles = { gap: { xs: '24px', sm: '30px' }, justifyContent: 'space-between' }, + addButton: { + justifyContent: 'flex-start', + pl: '10px', + gap: '5px' + }, + labelCategory: { + color: 'primary.600', + maxWidth: '464px', + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: '4px' + }, attachmentList: { container: { background: palette.basic.grey, diff --git a/src/pages/create-or-edit-lesson/CreateOrEditLesson.tsx b/src/pages/create-or-edit-lesson/CreateOrEditLesson.tsx index 5382f4f1b..5d5118567 100644 --- a/src/pages/create-or-edit-lesson/CreateOrEditLesson.tsx +++ b/src/pages/create-or-edit-lesson/CreateOrEditLesson.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { SyntheticEvent, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Link, useNavigate, useParams } from 'react-router-dom' import { AxiosResponse } from 'axios' @@ -11,12 +11,12 @@ import IconButton from '@mui/material/IconButton' import Loader from '~/components/loader/Loader' import AddResources from '~/containers/add-resources/AddResources' import IconExtensionWithTitle from '~/components/icon-extension-with-title/IconExtensionWithTitle' - import { useModalContext } from '~/context/modal-context' import AppButton from '~/components/app-button/AppButton' import AppTextField from '~/components/app-text-field/AppTextField' import FileEditor from '~/components/file-editor/FileEditor' import PageWrapper from '~/components/page-wrapper/PageWrapper' +import CategoryDropdown from '~/containers/category-dropdown/CategoryDropdown' import { useSnackBarContext } from '~/context/snackbar-context' import useAxios from '~/hooks/use-axios' import useForm from '~/hooks/use-form' @@ -45,7 +45,8 @@ import { SizeEnum, TextFieldVariantEnum, Attachment, - ResourcesTabsEnum + ResourcesTabsEnum, + CategoryNameInterface } from '~/types' const CreateOrEditLesson = () => { @@ -129,6 +130,13 @@ const CreateOrEditLesson = () => { onResponseError: handleResponseError }) + const onCategoryChange = ( + _: SyntheticEvent, + value: CategoryNameInterface | null + ) => { + handleNonInputValueChange('category', value?._id ?? null) + } + const { data, errors, @@ -154,7 +162,7 @@ const CreateOrEditLesson = () => { } const { loading: getLessonLoading, fetchData: fetchDataLesson } = useAxios< - Lesson, + LessonData, string >({ service: getLesson, @@ -216,6 +224,10 @@ const CreateOrEditLesson = () => { value={data.description} variant={TextFieldVariantEnum.Standard} /> + { const { t } = useTranslation() const navigate = useNavigate() const { setAlert } = useSnackBarContext() - const { openModal, closeModal } = useModalContext() - const [isFetched, setIsFetched] = useState(false) const createQuestion = useCallback( (data?: QuestionForm) => ResourceService.createQuestion(data), [] ) - const createCategory = useCallback( - (params?: CreateCategoriesParams) => - ResourceService.createResourceCategory(params), - [] - ) - - const getCategories = useCallback(() => { - setIsFetched(true) - return ResourceService.getResourcesCategoriesNames() - }, []) - const onCategoryChange = ( _: SyntheticEvent, value: CategoryNameInterface | null @@ -77,22 +56,6 @@ const CreateOrEditQuestion = () => { navigate(authRoutes.myResources.root.path) } - const onResponseCategory = useCallback( - (response: Categories | null) => { - const categoryName = response ? response.name : '' - - setAlert({ - severity: snackbarVariants.success, - message: t('myResourcesPage.categories.successCreation', { - category: categoryName - }) - }) - - setIsFetched(false) - }, - [setAlert, t] - ) - const onResponseError = (error: ErrorResponse) => { setAlert({ severity: snackbarVariants.error, @@ -108,14 +71,6 @@ const CreateOrEditQuestion = () => { onResponseError }) - const { fetchData: handleCreateCategory } = useAxios({ - service: createCategory, - defaultResponse: null, - fetchOnMount: false, - onResponse: onResponseCategory, - onResponseError - }) - const { data, handleInputChange, handleNonInputValueChange, handleSubmit } = useForm({ initialValues: { @@ -134,17 +89,6 @@ const CreateOrEditQuestion = () => { const { type, title, text, answers, openAnswer, category } = data const { isOpenAnswer } = questionType(type) - const onCreateCategory = () => { - openModal({ - component: ( - - ) - }) - } - const isButtonsVisible = isOpenAnswer ? Boolean(title && text && openAnswer) : Boolean(title && text && answers[0]?.text) @@ -167,34 +111,6 @@ const CreateOrEditQuestion = () => { ) - const optionsList = ( - props: HTMLAttributes, - option: string, - index: number - ) => ( - - {index === 0 && ( - - - - {t('myResourcesPage.categories.addBtn')} - - - - )} - - {option} - - - ) - return ( @@ -208,24 +124,10 @@ const CreateOrEditQuestion = () => { value={title} variant={TextFieldVariantEnum.Standard} /> - - - - {t('questionPage.chooseCategory')} - - - fetchCondition={!isFetched} - fetchOnFocus - labelField='name' - onChange={onCategoryChange} - renderOption={(props, option, state) => - optionsList(props, option.name, state.index) - } - service={getCategories} - value={category} - valueField='_id' - /> - + { + mockAxiosClient + .onGet(URLs.resources.resourcesCategories.getNames) + .reply(200, categoriesNamesMock) + + beforeEach(() => { + renderWithProviders() + }) + + it('should choose the category from options list', async () => { + const autocomplete = screen.getByRole('combobox') + + expect(autocomplete).toBeInTheDocument() + expect(autocomplete.value).toBe('') + + fireEvent.click(autocomplete) + fireEvent.focus(autocomplete) + + fireEvent.change(autocomplete, { + target: { value: categoriesNamesMock[1].name } + }) + + fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }) + fireEvent.keyDown(autocomplete, { key: 'Enter' }) + + await waitFor(() => { + expect(autocomplete.value).toBe(categoriesNamesMock[1].name) + }) + + fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }) + fireEvent.keyDown(autocomplete, { key: 'Enter' }) + + expect(autocomplete.value).toBe(categoriesNamesMock[1].name) + }) + + it('should click on "add button" in options list', async () => { + const autocomplete = screen.getByRole('combobox') + + fireEvent.click(autocomplete) + fireEvent.focus(autocomplete) + + fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }) + + await waitFor(() => { + const addButton = screen.queryByText('myResourcesPage.categories.addBtn') + + fireEvent.click(addButton) + }) + + await waitFor(() => { + const newCategory = screen.getByText('myResourcesPage.categories.name') + + expect(newCategory).toBeInTheDocument() + }) + }) +}) diff --git a/tests/unit/pages/create-or-edit-lesson/CreateOrEditLesson.spec.jsx b/tests/unit/pages/create-or-edit-lesson/CreateOrEditLesson.spec.jsx index be36a56b6..df15ec371 100644 --- a/tests/unit/pages/create-or-edit-lesson/CreateOrEditLesson.spec.jsx +++ b/tests/unit/pages/create-or-edit-lesson/CreateOrEditLesson.spec.jsx @@ -3,15 +3,7 @@ import { renderWithProviders } from '~tests/test-utils' import CreateOrEditLesson from '~/pages/create-or-edit-lesson/CreateOrEditLesson' -const mockFetchData = vi.fn() - -vi.mock('~/services/resource-service', () => ({ - ResourceService: { - addLesson: () => mockFetchData() - } -})) - -describe('CreateOrEditLesson', () => { +describe('CreateOrEditLesson component test', () => { beforeEach(() => { renderWithProviders() }) @@ -37,28 +29,4 @@ describe('CreateOrEditLesson', () => { expect(title).toBeInTheDocument() }) - - it('display validation error if title or description is empty', () => { - const titleInput = screen.getByLabelText('lesson.labels.title') - - fireEvent.change(titleInput, { target: { value: '' } }) - - const descriptionInput = screen.getByLabelText('lesson.labels.description') - - fireEvent.change(descriptionInput, { target: { value: '' } }) - - const saveButton = screen.getByText('common.save') - - fireEvent.click(saveButton) - - const errorTitle = screen.getByText('lesson.errorMessages.title') - - expect(errorTitle).toBeInTheDocument() - - const errorDescription = screen.getByText( - 'lesson.errorMessages.description' - ) - - expect(errorDescription).toBeInTheDocument() - }) }) diff --git a/tests/unit/pages/create-or-edit-question/CreateOrEditQuestion.spec.jsx b/tests/unit/pages/create-or-edit-question/CreateOrEditQuestion.spec.jsx index e369b1301..e42f52455 100644 --- a/tests/unit/pages/create-or-edit-question/CreateOrEditQuestion.spec.jsx +++ b/tests/unit/pages/create-or-edit-question/CreateOrEditQuestion.spec.jsx @@ -1,20 +1,9 @@ -import { describe } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen } from '@testing-library/react' -import { renderWithProviders, mockAxiosClient } from '~tests/test-utils' +import { renderWithProviders } from '~tests/test-utils' import CreateOrEditQuestion from '~/pages/create-or-edit-question/CreateOrEditQuestion' -import { URLs } from '~/constants/request' - -const categoriesNamesMock = [ - { _id: '650c27618a9fbf234b8bb4cf', name: 'New category in resources!' }, - { _id: '650c27618a9fbf234b8bb4cd', name: 'Category 1' } -] describe('CreateOrEditQuestion component test', () => { - mockAxiosClient - .onGet(URLs.resources.resourcesCategories.getNames) - .reply(200, categoriesNamesMock) - beforeEach(() => { renderWithProviders() }) @@ -24,51 +13,4 @@ describe('CreateOrEditQuestion component test', () => { expect(title).toBeInTheDocument() }) - - it('should choose the category from options list', async () => { - const autocomplete = screen.getByRole('combobox') - - expect(autocomplete).toBeInTheDocument() - expect(autocomplete.value).toBe('') - - fireEvent.click(autocomplete) - fireEvent.focus(autocomplete) - - fireEvent.change(autocomplete, { - target: { value: categoriesNamesMock[1].name } - }) - - fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }) - fireEvent.keyDown(autocomplete, { key: 'Enter' }) - - await waitFor(() => { - expect(autocomplete.value).toBe(categoriesNamesMock[1].name) - }) - - fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }) - fireEvent.keyDown(autocomplete, { key: 'Enter' }) - - expect(autocomplete.value).toBe(categoriesNamesMock[1].name) - }) - - it('should click on "add button" in options list', async () => { - const autocomplete = screen.getByRole('combobox') - - fireEvent.click(autocomplete) - fireEvent.focus(autocomplete) - - fireEvent.keyDown(autocomplete, { key: 'ArrowDown' }) - - await waitFor(() => { - const addButton = screen.queryByText('myResourcesPage.categories.addBtn') - - fireEvent.click(addButton) - }) - - await waitFor(() => { - const newCategory = screen.getByText('myResourcesPage.categories.name') - - expect(newCategory).toBeInTheDocument() - }) - }) })