From 450dab4eeeff09fe5dd7cd8843fcbeb83202cfc8 Mon Sep 17 00:00:00 2001 From: Sarakhman Anatoliy <63456632+Tolik170@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:16:17 +0300 Subject: [PATCH] implemented questions table (#1124) * implemented questions table * fixed comment and added description for title field * fix lint --- src/constants/request.ts | 3 + .../translations/en/my-resources-page.json | 9 ++ .../translations/ua/my-resources-page.json | 9 ++ .../my-quizzes/QuizzesContainer.tsx | 5 +- .../AttachmentsContainer.tsx | 6 +- .../lessons-container/LessonsContainer.tsx | 5 +- .../my-resources-table/MyResourcesTable.tsx | 19 ++-- .../QuestionsContainer.constants.tsx | 60 ++++++++++++ .../QuestionsContainer.styles.ts | 44 +++++++++ .../QuestionsContainer.tsx | 91 ++++++++++++++++++- src/services/attachment-service.ts | 17 ---- src/services/resource-service.ts | 8 +- src/styles/app-theme/app.pallete.ts | 3 +- .../interfaces/myCooperations.interfaces.ts | 5 +- .../my-resources/enum/myResources.enum.ts | 1 + .../interfaces/questions.interface.ts | 7 ++ src/utils/helper-functions.tsx | 5 +- .../my-resources/LessonsContainer.spec.jsx | 50 +++++++++- .../my-resources/QuestionsContainer.spec.jsx | 55 ++++++++++- 19 files changed, 355 insertions(+), 47 deletions(-) create mode 100644 src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx create mode 100644 src/containers/my-resources/questions-container/QuestionsContainer.styles.ts delete mode 100644 src/services/attachment-service.ts diff --git a/src/constants/request.ts b/src/constants/request.ts index 875e4fa00..8995f5e35 100644 --- a/src/constants/request.ts +++ b/src/constants/request.ts @@ -55,6 +55,9 @@ export const URLs = { get: '/attachments', patch: '/attachments', delete: '/attachments' + }, + questions: { + get: '/questions' } }, messages: { diff --git a/src/constants/translations/en/my-resources-page.json b/src/constants/translations/en/my-resources-page.json index 92ba72dbb..8ef225f7e 100644 --- a/src/constants/translations/en/my-resources-page.json +++ b/src/constants/translations/en/my-resources-page.json @@ -37,5 +37,14 @@ "confirmDeletionTitle":"Do you confirm quiz deletion?", "successDeletion":"Quiz was deleted successfully" }, + "questions": { + "addBtn": "New question", + "title": "Title", + "category": "Category", + "updated": "Last updates", + "emptyItems": "You have no questions yet", + "confirmDeletionTitle":"Do you confirm question deletion?", + "successDeletion":"Question was deleted successfully" + }, "confirmDeletionMessage":"This action is permanent and will remove all related content. Please review your decision before proceeding." } diff --git a/src/constants/translations/ua/my-resources-page.json b/src/constants/translations/ua/my-resources-page.json index f48e0f59c..d882d4155 100644 --- a/src/constants/translations/ua/my-resources-page.json +++ b/src/constants/translations/ua/my-resources-page.json @@ -37,5 +37,14 @@ "confirmDeletionTitle":"Ви підтверджуєте видалення тесту?", "successDeletion":"Тест було успішно видалено" }, + "questions": { + "addBtn": "Нове запитання", + "title": "Заголовок", + "category": "Категорія", + "updated": "Останнє оновлення", + "emptyItems": "У вас ще немає запитань", + "confirmDeletionTitle":"Ви підтверджуєте видалення запитання?", + "successDeletion":"Запитання було успішно видалено" + }, "confirmDeletionMessage":"Ця дія є остаточною, та призведе до видалення всього пов’язаного вмісту. Перш ніж продовжити, перегляньте своє рішення." } diff --git a/src/containers/my-quizzes/QuizzesContainer.tsx b/src/containers/my-quizzes/QuizzesContainer.tsx index 9ede05863..f1a12444e 100644 --- a/src/containers/my-quizzes/QuizzesContainer.tsx +++ b/src/containers/my-quizzes/QuizzesContainer.tsx @@ -30,7 +30,7 @@ import { ajustColumns, getScreenBasedLimit } from '~/utils/helper-functions' const QuizzesContainer = () => { const { setAlert } = useSnackBarContext() - const { page } = usePagination() + const { page, handleChangePage } = usePagination() const sortOptions = useSort({ initialSort }) const searchTitle = useRef('') const breakpoints = useBreakpoints() @@ -85,7 +85,8 @@ const QuizzesContainer = () => { itemsPerPage, actions: { onEdit: () => null }, resource: ResourcesTabsEnum.Quizzes, - sort: sortOptions + sort: sortOptions, + pagination: { page, onChange: handleChangePage } } return ( diff --git a/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx b/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx index 326531eed..cd0f2b812 100644 --- a/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx +++ b/src/containers/my-resources/attachments-container/AttachmentsContainer.tsx @@ -13,7 +13,6 @@ import useBreakpoints from '~/hooks/use-breakpoints' import useAxios from '~/hooks/use-axios' import usePagination from '~/hooks/table/use-pagination' import AddDocuments from '~/containers/add-documents/AddDocuments' -import { attachmentService } from '~/services/attachment-service' import { defaultResponses, snackbarVariants } from '~/constants' import { @@ -38,7 +37,7 @@ const AttachmentsContainer = () => { const { t } = useTranslation() const { setAlert } = useSnackBarContext() const breakpoints = useBreakpoints() - const { page } = usePagination() + const { page, handleChangePage } = usePagination() const sortOptions = useSort({ initialSort }) const searchFileName = useRef('') const [selectedItemId, setSelectedItemId] = useState('') @@ -107,7 +106,7 @@ const AttachmentsContainer = () => { }) const createAttachments = useCallback( - (data?: FormData) => attachmentService.createAttachments(data), + (data?: FormData) => ResourceService.createAttachments(data), [] ) @@ -151,6 +150,7 @@ const AttachmentsContainer = () => { actions: { onEdit }, resource: ResourcesTabsEnum.Attachments, sort: sortOptions, + pagination: { page, onChange: handleChangePage }, sx: styles.table } diff --git a/src/containers/my-resources/lessons-container/LessonsContainer.tsx b/src/containers/my-resources/lessons-container/LessonsContainer.tsx index 661bb3e29..db954f99e 100644 --- a/src/containers/my-resources/lessons-container/LessonsContainer.tsx +++ b/src/containers/my-resources/lessons-container/LessonsContainer.tsx @@ -36,7 +36,7 @@ import { const LessonsContainer = () => { const { setAlert } = useSnackBarContext() const navigate = useNavigate() - const { page } = usePagination() + const { page, handleChangePage } = usePagination() const sortOptions = useSort({ initialSort }) const searchTitle = useRef('') const breakpoints = useBreakpoints() @@ -95,7 +95,8 @@ const LessonsContainer = () => { itemsPerPage, actions: { onEdit }, resource: ResourcesTabsEnum.Lessons, - sort: sortOptions + sort: sortOptions, + pagination: { page, onChange: handleChangePage } } return ( diff --git a/src/containers/my-resources/my-resources-table/MyResourcesTable.tsx b/src/containers/my-resources/my-resources-table/MyResourcesTable.tsx index 613025069..6486da64d 100644 --- a/src/containers/my-resources/my-resources-table/MyResourcesTable.tsx +++ b/src/containers/my-resources/my-resources-table/MyResourcesTable.tsx @@ -1,10 +1,10 @@ import { useTranslation } from 'react-i18next' import { AxiosResponse } from 'axios' +import { PaginationProps } from '@mui/material' import { useSnackBarContext } from '~/context/snackbar-context' import useAxios from '~/hooks/use-axios' import useConfirm from '~/hooks/use-confirm' -import usePagination from '~/hooks/table/use-pagination' import AppPagination from '~/components/app-pagination/AppPagination' import EnhancedTable, { EnhancedTableProps @@ -21,6 +21,7 @@ interface MyResourcesTableInterface data: ResourcesTableData actions: { onEdit: (id: string) => void } services: { deleteService: (id?: string) => Promise } + pagination: PaginationProps } const MyResourcesTable = ({ @@ -29,13 +30,17 @@ const MyResourcesTable = ({ data, actions, services, + pagination, ...props }: MyResourcesTableInterface) => { const { t } = useTranslation() const { setAlert } = useSnackBarContext() - const { page, handleChangePage } = usePagination() const { openDialog } = useConfirm() + const { page, onChange } = pagination + const { response, getData } = data + const { onEdit } = actions + const onDeleteError = (error: ErrorResponse) => { setAlert({ severity: snackbarVariants.error, @@ -61,7 +66,7 @@ const MyResourcesTable = ({ const handleDelete = async (id: string, isConfirmed: boolean) => { if (isConfirmed) { await deleteItem(id) - if (!error) await data.getData() + if (!error) await getData() } } @@ -76,7 +81,7 @@ const MyResourcesTable = ({ const rowActions = [ { label: t('common.edit'), - func: actions.onEdit + func: onEdit }, { label: t('common.delete'), @@ -87,16 +92,16 @@ const MyResourcesTable = ({ return ( <> - data={{ items: data.response.items }} + data={{ items: response.items }} emptyTableKey={`myResourcesPage.${resource}.emptyItems`} rowActions={rowActions} sx={roundedBorderTable} {...props} /> ) diff --git a/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx b/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx new file mode 100644 index 000000000..a001b1e0e --- /dev/null +++ b/src/containers/my-resources/questions-container/QuestionsContainer.constants.tsx @@ -0,0 +1,60 @@ +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' + +import IconTitleDescription from '~/components/icon-title-description/IconTitleDescription' +import AppChip from '~/components/app-chip/AppChip' + +import { Question, RemoveColumnRules, SortEnum, TableColumn } from '~/types' +import { getFormattedDate } from '~/utils/helper-functions' +import { styles } from '~/containers/my-resources/questions-container/QuestionsContainer.styles' + +export const columns: TableColumn[] = [ + { + label: 'myResourcesPage.questions.title', + field: 'title', + calculatedCellValue: (item: Question) => { + return ( + + + + } + sx={styles.iconTitleDescription} + title={item.title} + /> + ) + } + }, + { + label: 'myResourcesPage.questions.category', + calculatedCellValue: () => ( + + {'Vocabulary Building'} + + ) + }, + { + label: 'myResourcesPage.questions.updated', + field: 'updatedAt', + calculatedCellValue: (item: Question) => ( + + {getFormattedDate({ date: item.updatedAt })} + + ) + } +] + +export const removeColumnRules: RemoveColumnRules = { + tablet: ['myOffersPage.tableHeaders.updated'] +} + +export const initialSort = { order: SortEnum.Desc, orderBy: 'updatedAt' } + +export const itemsLoadLimit = { + default: 10, + tablet: 8, + mobile: 6 +} diff --git a/src/containers/my-resources/questions-container/QuestionsContainer.styles.ts b/src/containers/my-resources/questions-container/QuestionsContainer.styles.ts new file mode 100644 index 000000000..025f833c7 --- /dev/null +++ b/src/containers/my-resources/questions-container/QuestionsContainer.styles.ts @@ -0,0 +1,44 @@ +import palette from '~/styles/app-theme/app.pallete' +import { TypographyVariantEnum } from '~/types' + +export const styles = { + iconTitleDescription: { + container: { display: 'flex', columnGap: '16px', alignItems: 'center' }, + icon: { + svg: { width: '16px', height: '16px', color: 'primary.600' } + }, + titleWithDescription: { + wrapper: { display: 'flex', flexDirection: 'column', rowGap: '3px' }, + title: { + typography: TypographyVariantEnum.Subtitle2, + color: 'primary.900' + }, + description: { + typography: TypographyVariantEnum.Caption, + color: 'primary.400' + } + } + }, + iconWrapper: { + display: 'flex', + alignItems: 'center', + backgroundColor: 'basic.grey', + borderRadius: '4px', + p: '8px' + }, + 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/questions-container/QuestionsContainer.tsx b/src/containers/my-resources/questions-container/QuestionsContainer.tsx index 5a4e52352..d7a4127cd 100644 --- a/src/containers/my-resources/questions-container/QuestionsContainer.tsx +++ b/src/containers/my-resources/questions-container/QuestionsContainer.tsx @@ -1,9 +1,98 @@ +import { useCallback, useRef } from 'react' import Box from '@mui/material/Box' +import { useSnackBarContext } from '~/context/snackbar-context' +import { ResourceService } from '~/services/resource-service' +import AddResourceWithInput from '~/containers/my-resources/add-resource-with-input/AddResourceWithInput' +import MyResourcesTable from '~/containers/my-resources/my-resources-table/MyResourcesTable' +import Loader from '~/components/loader/Loader' +import useSort from '~/hooks/table/use-sort' +import useBreakpoints from '~/hooks/use-breakpoints' +import useAxios from '~/hooks/use-axios' + +import { defaultResponses, snackbarVariants } from '~/constants' +import { + columns, + initialSort, + itemsLoadLimit, + removeColumnRules +} from '~/containers/my-resources/questions-container/QuestionsContainer.constants' +import { + ItemsWithCount, + GetResourcesParams, + ErrorResponse, + ResourcesTabsEnum, + Question +} from '~/types' +import { ajustColumns, getScreenBasedLimit } from '~/utils/helper-functions' + const QuestionsContainer = () => { + const { setAlert } = useSnackBarContext() + const sortOptions = useSort({ initialSort }) + const searchTitle = useRef('') + const breakpoints = useBreakpoints() + + const { sort } = sortOptions + const itemsPerPage = getScreenBasedLimit(breakpoints, itemsLoadLimit) + const columnsToShow = ajustColumns( + breakpoints, + columns, + removeColumnRules + ) + + const onResponseError = useCallback( + (error: ErrorResponse) => { + setAlert({ + severity: snackbarVariants.error, + message: error ? `errors.${error.code}` : '' + }) + }, + [setAlert] + ) + + const getQuestions = useCallback( + () => + ResourceService.getQuestions({ + limit: itemsPerPage, + sort, + title: searchTitle.current + }), + [itemsPerPage, sort] + ) + + const { response, loading, fetchData } = useAxios< + ItemsWithCount, + GetResourcesParams + >({ + service: getQuestions, + defaultResponse: defaultResponses.itemsWithCount, + onResponseError + }) + + const props = { + columns: columnsToShow, + data: { response, getData: fetchData }, + services: { deleteService: () => null }, + itemsPerPage, + actions: { onEdit: () => null }, + resource: ResourcesTabsEnum.Questions, + sort: sortOptions, + pagination: { page: 1, onChange: () => null } + } + return ( -

Questions

+ + {loading ? ( + + ) : ( + {...props} /> + )}
) } diff --git a/src/services/attachment-service.ts b/src/services/attachment-service.ts deleted file mode 100644 index a4f086eee..000000000 --- a/src/services/attachment-service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AxiosResponse } from 'axios' -import { URLs } from '~/constants/request' -import { axiosClient } from '~/plugins/axiosClient' -import { GetAttachmentsParams } from '~/types' - -export const attachmentService = { - getAttachments: async ( - params: GetAttachmentsParams - ): Promise => - await axiosClient.get(URLs.resources.attachments.get, { params }), - - createAttachments: (data?: FormData): Promise => { - return axiosClient.post(URLs.attachments.post, data, { - headers: { 'Content-Type': 'multipart/form-data' } - }) - } -} diff --git a/src/services/resource-service.ts b/src/services/resource-service.ts index ba91420b5..21eb78037 100644 --- a/src/services/resource-service.ts +++ b/src/services/resource-service.ts @@ -10,7 +10,8 @@ import { LessonData, Lesson, NewLesson, - UpdateAttachmentParams + UpdateAttachmentParams, + Question } from '~/types' import { createUrlPath } from '~/utils/helper-functions' @@ -47,5 +48,10 @@ export const ResourceService = { return axiosClient.post(URLs.attachments.post, data, { headers: { 'Content-Type': 'multipart/form-data' } }) + }, + getQuestions: ( + params?: GetResourcesParams + ): Promise>> => { + return axiosClient.get(URLs.resources.questions.get, { params }) } } diff --git a/src/styles/app-theme/app.pallete.ts b/src/styles/app-theme/app.pallete.ts index 56c297c46..f44b327bb 100644 --- a/src/styles/app-theme/app.pallete.ts +++ b/src/styles/app-theme/app.pallete.ts @@ -17,7 +17,8 @@ const palette = { fruitSalad: '#4CAF50', orientalHerbs: '#12A03A', lime: '#99CC00', - turquoise: '#489DA0' + turquoise: '#489DA0', + turquoiseDark: '#3B8587' }, companyBlue: 'rgba(0, 167, 167, 0.2)', error: { diff --git a/src/types/my-cooperations/interfaces/myCooperations.interfaces.ts b/src/types/my-cooperations/interfaces/myCooperations.interfaces.ts index 4b3787c78..94d9500e3 100644 --- a/src/types/my-cooperations/interfaces/myCooperations.interfaces.ts +++ b/src/types/my-cooperations/interfaces/myCooperations.interfaces.ts @@ -6,7 +6,8 @@ import { Offer, TableColumn, Attachment, - Quiz + Quiz, + Question } from '~/types' import { RequestParams } from '~/types/services/services.index' @@ -29,7 +30,7 @@ export interface ScreenBasedLimits { } export interface RemoveColumnRules< - T extends Cooperation | Offer | Lesson | Attachment | Quiz + T extends Cooperation | Offer | Lesson | Attachment | Quiz | Question > { desktop?: TableColumn['label'][] tablet?: TableColumn['label'][] diff --git a/src/types/my-resources/enum/myResources.enum.ts b/src/types/my-resources/enum/myResources.enum.ts index 0bb3d527d..420efac14 100644 --- a/src/types/my-resources/enum/myResources.enum.ts +++ b/src/types/my-resources/enum/myResources.enum.ts @@ -1,5 +1,6 @@ export enum ResourcesTabsEnum { Lessons = 'lessons', Quizzes = 'quizzes', + Questions = 'questions', Attachments = 'attachments' } diff --git a/src/types/questions/interfaces/questions.interface.ts b/src/types/questions/interfaces/questions.interface.ts index 88fd4dc06..fa6284d30 100644 --- a/src/types/questions/interfaces/questions.interface.ts +++ b/src/types/questions/interfaces/questions.interface.ts @@ -1,5 +1,12 @@ +import { CommonEntityFields, UserResponse } from '~/types' export interface Answer { id: number text: string isCorrect: boolean } + +export interface Question extends CommonEntityFields { + title: string + items: Omit[] + author: Pick +} diff --git a/src/utils/helper-functions.tsx b/src/utils/helper-functions.tsx index b656f4da1..c33f6054e 100644 --- a/src/utils/helper-functions.tsx +++ b/src/utils/helper-functions.tsx @@ -14,7 +14,8 @@ import { UserRole, UserRoleEnum, Attachment, - GroupedByDateItems + GroupedByDateItems, + Question } from '~/types' export const parseJwt = (token: string): T => { @@ -141,7 +142,7 @@ export const getScreenBasedLimit = ( } export const ajustColumns = < - T extends Cooperation | Offer | Lesson | Attachment | Quiz + T extends Cooperation | Offer | Lesson | Attachment | Quiz | Question >( breakpoints: Breakpoints, columns: TableColumn[], diff --git a/tests/unit/containers/my-resources/LessonsContainer.spec.jsx b/tests/unit/containers/my-resources/LessonsContainer.spec.jsx index 94866e769..cd552ac8d 100644 --- a/tests/unit/containers/my-resources/LessonsContainer.spec.jsx +++ b/tests/unit/containers/my-resources/LessonsContainer.spec.jsx @@ -1,15 +1,55 @@ import { screen } from '@testing-library/react' + import LessonsContainer from '~/containers/my-resources/lessons-container/LessonsContainer' -import { renderWithProviders } from '~tests/test-utils' -describe('LessonsContainer component ', () => { +import { mockAxiosClient, renderWithProviders } from '~tests/test-utils' +import { URLs } from '~/constants/request' + +const lessonMock = { + _id: '64e49ce305b3353b2ae6309e', + author: '648afee884936e09a37deaaa', + title: 'eew', + description: 'dsdfd', + attachments: [], + createdAt: '2023-08-22T11:32:51.995Z', + updatedAt: '2023-08-22T11:32:51.995Z' +} + +const responseItemsMock = Array(10) + .fill() + .map((_, index) => ({ + ...lessonMock, + _id: `${index}`, + title: index + lessonMock.title + })) + +const lessonResponseMock = { + count: 10, + items: responseItemsMock +} + +describe('LessonContainer test', () => { beforeEach(() => { + mockAxiosClient + .onGet(URLs.resources.lessons.get) + .reply(200, lessonResponseMock) renderWithProviders() }) - it('should render new lesson button', () => { - const newLessonBtn = screen.findByText('myResourcesPage.lessons.addBtn') + afterEach(() => { + vi.clearAllMocks() + }) + + it('should render "New lesson" button', () => { + const addBtn = screen.getByText('myResourcesPage.lessons.addBtn') + + expect(addBtn).toBeInTheDocument() + }) + it('should render table with lessons', () => { + const columnLabel = screen.getByText('myResourcesPage.lessons.title') + const lessonTitle = screen.getByText(responseItemsMock[5].title) - expect(newLessonBtn).not.toBeNull() + expect(columnLabel).toBeInTheDocument() + expect(lessonTitle).toBeInTheDocument() }) }) diff --git a/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx b/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx index f5d483464..c475ad670 100644 --- a/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx +++ b/tests/unit/containers/my-resources/QuestionsContainer.spec.jsx @@ -1,10 +1,57 @@ +import { screen } from '@testing-library/react' + import QuestionsContainer from '~/containers/my-resources/questions-container/QuestionsContainer' -import { renderWithProviders } from '~tests/test-utils' -describe('QuestionsContainer', () => { - it('should render text', () => { +import { URLs } from '~/constants/request' +import { mockAxiosClient, renderWithProviders } from '~tests/test-utils' + +const questionMock = { + _id: '64fb2c33eba89699411d22bb', + title: 'First Question', + answers: [ + { text: 'First answer', isCorrect: true }, + { text: 'Second answer', isCorrect: false } + ], + author: '648afee884936e09a37deaaa', + 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('QuestionsContainer test', () => { + beforeEach(() => { + mockAxiosClient + .onGet(URLs.resources.questions.get) + .reply(200, questionResponseMock) renderWithProviders() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should render "New question" button', () => { + const addBtn = screen.getByText('myResourcesPage.questions.addBtn') + + expect(addBtn).toBeInTheDocument() + }) + it('should render table with questions', () => { + const columnLabel = screen.getByText('myResourcesPage.questions.title') + const questionTitle = screen.getByText(responseItemsMock[5].title) - expect('Questions').toBeTruthy() + expect(columnLabel).toBeInTheDocument() + expect(questionTitle).toBeInTheDocument() }) })