From f55d5789de8f6cb41743655bb50614049aa6737f Mon Sep 17 00:00:00 2001 From: Flacial Date: Wed, 17 Aug 2022 07:00:25 +0400 Subject: [PATCH 01/16] feat(admin): Create Introduction page --- .../{modules.test.js => lessons.test.js} | 176 ++++++++++++++++-- .../lessons/[lessonSlug]/[pageName]/index.tsx | 124 ++++++++++-- 2 files changed, 269 insertions(+), 31 deletions(-) rename __tests__/pages/admin/lessons/[lessonSlug]/[pageName]/{modules.test.js => lessons.test.js} (54%) diff --git a/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/modules.test.js b/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js similarity index 54% rename from __tests__/pages/admin/lessons/[lessonSlug]/[pageName]/modules.test.js rename to __tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js index 6b923afab..533d4c6c8 100644 --- a/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/modules.test.js +++ b/__tests__/pages/admin/lessons/[lessonSlug]/[pageName]/lessons.test.js @@ -1,6 +1,9 @@ +jest.mock('@sentry/react') +import * as Sentry from '@sentry/react' + import React from 'react' -import Modules from '../../../../../../pages/admin/lessons/[lessonSlug]/[pageName]/index' -import { act, render, screen, waitFor } from '@testing-library/react' +import LessonPage from '../../../../../../pages/admin/lessons/[lessonSlug]/[pageName]/index' +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' @@ -9,6 +12,7 @@ import GET_APP from '../../../../../../graphql/queries/getApp' import { MockedProvider } from '@apollo/client/testing' import { gql } from '@apollo/client' import userEvent from '@testing-library/user-event' +import UPDATE_LESSON from '../../../../../../graphql/queries/updateLesson' // Imported to be able to use expect(...).toBeInTheDocument() import '@testing-library/jest-dom' @@ -92,11 +96,45 @@ const modulesQueryMock = { } } -const mocks = [getAppQueryMock, modulesQueryMock] +const js1 = { + id: 2, + title: 'Functions & JavaScript', + description: + 'Learn how to solve simple algorithm problems recursively with the following exercises. ', + docUrl: + 'https://www.notion.so/garagescript/JS-1-Functions-01dd8400b85f40d083966908acbfa184', + githubUrl: 'https://git.c0d3.com/song/curriculum', + videoUrl: + 'https://www.youtube.com/watch?v=H-eqRQo8KoI&list=PLKmS5c0UNZmewGBWlz0l9GZwh3bV8Rlc7&index=1', + order: 1, + slug: 'js1', + chatUrl: 'https://chat.c0d3.com/c0d3/channels/js2-arrays' +} + +const updateLessonMutationMock = { + request: { query: UPDATE_LESSON, variables: js1 }, + result: jest.fn(() => ({ + data: { + updateLesson: js1 + } + })) +} + +const updateLessonMutationMockWithError = { + request: { query: UPDATE_LESSON, variables: js1 }, + error: new Error('Error') +} + +const mocks = [getAppQueryMock, modulesQueryMock, updateLessonMutationMock] +const mocksWithError = [ + getAppQueryMock, + modulesQueryMock, + updateLessonMutationMockWithError +] const useRouter = jest.spyOn(require('next/router'), 'useRouter') const useRouterObj = { - asPath: 'c0d3.com/admin/lessons/1/modules', + asPath: 'c0d3.com/admin/lessons/js1/modules', query: { pageName: 'modules', lessonSlug: 'js1' @@ -104,15 +142,15 @@ const useRouterObj = { push: jest.fn() } -useRouter.mockImplementation(() => useRouterObj) - describe('modules', () => { - it('Should render modules', async () => { + beforeAll(() => useRouter.mockImplementation(() => useRouterObj)) + + it('Should render modules page', async () => { expect.assertions(2) render( - + ) @@ -132,7 +170,7 @@ describe('modules', () => { render( - + ) @@ -149,7 +187,7 @@ describe('modules', () => { render( - + ) @@ -178,7 +216,7 @@ describe('modules', () => { render( - + ) @@ -198,7 +236,7 @@ describe('modules', () => { render( - + ) @@ -215,7 +253,7 @@ describe('modules', () => { render( - + ) @@ -244,7 +282,7 @@ describe('modules', () => { render( - + ) @@ -256,3 +294,113 @@ describe('modules', () => { ) }) }) + +describe('introduction', () => { + const useRouterIntroduction = { + ...useRouterObj, + asPath: 'c0d3.com/admin/lessons/js1/introduction', + query: { + ...useRouterObj.query, + pageName: 'introduction' + } + } + + beforeAll(() => useRouter.mockImplementation(() => useRouterIntroduction)) + + const randomTitle = 'Functions & JavaScript' + + const fillOutIntroductionForms = async () => { + const titleField = screen.getByTestId('input1') + + // the type event needs to be delayed so the Formik validations finish + await userEvent.type(titleField, randomTitle, { delay: 1 }) + } + + it('Should render introduction', async () => { + expect.assertions(1) + + render( + + + + ) + + // Used to make the queries resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + await fillOutIntroductionForms() + + await waitFor(() => + // input1 is the Title + expect(screen.getByTestId('input1').value).toBe(randomTitle) + ) + }) + + it('Should update lesson (submit)', async () => { + expect.hasAssertions() + + render( + + + + ) + + // Used to make the queries resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + await fillOutIntroductionForms() + fireEvent.click(screen.queryByText('Save changes')) + + await act(() => new Promise(res => setTimeout(res, 0))) + + await waitFor(() => { + expect(mocks[2].result).toBeCalled() + }) + }) + + it('Should not update lesson on invalid inputs', async () => { + expect.hasAssertions() + + render( + + + + ) + + // Used to make the queries resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + const titleInput = screen.getByTestId('input1') + await userEvent.clear(titleInput) + + fireEvent.click(screen.queryByText('Save changes')) + + await act(() => new Promise(res => setTimeout(res, 0))) + + await waitFor(() => { + expect(screen.queryByText('Required')).toBeInTheDocument() + }) + }) + + it('Should capture error with Sentry', async () => { + expect.hasAssertions() + + render( + + + + ) + + // Used to make the queries resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + await fillOutIntroductionForms() + fireEvent.click(screen.queryByText('Save changes')) + + await act(() => new Promise(res => setTimeout(res, 0))) + + await waitFor(() => { + expect(Sentry.captureException).toBeCalled() + }) + }) +}) diff --git a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx index 7fe0a38e4..515f868bd 100644 --- a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx +++ b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx @@ -1,7 +1,13 @@ +import React, { useEffect, useMemo, useState } from 'react' +import * as Sentry from '@sentry/react' import { gql, useQuery } from '@apollo/client' -import { GetAppProps, withGetApp } from '../../../../../graphql' +import { + GetAppProps, + Lesson, + useUpdateLessonMutation, + withGetApp +} from '../../../../../graphql' import { toUpper } from 'lodash' -import React, { useMemo, useState } from 'react' import AdminLessonNav from '../../../../../components/admin/lessons/AdminLessonSideNavLayout' import AdminLessonSideNav from '../../../../../components/admin/lessons/AdminLessonSideNav' import AdminLessonInputs from '../../../../../components/admin/lessons/AdminLessonInputs' @@ -13,6 +19,14 @@ import { AdminLayout } from '../../../../../components/admin/AdminLayout' import { useRouter } from 'next/router' import Link from 'next/link' import { compose, filter, get, sortBy } from 'lodash/fp' +import { FormCard } from '../../../../../components/FormCard' +import { formChange } from '../../../../../helpers/formChange' +import { lessonSchema } from '../../../../../helpers/formValidation' +import { + getPropertyArr, + errorCheckAllFields, + makeGraphqlVariable +} from '../../../../../helpers/admin/adminHelpers' const MAIN_PATH = '/admin/lessons' @@ -39,26 +53,76 @@ type Module = { } type Modules = Module[] -type ContentProps = { - pageName?: string | string[] +const IntroductionPage = ({ lesson }: { lesson: Lesson }) => { + const [updateLesson] = useUpdateLessonMutation() + + const [formOptions, setFormOptions] = useState( + getPropertyArr(lesson, ['challenges', '__typename']) + ) + + // Update the Inputs values when the lesson changes + useEffect( + () => setFormOptions(getPropertyArr(lesson, ['challenges', '__typename'])), + [lesson] + ) + + const handleChange = async (value: string, propertyIndex: number) => { + await formChange( + value, + propertyIndex, + formOptions, + setFormOptions, + lessonSchema + ) + } + + const onClick = async () => { + try { + const newProperties = [...formOptions] + const valid = await errorCheckAllFields(newProperties, lessonSchema) + + if (!valid) { + // Update the forms so the error messages appear + setFormOptions(newProperties) + return + } + + await updateLesson(makeGraphqlVariable(formOptions)) + } catch (err) { + // TODO: Display error with QueryStateMessage #2181 + Sentry.captureException(err) + } + } + + return ( +
+

