From 0932d7740f2d2ff920060df3a11b34fbceb6080d Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 20 Oct 2022 02:23:32 +0400 Subject: [PATCH 1/5] feat(admin): Create Exercises page --- .../lessons/[lessonSlug]/[pageName]/index.tsx | 82 ++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx index a7696a9ea..e5febb059 100644 --- a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx +++ b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx @@ -5,6 +5,7 @@ import { GetAppProps, Lesson, useAddModuleMutation, + useGetExercisesQuery, useUpdateLessonMutation, useUpdateModuleMutation, withGetApp @@ -31,9 +32,21 @@ import { makeGraphqlVariable } from '../../../../../helpers/admin/adminHelpers' import QueryInfo from '../../../../../components/QueryInfo' +import LoadingSpinner from '../../../../../components/LoadingSpinner' +import AdminLessonExerciseCard from '../../../../../components/admin/lessons/AdminLessonExerciseCard' +import Card from '../../../../../components/Card' +import { Text } from '../../../../../components/theme/Text' +import Link from 'next/link' +import { CURRICULUM_PATH } from '../../../../../constants' const MAIN_PATH = '/admin/lessons' +enum Pages { + INTRODUCTION = 'introduction', + MODULES = 'modules', + EXERCISE_QUESTION = 'exercises' +} + const MODULES = gql` query { modules { @@ -168,6 +181,61 @@ const ModulesPage = ({ modules, lessonId, refetch }: ModulesPageProps) => { ) } +type ExercisesProps = { lessonSlug: string } +const ExercisesPage = ({ lessonSlug }: ExercisesProps) => { + const router = useRouter() + + const { data, loading, refetch } = useGetExercisesQuery() + + if (loading || !router.isReady) return + + const mapExercisesToExerciseCard = data?.exercises + .filter( + exercise => + exercise.flaggedAt && exercise.module.lesson.slug === lessonSlug + ) + .map(exercise => { + return ( + refetch()} + onUnflag={() => refetch()} + /> + ) + }) + + if (!mapExercisesToExerciseCard || !mapExercisesToExerciseCard.length) { + return ( + + + Exercises can be added from{' '} + + + + addExercise page + + + + + + ) + } + + return ( +
+ {mapExercisesToExerciseCard} +
+ ) +} + type ContentProps = { pageName?: string | string[] modules: Modules @@ -182,12 +250,16 @@ const Content = ({ refetch, lesson }: ContentProps) => { - if (pageName === 'modules') { + if (pageName === Pages.MODULES) { return ( ) } + if (pageName === Pages.EXERCISE_QUESTION) { + return + } + // The "key" prop is passed so the component update its states (re-render and reset states) return } @@ -228,11 +300,15 @@ const Lessons = ({ data }: GetAppProps) => { const tabs = [ { text: 'introduction', - url: `${MAIN_PATH}/${lessonSlug}/introduction` + url: `${MAIN_PATH}/${lessonSlug}/${Pages.INTRODUCTION}` }, { text: 'modules', - url: `${MAIN_PATH}/${lessonSlug}/modules` + url: `${MAIN_PATH}/${lessonSlug}/${Pages.MODULES}` + }, + { + text: 'exercises', + url: `${MAIN_PATH}/${lessonSlug}/${Pages.EXERCISE_QUESTION}` } ] const tabSelected = tabs.findIndex(tab => tab.text === pageName) From 2436cf889b0b7f7aa22b280baa04644de6704ff7 Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 20 Oct 2022 02:23:45 +0400 Subject: [PATCH 2/5] test: Add Exercises page test --- .../[lessonSlug]/[pageName]/lessons.test.js | 202 +++++++++++++++++- 1 file changed, 198 insertions(+), 4 deletions(-) diff --git a/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js b/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js index 97bf97cce..0c727b469 100644 --- a/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js +++ b/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js @@ -7,8 +7,12 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import dummyLessonData from '../../../../../../__dummy__/lessonData' import dummySessionData from '../../../../../../__dummy__/sessionData' import dummyAlertData from '../../../../../../__dummy__/alertData' +import getExercisesData from '../../../../../../__dummy__/getExercisesData' import '@testing-library/jest-dom' import GET_APP from '../../../../../../graphql/queries/getApp' +import GET_EXERCISES from '../../../../../../graphql/queries/getExercises' +import REMOVE_EXERCISE_FLAG from '../../../../../../graphql/queries/removeExerciseFlag' +import DELETE_EXERCISE from '../../../../../../graphql/queries/deleteExercise' import { MockedProvider } from '@apollo/client/testing' import { gql } from '@apollo/client' import userEvent from '@testing-library/user-event' @@ -151,17 +155,112 @@ const addModuleMock = { } } +const getExercisesMock = { + request: { + query: GET_EXERCISES + }, + result: { + data: { + ...getExercisesData, + exercises: [...getExercisesData.exercises].map(e => ({ + ...e, + flaggedAt: '2020-01-10', + flagReason: 'Bad exercise' + })) + } + } +} + +let getExercisesWithRefetchCalled = false +const getExerciseWithRefetchMock = { + request: { + query: GET_EXERCISES + }, + newData: () => { + if (getExercisesWithRefetchCalled) { + return { + data: { + ...getExercisesData, + exercises: [ + [...getExercisesData.exercises].map(e => ({ + ...e, + flaggedAt: '2020-01-10', + flagReason: 'Bad exercise' + }))[0] + ] + } + } + } else { + getExercisesWithRefetchCalled = true + return { + data: { + ...getExercisesData, + exercises: [...getExercisesData.exercises].map(e => ({ + ...e, + flaggedAt: '2020-01-10', + flagReason: 'Bad exercise' + })) + } + } + } + } +} + +const deleteExerciseMock = { + request: { + query: DELETE_EXERCISE, + variables: { id: 1 } + }, + result: { + data: { + deleteExercise: { + id: 1 + } + } + } +} + +const unflagExerciseMock = { + request: { + query: REMOVE_EXERCISE_FLAG, + variables: { + id: 1 + } + }, + result: { + data: { + removeExerciseFlag: { + id: 1 + } + } + } +} + const mocks = [ + getAppQueryMock, + getAppQueryMock, getAppQueryMock, modulesQueryMock, updateLessonMutationMock, - addModuleMock + addModuleMock, + getExercisesMock ] + const mocksWithError = [ getAppQueryMock, modulesQueryMock, updateLessonMutationMockWithError, - addModuleMock + addModuleMock, + getExercisesMock +] + +const mocksWithRefetchExercises = [ + getAppQueryMock, + getAppQueryMock, + getAppQueryMock, + getExerciseWithRefetchMock, + deleteExerciseMock, + unflagExerciseMock ] const useRouter = jest.spyOn(require('next/router'), 'useRouter') @@ -171,7 +270,8 @@ const useRouterObj = { pageName: 'modules', lessonSlug: 'js1' }, - push: jest.fn() + push: jest.fn(), + isReady: true } describe('modules', () => { @@ -419,7 +519,7 @@ describe('introduction', () => { await act(() => new Promise(res => setTimeout(res, 0))) await waitFor(() => { - expect(mocks[2].result).toBeCalled() + expect(updateLessonMutationMock.result).toBeCalled() }) }) @@ -513,3 +613,97 @@ describe('introduction', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument() }) }) + +describe('exercises', () => { + const useRouterExercises = { + ...useRouterObj, + asPath: 'c0d3.com/admin/lessons/js0/exercises', + query: { + ...useRouterObj.query, + lessonSlug: 'js0', + pageName: 'exercises' + } + } + + beforeAll(() => useRouter.mockImplementation(() => useRouterExercises)) + + it('Should render exercises page', async () => { + expect.assertions(1) + + render( + + + + ) + + expect((await screen.findAllByText('Email')).length).toBe(3) + }) + + it('Should not render exercises if they are none', async () => { + expect.assertions(1) + + const mocksOfMocks = mocks.slice(0, -1) + mocksOfMocks.push({ + ...getExercisesMock, + result: { + data: { + ...getExercisesData, + exercises: [...getExercisesData.exercises].map(e => ({ + ...e, + flaggedAt: '2020-01-10', + flagReason: 'Bad exercise', + module: { + ...e.module, + lesson: { + ...e.module.lesson, + slug: 'js3' + } + } + })) + } + } + }) + + render( + + + + ) + + await act(() => new Promise(res => setTimeout(res, 0))) + + expect(screen.getByText('addExercise page')).toBeInTheDocument() + }) + + it('Should refresh the exercises upon removing one', async () => { + expect.assertions(1) + + render( + + + + ) + + const [removeExerciseBtn] = await screen.findAllByText('REMOVE EXERCISE') + await userEvent.click(removeExerciseBtn) + + await act(() => new Promise(res => setTimeout(res, 0))) + + expect((await screen.findAllByText('Email')).length).toBe(1) + }) + + it('Should refresh the exercises upon unflagging one', async () => { + expect.assertions(1) + + render( + + + + ) + + const [removeExerciseBtn] = await screen.findAllByText('UNFLAG EXERCISE') + await userEvent.click(removeExerciseBtn) + + expect((await screen.findAllByText('Email')).length).toBe(1) + }) +}) From 522381277ad8a7acbf3cbf436dbda8321ca503c9 Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 20 Oct 2022 02:23:52 +0400 Subject: [PATCH 3/5] Update AdminLessonExerciseCard styles --- scss/adminLessonExerciseCard.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss/adminLessonExerciseCard.module.scss b/scss/adminLessonExerciseCard.module.scss index 2cb5481a8..41f4b2c32 100644 --- a/scss/adminLessonExerciseCard.module.scss +++ b/scss/adminLessonExerciseCard.module.scss @@ -10,7 +10,6 @@ background: colors.$bg-white; box-shadow: 0px 4px 25px hsla(0, 0%, 0%, 0.1); border-radius: 10px; - align-items: center; } .card__header { @@ -94,6 +93,7 @@ .card__footer { display: flex; column-gap: 26px; + margin-top: auto; .card__footer__btn { display: flex; From eba88d9c2efb456130cefbfdf90821427efc345b Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 20 Oct 2022 02:23:58 +0400 Subject: [PATCH 4/5] Update Admin page styles --- scss/modules.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scss/modules.module.scss b/scss/modules.module.scss index 0d0ed661e..83f1ce9d2 100644 --- a/scss/modules.module.scss +++ b/scss/modules.module.scss @@ -12,3 +12,9 @@ .container__modulesPanel__inputs { flex: 1; } + +.container__exercisesPanel { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 20px; +} From c1749f631d296a0605095ba52bc70419c61b4c3f Mon Sep 17 00:00:00 2001 From: Flacial Date: Mon, 24 Oct 2022 08:02:42 +0400 Subject: [PATCH 5/5] refactor: Simplify code --- pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx index e5febb059..c603bf3b8 100644 --- a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx +++ b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx @@ -197,11 +197,8 @@ const ExercisesPage = ({ lessonSlug }: ExercisesProps) => { .map(exercise => { return ( refetch()} onUnflag={() => refetch()}