Implemented strict mode for the project (broken tests, hundreds of @ts-ignore and new errors are included) [skip ci]

This commit is contained in:
ErickSkrauch
2020-01-17 23:37:52 +03:00
committed by SleepWalker
parent 10e8b77acf
commit 96049ad4ad
151 changed files with 2470 additions and 1869 deletions

View File

@@ -1,4 +1,4 @@
import React, { InputHTMLAttributes } from 'react';
import React, { InputHTMLAttributes, MouseEventHandler } from 'react';
import ReactDOM from 'react-dom';
import { MessageDescriptor } from 'react-intl';
import clsx from 'clsx';
@@ -40,10 +40,12 @@ export default class Dropdown extends FormInputComponent<Props, State> {
componentDidMount() {
// listen to capturing phase to ensure, that our event handler will be
// called before all other
// @ts-ignore
document.addEventListener('click', this.onBodyClick, true);
}
componentWillUnmount() {
// @ts-ignore
document.removeEventListener('click', this.onBodyClick);
}
@@ -98,7 +100,7 @@ export default class Dropdown extends FormInputComponent<Props, State> {
});
}
onSelectItem(item: OptionItem) {
onSelectItem(item: OptionItem): MouseEventHandler {
return event => {
event.preventDefault();
@@ -141,11 +143,12 @@ export default class Dropdown extends FormInputComponent<Props, State> {
this.toggle();
};
onBodyClick = (event: MouseEvent) => {
onBodyClick: MouseEventHandler = event => {
if (this.state.isActive) {
const el = ReactDOM.findDOMNode(this);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const el = ReactDOM.findDOMNode(this)!;
if (!el.contains(event.target) && el !== event.target) {
if (!el.contains(event.target as HTMLElement) && el !== event.target) {
event.preventDefault();
event.stopPropagation();

View File

@@ -1,18 +1,33 @@
import React from 'react';
import clsx from 'clsx';
import logger from 'app/services/logger';
import FormModel from './FormModel';
import styles from './form.scss';
interface Props {
interface BaseProps {
id: string;
isLoading: boolean;
form?: FormModel;
onSubmit: (form: FormModel | FormData) => void | Promise<void>;
onInvalid: (errors: { [errorKey: string]: string }) => void;
onInvalid: (errors: Record<string, string>) => void;
children: React.ReactNode;
}
interface PropsWithoutForm extends BaseProps {
onSubmit: (form: FormData) => Promise<void> | void;
}
interface PropsWithForm extends BaseProps {
form: FormModel;
onSubmit: (form: FormModel) => Promise<void> | void;
}
type Props = PropsWithoutForm | PropsWithForm;
function hasForm(props: Props): props is PropsWithForm {
return 'form' in props;
}
interface State {
id: string; // just to track value for derived updates
isTouched: boolean;
@@ -39,7 +54,7 @@ export default class Form extends React.Component<Props, State> {
mounted = false;
componentDidMount() {
if (this.props.form) {
if (hasForm(this.props)) {
this.props.form.addLoadingListener(this.onLoading);
}
@@ -65,8 +80,8 @@ export default class Form extends React.Component<Props, State> {
}
componentDidUpdate(prevProps: Props) {
const nextForm = this.props.form;
const prevForm = prevProps.form;
const nextForm = hasForm(this.props) ? this.props.form : undefined;
const prevForm = hasForm(prevProps) ? prevProps.form : undefined;
if (nextForm !== prevForm) {
if (prevForm) {
@@ -80,7 +95,7 @@ export default class Form extends React.Component<Props, State> {
}
componentWillUnmount() {
if (this.props.form) {
if (hasForm(this.props)) {
this.props.form.removeLoadingListener(this.onLoading);
}
@@ -119,15 +134,19 @@ export default class Form extends React.Component<Props, State> {
}
if (form.checkValidity()) {
const result = this.props.onSubmit(
this.props.form ? this.props.form : new FormData(form),
);
let result: Promise<void> | void;
if (hasForm(this.props)) {
result = this.props.onSubmit(this.props.form);
} else {
result = this.props.onSubmit(new FormData(form));
}
if (result && result.then) {
this.setState({ isLoading: true });
result
.catch((errors: { [key: string]: string }) => {
.catch((errors: Record<string, string>) => {
this.setErrors(errors);
})
.finally(() => this.mounted && this.setState({ isLoading: false }));
@@ -136,10 +155,10 @@ export default class Form extends React.Component<Props, State> {
const invalidEls: NodeListOf<InputElement> = form.querySelectorAll(
':invalid',
);
const errors = {};
const errors: Record<string, string> = {};
invalidEls[0].focus(); // focus on first error
Array.from(invalidEls).reduce((acc, el: InputElement) => {
Array.from(invalidEls).reduce((acc, el) => {
if (!el.name) {
logger.warn('Found an element without name', { el });
@@ -164,7 +183,10 @@ export default class Form extends React.Component<Props, State> {
}
setErrors(errors: { [key: string]: string }) {
this.props.form && this.props.form.setErrors(errors);
if (hasForm(this.props)) {
this.props.form.setErrors(errors);
}
this.props.onInvalid(errors);
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { MessageDescriptor } from 'react-intl';
import i18n from 'app/services/i18n';
class FormComponent<P, S = {}> extends React.Component<P, S> {
export default class FormComponent<P, S = {}> extends React.Component<P, S> {
/**
* Formats message resolving intl translations
*
@@ -37,5 +37,3 @@ class FormComponent<P, S = {}> extends React.Component<P, S> {
*/
onFormInvalid() {}
}
export default FormComponent;

View File

@@ -1,31 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import errorsDict from 'app/services/errorsDict';
import React, { ComponentType, ReactNode } from 'react';
import { resolve as resolveError } from 'app/services/errorsDict';
import { MessageDescriptor } from 'react-intl';
import styles from './form.scss';
export default function FormError({
error,
}: {
error?:
| string
| React.ReactNode
| {
type: string;
payload: { [key: string]: any };
};
}) {
return error ? (
<div className={styles.fieldError}>{errorsDict.resolve(error)}</div>
) : null;
interface Props {
error?: Parameters<typeof resolveError>[0] | MessageDescriptor | null;
}
FormError.propTypes = {
error: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
type: PropTypes.string.isRequired,
payload: PropTypes.object.isRequired,
}),
]),
function isMessageDescriptor(
message: Props['error'],
): message is MessageDescriptor {
return (
typeof message === 'object' &&
typeof (message as MessageDescriptor).id !== 'undefined'
);
}
const FormError: ComponentType<Props> = ({ error }) => {
if (!error) {
return null;
}
let content: ReactNode;
if (isMessageDescriptor(error)) {
content = error;
} else {
content = resolveError(error);
}
return <div className={styles.fieldError}>{content}</div>;
};
export default FormError;

View File

@@ -6,15 +6,13 @@ export type ValidationError =
| string
| {
type: string;
payload?: { [key: string]: any };
payload?: Record<string, any>;
};
export default class FormModel {
fields = {};
errors: {
[fieldId: string]: ValidationError;
} = {};
handlers: LoadingListener[] = [];
fields: Record<string, any> = {};
errors: Record<string, ValidationError> = {};
handlers: Array<LoadingListener> = [];
renderErrors: boolean;
_isLoading: boolean;
@@ -27,7 +25,7 @@ export default class FormModel {
this.renderErrors = options.renderErrors !== false;
}
hasField(fieldId: string) {
hasField(fieldId: string): boolean {
return !!this.fields[fieldId];
}
@@ -83,7 +81,7 @@ export default class FormModel {
*
* @param {string} fieldId - an id of field to focus
*/
focus(fieldId: string) {
focus(fieldId: string): void {
if (!this.fields[fieldId]) {
throw new Error(
`Can not focus. The field with an id ${fieldId} does not exists`,
@@ -100,7 +98,7 @@ export default class FormModel {
*
* @returns {string}
*/
value(fieldId: string) {
value(fieldId: string): string {
const field = this.fields[fieldId];
if (!field) {
@@ -124,7 +122,7 @@ export default class FormModel {
*
* @param {object} errors - object maping {fieldId: errorType}
*/
setErrors(errors: { [key: string]: ValidationError }) {
setErrors(errors: Record<string, ValidationError>): void {
if (typeof errors !== 'object' || errors === null) {
throw new Error('Errors must be an object');
}
@@ -151,21 +149,11 @@ export default class FormModel {
return error || null;
}
/**
* Get error by id
*
* @param {string} fieldId - an id of field to get error for
*
* @returns {string|object|null}
*/
getError(fieldId: string) {
getError(fieldId: string): ValidationError | null {
return this.errors[fieldId] || null;
}
/**
* @returns {bool}
*/
hasErrors() {
hasErrors(): boolean {
return Object.keys(this.errors).length > 0;
}
@@ -174,7 +162,7 @@ export default class FormModel {
*
* @returns {object}
*/
serialize(): { [key: string]: any } {
serialize(): Record<string, any> {
return Object.keys(this.fields).reduce((acc, fieldId) => {
const field = this.fields[fieldId];
@@ -185,7 +173,7 @@ export default class FormModel {
}
return acc;
}, {});
}, {} as Record<string, any>);
}
/**
@@ -193,7 +181,7 @@ export default class FormModel {
*
* @param {Function} fn
*/
addLoadingListener(fn: LoadingListener) {
addLoadingListener(fn: LoadingListener): void {
this.removeLoadingListener(fn);
this.handlers.push(fn);
}
@@ -203,14 +191,14 @@ export default class FormModel {
*
* @param {Function} fn
*/
removeLoadingListener(fn: LoadingListener) {
removeLoadingListener(fn: LoadingListener): void {
this.handlers = this.handlers.filter(handler => handler !== fn);
}
/**
* Switch form in loading state
*/
beginLoading() {
beginLoading(): void {
this._isLoading = true;
this.notifyHandlers();
}
@@ -218,12 +206,12 @@ export default class FormModel {
/**
* Disable loading state
*/
endLoading() {
endLoading(): void {
this._isLoading = false;
this.notifyHandlers();
}
private notifyHandlers() {
private notifyHandlers(): void {
this.handlers.forEach(fn => fn(this._isLoading));
}
}

View File

@@ -22,7 +22,7 @@ describe('Input', () => {
);
expect(
wrapper.find('input[name="test"]').getDOMNode().value,
wrapper.find('input[name="test"]').getDOMNode<HTMLInputElement>().value,
'to equal',
'foo',
);

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { MessageDescriptor } from 'react-intl';
import TextareaAutosize from 'react-textarea-autosize';
import TextareaAutosize, {
TextareaAutosizeProps,
} from 'react-textarea-autosize';
import clsx from 'clsx';
import { uniqueId, omit } from 'app/functions';
import { SKIN_DARK, COLOR_GREEN, Skin, Color } from 'app/components/ui';
@@ -8,22 +10,15 @@ import { SKIN_DARK, COLOR_GREEN, Skin, Color } from 'app/components/ui';
import styles from './form.scss';
import FormInputComponent from './FormInputComponent';
type TextareaAutosizeProps = {
onHeightChange?: (number, TextareaAutosizeProps) => void;
useCacheForDOMMeasurements?: boolean;
minRows?: number;
maxRows?: number;
inputRef?: (el?: HTMLTextAreaElement) => void;
};
interface OwnProps {
placeholder?: string | MessageDescriptor;
label?: string | MessageDescriptor;
skin: Skin;
color: Color;
}
export default class TextArea extends FormInputComponent<
{
placeholder?: string | MessageDescriptor;
label?: string | MessageDescriptor;
skin: Skin;
color: Color;
} & TextareaAutosizeProps &
React.TextareaHTMLAttributes<HTMLTextAreaElement>
OwnProps & Omit<TextareaAutosizeProps, keyof OwnProps>
> {
static defaultProps = {
color: COLOR_GREEN,