feat(client): ts-migrate multiple files (#43262)

* feat(client): ts-migrate rename files

* feat(client): ts-migrate client/src/templates/Introduction/*

* feat(client): ts-migrate client/src/components/formHelpers/form*

* fix: import

* Update client/src/components/formHelpers/form-validators.tsx

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>

* Update client/src/components/formHelpers/form-fields.tsx

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>

* Update client/src/components/formHelpers/form-fields.tsx

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>

* fix: types in client/src/components/formHelpers/index.tsx

* fix: types in client/src/templates/Introduction/super-block-intro.tsx

* fix: types in client/src/components/formHelpers/*

* fix: signInLoading and value types

* Update client/src/templates/Introduction/super-block-intro.tsx

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* Update client/src/components/formHelpers/index.tsx

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* Update client/src/components/formHelpers/index.tsx

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* Update client/src/components/formHelpers/index.tsx

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* chore(deps): update dependency rollup to v2.58.1

* fix: rename variables and fix imports for ts-migrate

* fix: remove `Type` suffix from the type definition.

* Update client/src/components/formHelpers/form.tsx

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Renovate Bot <bot@renovateapp.com>
This commit is contained in:
Vishwasa Navada K
2021-10-25 23:15:36 +05:30
committed by GitHub
parent 001aa3ea9e
commit 9abc5f66ba
13 changed files with 254 additions and 215 deletions

View File

@ -40,7 +40,7 @@ const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
);
type DonateModalProps = {
activeDonors: number;
activeDonors?: number;
closeDonationModal: typeof closeDonationModal;
executeGA: typeof executeGA;
location: WindowLocation | undefined;

View File

@ -1,75 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form } from 'react-final-form';
import {
FormFields,
BlockSaveButton,
BlockSaveWrapper,
formatUrlValues
} from './';
const propTypes = {
buttonText: PropTypes.string,
enableSubmit: PropTypes.bool,
formFields: PropTypes.arrayOf(
PropTypes.shape({ name: PropTypes.string, label: PropTypes.string })
.isRequired
).isRequired,
hideButton: PropTypes.bool,
id: PropTypes.string.isRequired,
initialValues: PropTypes.object,
options: PropTypes.shape({
ignored: PropTypes.arrayOf(PropTypes.string),
isEditorLinkAllowed: PropTypes.bool,
required: PropTypes.arrayOf(PropTypes.string),
types: PropTypes.objectOf(PropTypes.string),
placeholders: PropTypes.shape({
solution: PropTypes.string,
githubLink: PropTypes.string
})
}),
submit: PropTypes.func.isRequired
};
function DynamicForm({
id,
formFields,
initialValues,
options,
submit,
buttonText,
enableSubmit,
hideButton
}) {
return (
<Form
initialValues={initialValues}
onSubmit={(values, ...args) =>
submit(formatUrlValues(values, options), ...args)
}
>
{({ handleSubmit, pristine, error }) => (
<form
id={`dynamic-${id}`}
onSubmit={handleSubmit}
style={{ width: '100%' }}
>
<FormFields formFields={formFields} options={options} />
<BlockSaveWrapper>
{hideButton ? null : (
<BlockSaveButton disabled={(pristine && !enableSubmit) || error}>
{buttonText ? buttonText : null}
</BlockSaveButton>
)}
</BlockSaveWrapper>
</form>
)}
</Form>
);
}
DynamicForm.displayName = 'DynamicForm';
DynamicForm.propTypes = propTypes;
export default DynamicForm;

View File

@ -1,7 +1,7 @@
import { render, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import Form from './Form';
import Form from './form';
const defaultTestProps = {
buttonText: 'Submit',

View File

@ -7,7 +7,7 @@ const style = {
function BlockSaveWrapper({
children
}: {
children?: React.ReactElement;
children?: React.ReactElement | null;
}): JSX.Element {
return <div style={style}>{children}</div>;
}

View File

@ -8,35 +8,26 @@ import {
} from '@freecodecamp/react-bootstrap';
import { kebabCase } from 'lodash-es';
import normalizeUrl from 'normalize-url';
import PropTypes from 'prop-types';
import React from 'react';
import { Field } from 'react-final-form';
import { useTranslation } from 'react-i18next';
import { FormOptions } from './form';
import {
editorValidator,
localhostValidator,
composeValidators,
fCCValidator,
httpValidator
} from './FormValidators';
} from './form-validators';
const propTypes = {
formFields: PropTypes.arrayOf(
PropTypes.shape({ name: PropTypes.string, label: PropTypes.string })
.isRequired
).isRequired,
options: PropTypes.shape({
ignored: PropTypes.arrayOf(PropTypes.string),
isEditorLinkAllowed: PropTypes.bool,
placeholders: PropTypes.objectOf(PropTypes.string),
required: PropTypes.arrayOf(PropTypes.string),
types: PropTypes.objectOf(PropTypes.string)
})
type FormFieldsProps = {
formFields: { name: string; label: string }[];
options: FormOptions;
};
function FormFields(props) {
function FormFields(props: FormFieldsProps): JSX.Element {
const { t } = useTranslation();
const { formFields, options = {} } = props;
const { formFields, options = {} }: FormFieldsProps = props;
const {
ignored = [],
placeholders = {},
@ -46,13 +37,18 @@ function FormFields(props) {
isLocalLinkAllowed = false
} = options;
const nullOrWarning = (value, error, isURL, name) => {
let validationError;
const nullOrWarning = (
value: string,
error: unknown,
isURL: boolean,
name: string
) => {
let validationError: string | undefined;
if (value && isURL) {
try {
normalizeUrl(value, { stripWWW: false });
} catch (err) {
validationError = err.message;
} catch (err: unknown) {
validationError = (err as { message?: string })?.message;
}
}
const validationWarning = composeValidators(
@ -61,7 +57,9 @@ function FormFields(props) {
httpValidator,
isLocalLinkAllowed ? null : localhostValidator
)(value);
const message = error || validationError || validationWarning;
const message: string = (error ||
validationError ||
validationWarning) as string;
return message ? (
<HelpBlock>
<Alert
@ -78,6 +76,7 @@ function FormFields(props) {
{formFields
.filter(formField => !ignored.includes(formField.name))
.map(({ name, label }) => (
// TODO: verify if the value is always a string
<Field key={`${kebabCase(name)}-field`} name={name}>
{({ input: { value, onChange }, meta: { pristine, error } }) => {
const key = kebabCase(name);
@ -100,7 +99,7 @@ function FormFields(props) {
required={required.includes(name)}
rows={4}
type={type}
value={value}
value={value as string}
/>
{nullOrWarning(value, !pristine && error, isURL, name)}
</FormGroup>
@ -114,6 +113,5 @@ function FormFields(props) {
}
FormFields.displayName = 'FormFields';
FormFields.propTypes = propTypes;
export default FormFields;

View File

@ -9,21 +9,26 @@ const fCCRegex =
const localhostRegex = /localhost:/;
const httpRegex = /http(?!s|([^s]+?localhost))/;
export const editorValidator = value =>
export const editorValidator = (value: string): React.ReactElement | null =>
editorRegex.test(value) ? <Trans>validation.editor-url</Trans> : null;
export const fCCValidator = value =>
export const fCCValidator = (value: string): React.ReactElement | null =>
fCCRegex.test(value) ? <Trans>validation.own-work-url</Trans> : null;
export const localhostValidator = value =>
export const localhostValidator = (value: string): React.ReactElement | null =>
localhostRegex.test(value) ? (
<Trans>validation.publicly-visible-url</Trans>
) : null;
export const httpValidator = value =>
export const httpValidator = (value: string): React.ReactElement | null =>
httpRegex.test(value) ? <Trans>validation.http-url</Trans> : null;
export const composeValidators =
(...validators) =>
value =>
validators.reduce((error, validator) => error ?? validator?.(value), null);
export type Validator = (value: string) => React.ReactElement | null;
export function composeValidators(...validators: (Validator | null)[]) {
return (value: string): ReturnType<Validator> | null =>
validators.reduce(
(error: ReturnType<Validator>, validator) =>
error ?? (validator ? validator(value) : null),
null
);
}

View File

@ -0,0 +1,75 @@
import React from 'react';
import { Form } from 'react-final-form';
import {
URLValues,
ValidatedValues,
FormFields,
BlockSaveButton,
BlockSaveWrapper,
formatUrlValues
} from '../formHelpers/index';
export type FormOptions = {
ignored?: string[];
isEditorLinkAllowed?: boolean;
isLocalLinkAllowed?: boolean;
required?: string[];
types?: { [key: string]: string };
placeholders?: { [key: string]: string };
};
type FormProps = {
buttonText?: string;
enableSubmit?: boolean;
formFields: { name: string; label: string }[];
hideButton?: boolean;
id?: string;
initialValues?: Record<string, unknown>;
options: FormOptions;
submit: (values: ValidatedValues, ...args: unknown[]) => void;
};
function DynamicForm({
id,
formFields,
initialValues,
options,
submit,
buttonText,
enableSubmit,
hideButton
}: FormProps): JSX.Element {
return (
<Form
initialValues={initialValues}
onSubmit={(values: URLValues, ...args: unknown[]) => {
submit(formatUrlValues(values, options), ...args);
}}
>
{({ handleSubmit, pristine, error }) => (
<form
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
id={`dynamic-${id}`}
onSubmit={handleSubmit}
style={{ width: '100%' }}
>
<FormFields formFields={formFields} options={options} />
<BlockSaveWrapper>
{hideButton ? null : (
<BlockSaveButton
disabled={(pristine && !enableSubmit) || (error as boolean)}
>
{buttonText ? buttonText : null}
</BlockSaveButton>
)}
</BlockSaveWrapper>
</form>
)}
</Form>
);
}
DynamicForm.displayName = 'DynamicForm';
export default DynamicForm;

View File

@ -1,45 +0,0 @@
import normalizeUrl from 'normalize-url';
import {
localhostValidator,
editorValidator,
composeValidators,
fCCValidator,
httpValidator
} from './FormValidators';
export { default as BlockSaveButton } from './block-save-button';
export { default as BlockSaveWrapper } from './block-save-wrapper';
export { default as Form } from './Form.js';
export { default as FormFields } from './FormFields.js';
const normalizeOptions = {
stripWWW: false
};
export function formatUrlValues(values, options) {
const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options;
const validatedValues = { values: {}, errors: [], invalidValues: [] };
const urlValues = Object.keys(values).reduce((result, key) => {
let value = values[key];
const nullOrWarning = composeValidators(
fCCValidator,
httpValidator,
isLocalLinkAllowed ? null : localhostValidator,
key === 'githubLink' || isEditorLinkAllowed ? null : editorValidator
)(value);
if (nullOrWarning) {
validatedValues.invalidValues.push(nullOrWarning);
}
if (value && types[key] === 'url') {
try {
value = normalizeUrl(value, normalizeOptions);
} catch (err) {
// Not a valid URL for testing or submission
validatedValues.errors.push({ error: err, value });
}
}
return { ...result, [key]: value };
}, {});
validatedValues.values = urlValues;
return validatedValues;
}

View File

@ -0,0 +1,70 @@
import normalizeUrl from 'normalize-url';
import { FormOptions } from './form';
import {
localhostValidator,
editorValidator,
composeValidators,
fCCValidator,
httpValidator
} from './form-validators';
export { default as BlockSaveButton } from './block-save-button';
export { default as BlockSaveWrapper } from './block-save-wrapper';
export { default as Form } from './form';
export { default as FormFields } from './form-fields';
const normalizeOptions = {
stripWWW: false
};
export type URLValues = {
[key: string]: string;
};
type ValidationError = {
error: { message?: string };
value: string;
};
export type ValidatedValues = {
values: URLValues;
errors: ValidationError[];
invalidValues: (JSX.Element | null)[];
};
export function formatUrlValues(
values: URLValues,
options: FormOptions
): ValidatedValues {
const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options;
const validatedValues: ValidatedValues = {
values: {},
errors: [],
invalidValues: []
};
const urlValues = Object.keys(values).reduce((result, key: string) => {
let value: string = values[key];
const nullOrWarning: JSX.Element | null = composeValidators(
fCCValidator,
httpValidator,
isLocalLinkAllowed ? null : localhostValidator,
key === 'githubLink' || isEditorLinkAllowed ? null : editorValidator
)(value);
if (nullOrWarning) {
validatedValues.invalidValues.push(nullOrWarning);
}
if (value && types && types[key] === 'url') {
try {
value = normalizeUrl(value, normalizeOptions);
} catch (err: unknown) {
validatedValues.errors.push({
error: err as { message?: string },
value
});
}
}
return { ...result, [key]: value };
}, {});
validatedValues.values = urlValues;
return validatedValues;
}

View File

@ -8,7 +8,7 @@ import {
frontEndProject,
pythonProject
} from '../../../../utils/challenge-types';
import { Form } from '../../../components/formHelpers';
import { Form, ValidatedValues } from '../../../components/formHelpers';
interface SubmitProps {
showCompletionModal: boolean;
@ -21,12 +21,6 @@ interface FormProps extends WithTranslation {
updateSolutionForm: (arg0: Record<string, unknown>) => void;
}
interface ValidatedValues {
errors: string[];
invalidValues: string[];
values: Record<string, unknown>;
}
export class SolutionForm extends Component<FormProps> {
constructor(props: FormProps) {
super(props);

View File

@ -1,6 +1,5 @@
import { Grid, ListGroup, ListGroupItem } from '@freecodecamp/react-bootstrap';
import { Link, graphql } from 'gatsby';
import PropTypes from 'prop-types';
import React from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
@ -8,18 +7,19 @@ import { useTranslation } from 'react-i18next';
import ButtonSpacer from '../../components/helpers/button-spacer';
import FullWidthRow from '../../components/helpers/full-width-row';
import LearnLayout from '../../components/layouts/learn';
import { MarkdownRemark, AllChallengeNode } from '../../redux/prop-types';
import {
MarkdownRemarkType,
AllChallengeNodeType,
ChallengeNodeType
} from '../../redux/prop-types';
import './intro.css';
const propTypes = {
data: PropTypes.shape({
markdownRemark: MarkdownRemark,
allChallengeNode: AllChallengeNode
})
};
function renderMenuItems({ edges = [] }) {
function renderMenuItems({
edges = []
}: {
edges?: Array<{ node: ChallengeNodeType }>;
}) {
return edges
.map(({ node }) => node)
.map(({ title, fields: { slug } }) => (
@ -29,7 +29,14 @@ function renderMenuItems({ edges = [] }) {
));
}
function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
function IntroductionPage({
data: { markdownRemark, allChallengeNode }
}: {
data: {
markdownRemark: MarkdownRemarkType;
allChallengeNode: AllChallengeNodeType;
};
}): React.FunctionComponentElement<typeof LearnLayout> {
const { t } = useTranslation();
const {
html,
@ -78,7 +85,6 @@ function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
}
IntroductionPage.displayName = 'IntroductionPage';
IntroductionPage.propTypes = propTypes;
export default IntroductionPage;

View File

@ -1,16 +1,16 @@
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
import { WindowLocation } from '@reach/router';
import { graphql } from 'gatsby';
import { uniq } from 'lodash-es';
import PropTypes from 'prop-types';
import React, { Fragment, useEffect, memo } from 'react';
import Helmet from 'react-helmet';
import { withTranslation } from 'react-i18next';
import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { configureAnchors } from 'react-scrollable-anchor';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
import DonateModal from '../../../../client/src/components/Donation/DonationModal';
import DonateModal from '../../components/Donation/DonationModal';
import Login from '../../components/Header/components/Login';
import Map from '../../components/Map';
import { Spacer } from '../../components/helpers';
@ -22,7 +22,11 @@ import {
tryToShowDonationModal,
userSelector
} from '../../redux';
import { MarkdownRemark, AllChallengeNode, User } from '../../redux/prop-types';
import {
MarkdownRemarkType,
AllChallengeNodeType,
UserType
} from '../../redux/prop-types';
import Block from './components/Block';
import CertChallenge from './components/CertChallenge';
import SuperBlockIntro from './components/SuperBlockIntro';
@ -30,44 +34,48 @@ import { resetExpansion, toggleBlock } from './redux';
import './intro.css';
const propTypes = {
currentChallengeId: PropTypes.string,
data: PropTypes.shape({
markdownRemark: MarkdownRemark,
allChallengeNode: AllChallengeNode
}),
expandedState: PropTypes.object,
fetchState: PropTypes.shape({
pending: PropTypes.bool,
complete: PropTypes.bool,
errored: PropTypes.bool
}),
isSignedIn: PropTypes.bool,
location: PropTypes.shape({
hash: PropTypes.string,
// TODO: state is sometimes a string
state: PropTypes.shape({
breadcrumbBlockClick: PropTypes.string
})
}),
resetExpansion: PropTypes.func,
signInLoading: PropTypes.bool,
t: PropTypes.func,
toggleBlock: PropTypes.func,
tryToShowDonationModal: PropTypes.func.isRequired,
user: User
type FetchState = {
pending: boolean;
complete: boolean;
errored: boolean;
};
type SuperBlockProp = {
currentChallengeId: string;
data: {
markdownRemark: MarkdownRemarkType;
allChallengeNode: AllChallengeNodeType;
};
expandedState: {
[key: string]: boolean;
};
fetchState: FetchState;
isSignedIn: boolean;
signInLoading: boolean;
location: WindowLocation<{ breadcrumbBlockClick: string }>;
resetExpansion: () => void;
t: TFunction;
toggleBlock: (arg0: string) => void;
tryToShowDonationModal: () => void;
user: UserType;
};
configureAnchors({ offset: -40, scrollDuration: 0 });
const mapStateToProps = state => {
const mapStateToProps = (state: unknown) => {
return createSelector(
currentChallengeIdSelector,
isSignedInSelector,
signInLoadingSelector,
userFetchStateSelector,
userSelector,
(currentChallengeId, isSignedIn, signInLoading, fetchState, user) => ({
(
currentChallengeId: string,
isSignedIn,
signInLoading: boolean,
fetchState: FetchState,
user: UserType
) => ({
currentChallengeId,
isSignedIn,
signInLoading,
@ -77,7 +85,7 @@ const mapStateToProps = state => {
)(state);
};
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
tryToShowDonationModal,
@ -87,7 +95,7 @@ const mapDispatchToProps = dispatch =>
dispatch
);
const SuperBlockIntroductionPage = props => {
const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
useEffect(() => {
initializeExpandedState();
props.tryToShowDonationModal();
@ -102,7 +110,7 @@ const SuperBlockIntroductionPage = props => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getChosenBlock = () => {
const getChosenBlock = (): string => {
const {
data: {
allChallengeNode: { edges }
@ -110,10 +118,14 @@ const SuperBlockIntroductionPage = props => {
isSignedIn,
currentChallengeId,
location
} = props;
}: SuperBlockProp = props;
// if coming from breadcrumb click
if (location.state && location.state.breadcrumbBlockClick) {
if (
location.state &&
typeof location.state === 'object' &&
location.state.hasOwnProperty('breadcrumbBlockClick')
) {
return location.state.breadcrumbBlockClick;
}
@ -123,7 +135,7 @@ const SuperBlockIntroductionPage = props => {
return dashedBlock;
}
let edge = edges[0];
const edge = edges[0];
if (isSignedIn) {
// see if currentChallenge is in this superBlock
@ -232,7 +244,6 @@ const SuperBlockIntroductionPage = props => {
};
SuperBlockIntroductionPage.displayName = 'SuperBlockIntroductionPage';
SuperBlockIntroductionPage.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -21,11 +21,11 @@ const codeally = path.resolve(
);
const intro = path.resolve(
__dirname,
'../../src/templates/Introduction/Intro.js'
'../../src/templates/Introduction/intro.tsx'
);
const superBlockIntro = path.resolve(
__dirname,
'../../src/templates/Introduction/SuperBlockIntro.js'
'../../src/templates/Introduction/super-block-intro.tsx'
);
const video = path.resolve(
__dirname,