Lesson Info

+
+ +
+
+ ) +} + +type ModulesPageProps = { modules: Modules lessonId: number refetch: Props['refetch'] } - -const Content = ({ pageName, modules, lessonId, refetch }: ContentProps) => { +const ModulesPage = ({ modules, lessonId, refetch }: ModulesPageProps) => { const [selectedIndex, setSelectedIndex] = useState(-1) const onAddItem = () => setSelectedIndex(-1) const onSelect = (item: Omit) => setSelectedIndex(item.id) - if (pageName !== 'modules') { - return ( -

- For now, you can only access /modules page -

- ) - } - return (
{ ) } +type ContentProps = { + pageName?: string | string[] + modules: Modules + lessonId: number + refetch: Props['refetch'] + lesson: Lesson +} +const Content = ({ + pageName, + modules, + lessonId, + refetch, + lesson +}: ContentProps) => { + if (pageName === 'modules') { + return ( + + ) + } + + return +} + const Lessons = ({ data }: GetAppProps) => { const router = useRouter() const { pageName, lessonSlug } = router.query @@ -92,9 +179,7 @@ const Lessons = ({ data }: GetAppProps) => { if (lessonFromParam) return { - title: lessonFromParam.title, - slug: lessonFromParam.slug, - id: lessonFromParam.id + ...lessonFromParam } } @@ -150,6 +235,10 @@ const Lessons = ({ data }: GetAppProps) => { { tabName: 'modules', urlPageName: 'modules' + }, + { + tabName: 'introduction', + urlPageName: 'introduction' } ]} Component={LessonNav} @@ -161,6 +250,7 @@ const Lessons = ({ data }: GetAppProps) => { modules={filteredModules} lessonId={lesson.id} refetch={refetch} + lesson={lesson as Lesson} /> From d5213b794880017b18130d8516eb534f5722530a Mon Sep 17 00:00:00 2001 From: Flacial Date: Mon, 22 Aug 2022 01:48:22 +0400 Subject: [PATCH 02/16] refactor: Pass key prop The IntroductionPage component has been passed a key prop because when the lesson changes, we want the inputs to also change and have the new lesson data (as their default values) --- .../admin/lessons/[lessonSlug]/[pageName]/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx index 515f868bd..417c1c29a 100644 --- a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx +++ b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React, { useMemo, useState } from 'react' import * as Sentry from '@sentry/react' import { gql, useQuery } from '@apollo/client' import { @@ -61,10 +61,10 @@ const IntroductionPage = ({ lesson }: { lesson: Lesson }) => { ) // Update the Inputs values when the lesson changes - useEffect( - () => setFormOptions(getPropertyArr(lesson, ['challenges', '__typename'])), - [lesson] - ) + // useEffect( + // () => setFormOptions(getPropertyArr(lesson, ['challenges', '__typename'])), + // [lesson] + // ) const handleChange = async (value: string, propertyIndex: number) => { await formChange( @@ -163,7 +163,7 @@ const Content = ({ ) } - return + return } const Lessons = ({ data }: GetAppProps) => { From 282267bc01cd52a0bde7f8894d1457f6570ce77b Mon Sep 17 00:00:00 2001 From: Flacial Date: Mon, 22 Aug 2022 04:16:56 +0400 Subject: [PATCH 03/16] refactor: Remove comments --- pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx index 417c1c29a..18ccdd98f 100644 --- a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx +++ b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx @@ -60,12 +60,6 @@ const IntroductionPage = ({ lesson }: { lesson: Lesson }) => { getPropertyArr(lesson, ['challenges', '__typename']) ) - // Update the Inputs values when the lesson changes - // useEffect( - // () => setFormOptions(getPropertyArr(lesson, ['challenges', '__typename'])), - // [lesson] - // ) - const handleChange = async (value: string, propertyIndex: number) => { await formChange( value, From f7be4769eda010abcddfe3c1633ab69c44c52bce Mon Sep 17 00:00:00 2001 From: Flacial Date: Mon, 22 Aug 2022 04:18:30 +0400 Subject: [PATCH 04/16] refactor: Add comment --- pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx index 18ccdd98f..f2884fb62 100644 --- a/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx +++ b/pages/admin/lessons/[lessonSlug]/[pageName]/index.tsx @@ -157,6 +157,7 @@ const Content = ({ ) } + // The "key" prop is passed so the component update its states (re-render and reset states) return } From bb5e6be417aebecdf9623740f394b2ff9ffaf305 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 15:57:42 +0000 Subject: [PATCH 05/16] docs: update README.md [skip ci] --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7d3fc67e..dc3f0b64f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # c0d3.com - -[![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-) - +[![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat-square)](#contributors-) ![CI](https://github.com/garageScript/c0d3.com/workflows/CI/badge.svg) @@ -72,6 +70,7 @@ Thanks goes to these wonderful people
Thomas Herzog

💻 📖
mino323

💻 📖
Flacial

💻 +
HS-90

💻 From 6b1530d8e1ef162d3f83fa34a53371f3b9129370 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 15:57:43 +0000 Subject: [PATCH 06/16] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 287168e5f..acbb2da5a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -201,6 +201,15 @@ "contributions": [ "code" ] + }, + { + "login": "HS-90", + "name": "HS-90", + "avatar_url": "https://avatars.githubusercontent.com/u/77421872?v=4", + "profile": "https://github.com/HS-90", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 From 9419e16afc51e1da3be6dccf839b607ada7de7b0 Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 8 Sep 2022 14:21:26 +0400 Subject: [PATCH 07/16] fix: Remove button background in focus state --- components/theme/Button.tsx | 8 ++++++-- scss/button.module.scss | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/components/theme/Button.tsx b/components/theme/Button.tsx index 11539aa22..682000f57 100644 --- a/components/theme/Button.tsx +++ b/components/theme/Button.tsx @@ -29,10 +29,14 @@ export const Button: React.FC = ({ const classes = ['btn'] if (border && !outline) classes.push('border') - if (!border) classes.push(styles['borderless']) + if (!border) classes.push(styles.borderless) if (type) { if (outline) - classes.push(`btn-outline-${type} ${styles[`btn-outline-bg-${type}`]}`) + classes.push( + `btn-outline-${type} ${styles.onFocusBgFix} ${ + styles[`btn-outline-bg-${type}`] + }` + ) else classes.push(`btn-${type}`) } if (m) classes.push(`m-${m}`) diff --git a/scss/button.module.scss b/scss/button.module.scss index 7b99171ef..505ee432f 100644 --- a/scss/button.module.scss +++ b/scss/button.module.scss @@ -5,6 +5,13 @@ border: 0; } +.onFocusBgFix { + &:focus, + &:active { + background-color: transparent; + } +} + @each $name, $color in variables.$colors { .btn-outline-bg-#{$name}:hover { background-color: rgba($color, 0.2); From b879a927613db9cc0c44b48dc4e0241956fb9f57 Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 8 Sep 2022 14:26:21 +0400 Subject: [PATCH 08/16] test: Update snapshots --- .../__snapshots__/storyshots.test.js.snap | 44 +++++++++---------- .../__snapshots__/username.test.js.snap | 2 +- .../__snapshots__/[lesson].test.js.snap | 8 ++-- .../ChallengeMaterial.test.js.snap | 2 +- .../__snapshots__/CopyButton.test.js.snap | 2 +- .../__snapshots__/DiffView.test.js.snap | 12 ++--- .../__snapshots__/ReviewCard.test.js.snap | 12 ++--- .../SubmissionComments.test.js.snap | 2 +- 8 files changed, 42 insertions(+), 42 deletions(-) diff --git a/__tests__/__snapshots__/storyshots.test.js.snap b/__tests__/__snapshots__/storyshots.test.js.snap index 56c73e1ae..0d8d0d9e5 100644 --- a/__tests__/__snapshots__/storyshots.test.js.snap +++ b/__tests__/__snapshots__/storyshots.test.js.snap @@ -281,7 +281,7 @@ exports[`Storyshots Components/AdminLessonExerciseCard Basic 1`] = ` noob@c0d3.com
diff --git a/__tests__/pages/review/__snapshots__/[lesson].test.js.snap b/__tests__/pages/review/__snapshots__/[lesson].test.js.snap index 352682b31..c24c78f7d 100644 --- a/__tests__/pages/review/__snapshots__/[lesson].test.js.snap +++ b/__tests__/pages/review/__snapshots__/[lesson].test.js.snap @@ -864,7 +864,7 @@ exports[`Lesson Page Should render new submissions 1`] = `