From 6cd8a025a7e37d5133cdaf945850977059d555cb Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Fri, 25 Jun 2021 10:22:50 -0500 Subject: [PATCH] feat(client): ts-migrate challenge-templates complete-modal (#42598) --- client/src/redux/prop-types.ts | 1 + .../src/templates/Challenges/classic/Show.tsx | 2 +- ...ap => completion-modal-body.test.tsx.snap} | 0 ...test.js => completion-modal-body.test.tsx} | 4 +- ...ModalBody.js => completion-modal-body.tsx} | 37 +++-- ...odal.test.js => completion-modal.test.tsx} | 2 +- ...ompletionModal.js => completion-modal.tsx} | 140 ++++++++++-------- .../Challenges/projects/backend/Show.tsx | 2 +- .../Challenges/projects/frontend/Show.tsx | 2 +- .../src/templates/Challenges/video/Show.tsx | 2 +- 10 files changed, 110 insertions(+), 82 deletions(-) rename client/src/templates/Challenges/components/__snapshots__/{CompletionModalBody.test.js.snap => completion-modal-body.test.tsx.snap} (100%) rename client/src/templates/Challenges/components/{CompletionModalBody.test.js => completion-modal-body.test.tsx} (89%) rename client/src/templates/Challenges/components/{CompletionModalBody.js => completion-modal-body.tsx} (76%) rename client/src/templates/Challenges/components/{CompletionModal.test.js => completion-modal.test.tsx} (95%) rename client/src/templates/Challenges/components/{CompletionModal.js => completion-modal.tsx} (68%) diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index d668274d87..3bf4cc7b1b 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -167,6 +167,7 @@ export type ChallengeNodeType = { guideUrl: string; head: string[]; helpCategory: string; + id: string; instructions: string; isComingSoon: boolean; removeComments: boolean; diff --git a/client/src/templates/Challenges/classic/Show.tsx b/client/src/templates/Challenges/classic/Show.tsx index 85dc114b50..b93e8200a5 100644 --- a/client/src/templates/Challenges/classic/Show.tsx +++ b/client/src/templates/Challenges/classic/Show.tsx @@ -17,7 +17,7 @@ import MultifileEditor from './MultifileEditor'; import Preview from '../components/Preview'; import SidePanel from '../components/Side-Panel'; import Output from '../components/Output'; -import CompletionModal from '../components/CompletionModal'; +import CompletionModal from '../components/completion-modal'; import HelpModal from '../components/HelpModal'; import VideoModal from '../components/VideoModal'; import ResetModal from '../components/ResetModal'; diff --git a/client/src/templates/Challenges/components/__snapshots__/CompletionModalBody.test.js.snap b/client/src/templates/Challenges/components/__snapshots__/completion-modal-body.test.tsx.snap similarity index 100% rename from client/src/templates/Challenges/components/__snapshots__/CompletionModalBody.test.js.snap rename to client/src/templates/Challenges/components/__snapshots__/completion-modal-body.test.tsx.snap diff --git a/client/src/templates/Challenges/components/CompletionModalBody.test.js b/client/src/templates/Challenges/components/completion-modal-body.test.tsx similarity index 89% rename from client/src/templates/Challenges/components/CompletionModalBody.test.js rename to client/src/templates/Challenges/components/completion-modal-body.test.tsx index 92827be419..2e69a480d1 100644 --- a/client/src/templates/Challenges/components/CompletionModalBody.test.js +++ b/client/src/templates/Challenges/components/completion-modal-body.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import CompletionModalBody from './CompletionModalBody'; +import CompletionModalBody from './completion-modal-body'; const props = { block: 'basic-html-and-html5', @@ -33,7 +33,7 @@ describe('', () => { const { container } = render(); fireEvent.animationEnd( - container.querySelector('.completion-success-icon') + container.querySelector('.completion-success-icon') as HTMLElement ); jest.runAllTimers(); diff --git a/client/src/templates/Challenges/components/CompletionModalBody.js b/client/src/templates/Challenges/components/completion-modal-body.tsx similarity index 76% rename from client/src/templates/Challenges/components/CompletionModalBody.js rename to client/src/templates/Challenges/components/completion-modal-body.tsx index 83847f0ca7..d01f2329c1 100644 --- a/client/src/templates/Challenges/components/CompletionModalBody.js +++ b/client/src/templates/Challenges/components/completion-modal-body.tsx @@ -1,18 +1,28 @@ import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import BezierEasing from 'bezier-easing'; import GreenPass from '../../../assets/icons/green-pass'; import { withTranslation } from 'react-i18next'; -const propTypes = { - block: PropTypes.string, - completedPercent: PropTypes.number, - superBlock: PropTypes.string, - t: PropTypes.func.isRequired -}; +interface CompletionModalBodyProps { + block: string; + completedPercent: number; + superBlock: string; + t: (arg0: string, arg1?: { percent: number }) => string; +} -export class CompletionModalBody extends PureComponent { - constructor(props) { +interface CompletionModalBodyState { + // This type was driving me nuts - seems like `NodeJS.Timeout | null;` should work + // eslint-disable-next-line @typescript-eslint/no-explicit-any + progressInterval: any; + shownPercent: number; +} + +export class CompletionModalBody extends PureComponent< + CompletionModalBodyProps, + CompletionModalBodyState +> { + static displayName: string; + constructor(props: CompletionModalBodyProps) { super(props); this.state = { @@ -23,7 +33,7 @@ export class CompletionModalBody extends PureComponent { this.animateProgressBar = this.animateProgressBar.bind(this); } - animateProgressBar(completedPercent) { + animateProgressBar(completedPercent: number): void { const easing = BezierEasing(0.2, 0.5, 0.4, 1); if (completedPercent > 100) completedPercent = 100; @@ -54,11 +64,11 @@ export class CompletionModalBody extends PureComponent { }); } - componentWillUnmount() { + componentWillUnmount(): void { clearInterval(this.state.progressInterval); } - render() { + render(): JSX.Element { const { block, completedPercent, superBlock, t } = this.props; const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`); @@ -84,7 +94,7 @@ export class CompletionModalBody extends PureComponent {
{t('learn.percent-complete', { @@ -100,6 +110,5 @@ export class CompletionModalBody extends PureComponent { } CompletionModalBody.displayName = 'CompletionModalBody'; -CompletionModalBody.propTypes = propTypes; export default withTranslation()(CompletionModalBody); diff --git a/client/src/templates/Challenges/components/CompletionModal.test.js b/client/src/templates/Challenges/components/completion-modal.test.tsx similarity index 95% rename from client/src/templates/Challenges/components/CompletionModal.test.js rename to client/src/templates/Challenges/components/completion-modal.test.tsx index fd78bc09f6..8e6c76a760 100644 --- a/client/src/templates/Challenges/components/CompletionModal.test.js +++ b/client/src/templates/Challenges/components/completion-modal.test.tsx @@ -1,4 +1,4 @@ -import { getCompletedPercent } from './CompletionModal'; +import { getCompletedPercent } from './completion-modal'; jest.mock('../../../analytics'); diff --git a/client/src/templates/Challenges/components/CompletionModal.js b/client/src/templates/Challenges/components/completion-modal.tsx similarity index 68% rename from client/src/templates/Challenges/components/CompletionModal.js rename to client/src/templates/Challenges/components/completion-modal.tsx index 4862d89dd6..24d2842f17 100644 --- a/client/src/templates/Challenges/components/CompletionModal.js +++ b/client/src/templates/Challenges/components/completion-modal.tsx @@ -1,15 +1,18 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { noop } from 'lodash-es'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { Button, Modal } from '@freecodecamp/react-bootstrap'; import { useStaticQuery, graphql } from 'gatsby'; import { withTranslation } from 'react-i18next'; +import { Dispatch } from 'redux'; import Login from '../../../components/Header/components/Login'; -import CompletionModalBody from './CompletionModalBody'; +import CompletionModalBody from './completion-modal-body'; import { dasherize } from '../../../../../utils/slugs'; +import { AllChallengeNodeType } from '../../../redux/prop-types'; import './completion-modal.css'; @@ -37,12 +40,12 @@ const mapStateToProps = createSelector( isSignedInSelector, successMessageSelector, ( - files, - { title, id }, - completedChallengesIds, - isOpen, - isSignedIn, - message + files: Record, + { title, id }: { title: string; id: string }, + completedChallengesIds: string[], + isOpen: boolean, + isSignedIn: boolean, + message: string ) => ({ files, title, @@ -54,13 +57,13 @@ const mapStateToProps = createSelector( }) ); -const mapDispatchToProps = function (dispatch) { +const mapDispatchToProps = function (dispatch: Dispatch) { const dispatchers = { close: () => dispatch(closeModal('completion')), submitChallenge: () => { dispatch(submitChallenge()); }, - allowBlockDonationRequests: block => { + allowBlockDonationRequests: (block: string) => { dispatch(allowBlockDonationRequests(block)); }, executeGA @@ -68,30 +71,11 @@ const mapDispatchToProps = function (dispatch) { return () => dispatchers; }; -const propTypes = { - allowBlockDonationRequests: PropTypes.func, - block: PropTypes.string, - blockName: PropTypes.string, - close: PropTypes.func.isRequired, - completedChallengesIds: PropTypes.array, - currentBlockIds: PropTypes.array, - executeGA: PropTypes.func, - files: PropTypes.object.isRequired, - id: PropTypes.string, - isOpen: PropTypes.bool, - isSignedIn: PropTypes.bool.isRequired, - message: PropTypes.string, - submitChallenge: PropTypes.func.isRequired, - superBlock: PropTypes.string, - t: PropTypes.func.isRequired, - title: PropTypes.string -}; - export function getCompletedPercent( - completedChallengesIds = [], - currentBlockIds = [], - currentChallengeId -) { + completedChallengesIds: string[] = [], + currentBlockIds: string[] = [], + currentChallengeId: string +): number { completedChallengesIds = completedChallengesIds.includes(currentChallengeId) ? completedChallengesIds : [...completedChallengesIds, currentChallengeId]; @@ -107,36 +91,70 @@ export function getCompletedPercent( return completedPercent > 100 ? 100 : completedPercent; } -export class CompletionModalInner extends Component { - constructor(props) { +interface CompletionModalsProps { + allowBlockDonationRequests: (arg0: string) => void; + block: string; + blockName: string; + close: () => void; + completedChallengesIds: string[]; + currentBlockIds?: string[]; + executeGA: () => void; + files: Record; + id: string; + isOpen: boolean; + isSignedIn: boolean; + message: string; + submitChallenge: () => void; + superBlock: string; + t: (arg0: string) => string; + title: string; +} + +interface CompletionModalInnerState { + downloadURL: null | string; + completedPercent: number; +} + +export class CompletionModalInner extends Component< + CompletionModalsProps, + CompletionModalInnerState +> { + constructor(props: CompletionModalsProps) { super(props); this.handleSubmit = this.handleSubmit.bind(this); this.handleKeypress = this.handleKeypress.bind(this); + + this.state = { + downloadURL: null, + completedPercent: 0 + }; } - state = { - downloadURL: null, - completedPercent: 0 - }; - - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps( + props: CompletionModalsProps, + state: CompletionModalInnerState + ): CompletionModalInnerState { const { files, isOpen } = props; if (!isOpen) { - return null; + return { downloadURL: null, completedPercent: 0 }; } const { downloadURL } = state; if (downloadURL) { URL.revokeObjectURL(downloadURL); } let newURL = null; - if (Object.keys(files).length) { - const filesForDownload = Object.keys(files) + const fileKeys = Object.keys(files); + if (fileKeys.length) { + const filesForDownload = fileKeys .map(key => files[key]) - .reduce((allFiles, { path, contents }) => { - const beforeText = `** start of ${path} **\n\n`; - const afterText = `\n\n** end of ${path} **\n\n`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .reduce((allFiles, currentFile: any) => { + const beforeText = `** start of ${currentFile.path} **\n\n`; + const afterText = `\n\n** end of ${currentFile.path} **\n\n`; allFiles += - files.length > 1 ? beforeText + contents + afterText : contents; + fileKeys.length > 1 + ? `${beforeText}${currentFile.contents}${afterText}` + : currentFile.contents; return allFiles; }, ''); const blob = new Blob([filesForDownload], { @@ -146,13 +164,13 @@ export class CompletionModalInner extends Component { } const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props; - let completedPercent = isSignedIn + const completedPercent = isSignedIn ? getCompletedPercent(completedChallengesIds, currentBlockIds, id) : 0; return { downloadURL: newURL, completedPercent: completedPercent }; } - handleKeypress(e) { + handleKeypress(e: React.KeyboardEvent): void { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { e.preventDefault(); // Since Hotkeys also listens to Ctrl + Enter we have to stop this event @@ -162,13 +180,13 @@ export class CompletionModalInner extends Component { } } - handleSubmit() { + handleSubmit(): void { this.props.submitChallenge(); this.checkBlockCompletion(); } // check block completion for donation - checkBlockCompletion() { + checkBlockCompletion(): void { if ( this.state.completedPercent === 100 && !this.props.completedChallengesIds.includes(this.props.id) @@ -177,14 +195,14 @@ export class CompletionModalInner extends Component { } } - componentWillUnmount() { + componentWillUnmount(): void { if (this.state.downloadURL) { URL.revokeObjectURL(this.state.downloadURL); } this.props.close(); } - render() { + render(): JSX.Element { const { block, close, @@ -212,6 +230,7 @@ export class CompletionModalInner extends Component { dialogClassName='challenge-success-modal' keyboard={true} onHide={close} + // eslint-disable-next-line @typescript-eslint/unbound-method onKeyDown={isOpen ? this.handleKeypress : noop} show={isOpen} > @@ -236,7 +255,7 @@ export class CompletionModalInner extends Component { block={true} bsSize='large' bsStyle='primary' - onClick={this.handleSubmit} + onClick={() => this.handleSubmit()} > {isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')} (Ctrl + Enter) @@ -259,12 +278,10 @@ export class CompletionModalInner extends Component { } } -CompletionModalInner.propTypes = propTypes; - -const useCurrentBlockIds = blockName => { +const useCurrentBlockIds = (blockName: string) => { const { allChallengeNode: { edges } - } = useStaticQuery(graphql` + }: { allChallengeNode: AllChallengeNodeType } = useStaticQuery(graphql` query getCurrentBlockNodes { allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) { edges { @@ -281,17 +298,18 @@ const useCurrentBlockIds = blockName => { const currentBlockIds = edges .filter(edge => edge.node.fields.blockName === blockName) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return .map(edge => edge.node.id); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return currentBlockIds; }; -const CompletionModal = props => { +const CompletionModal = (props: CompletionModalsProps) => { const currentBlockIds = useCurrentBlockIds(props.blockName || ''); return ; }; CompletionModal.displayName = 'CompletionModal'; -CompletionModal.propTypes = propTypes; export default connect( mapStateToProps, diff --git a/client/src/templates/Challenges/projects/backend/Show.tsx b/client/src/templates/Challenges/projects/backend/Show.tsx index 8f6f02b55d..13e301f74e 100644 --- a/client/src/templates/Challenges/projects/backend/Show.tsx +++ b/client/src/templates/Challenges/projects/backend/Show.tsx @@ -27,7 +27,7 @@ import ChallengeTitle from '../../components/challenge-title'; import ChallengeDescription from '../../components/Challenge-Description'; import TestSuite from '../../components/Test-Suite'; import Output from '../../components/Output'; -import CompletionModal from '../../components/CompletionModal'; +import CompletionModal from '../../components/completion-modal'; import HelpModal from '../../components/HelpModal'; import ProjectToolPanel from '../Tool-Panel'; import SolutionForm from '../SolutionForm'; diff --git a/client/src/templates/Challenges/projects/frontend/Show.tsx b/client/src/templates/Challenges/projects/frontend/Show.tsx index b374b34929..728495955c 100644 --- a/client/src/templates/Challenges/projects/frontend/Show.tsx +++ b/client/src/templates/Challenges/projects/frontend/Show.tsx @@ -30,7 +30,7 @@ import ChallengeDescription from '../../components/Challenge-Description'; import Spacer from '../../../../components/helpers/spacer'; import SolutionForm from '../SolutionForm'; import ProjectToolPanel from '../Tool-Panel'; -import CompletionModal from '../../components/CompletionModal'; +import CompletionModal from '../../components/completion-modal'; import HelpModal from '../../components/HelpModal'; import Hotkeys from '../../components/Hotkeys'; diff --git a/client/src/templates/Challenges/video/Show.tsx b/client/src/templates/Challenges/video/Show.tsx index 8fdc9a4ebe..1000a64984 100644 --- a/client/src/templates/Challenges/video/Show.tsx +++ b/client/src/templates/Challenges/video/Show.tsx @@ -21,7 +21,7 @@ import LearnLayout from '../../../components/layouts/Learn'; import ChallengeTitle from '../components/challenge-title'; import ChallengeDescription from '../components/Challenge-Description'; import Spacer from '../../../components/helpers/spacer'; -import CompletionModal from '../components/CompletionModal'; +import CompletionModal from '../components/completion-modal'; import Hotkeys from '../components/Hotkeys'; import Loader from '../../../components/helpers/loader'; import {