Remove deprecated methods usage

This commit is contained in:
SleepWalker 2019-12-10 09:47:32 +02:00
parent 2a2a473e39
commit 3b20475cc6
19 changed files with 231 additions and 185 deletions

View File

@ -140,7 +140,7 @@ module.exports = {
'react/jsx-uses-vars': 'warn',
'react/jsx-no-comment-textnodes': 'warn',
'react/jsx-wrap-multilines': 'warn',
'react/no-deprecated': 'warn',
'react/no-deprecated': 'error',
'react/no-did-mount-set-state': 'warn',
'react/no-did-update-set-state': 'warn',
'react/no-direct-mutation-state': 'warn',

View File

@ -23,6 +23,8 @@ import { omit, debounce } from 'app/functions';
type ChildState = any;
// TODO: this may be rewritten in more efficient way using resize/mutation observer
export default class MeasureHeight extends React.PureComponent<
{
shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean;

View File

@ -31,6 +31,7 @@ export default class BaseAuthBody extends React.Component<
autoFocusField: string | null = '';
// eslint-disable-next-line react/no-deprecated
componentWillReceiveProps(nextProps, nextContext) {
if (nextContext.auth.error !== this.context.auth.error) {
this.form.setErrors(nextContext.auth.error || {});

View File

@ -185,17 +185,18 @@ class PanelTransition extends React.Component<Props, State> {
};
}
componentWillReceiveProps(nextProps: Props) {
componentDidUpdate(prevProps: Props) {
const nextPanel: PanelId =
nextProps.Body && (nextProps.Body.type as any).panelId;
const prevPanel: PanelId =
this.props.Body && (this.props.Body.type as any).panelId;
const prevPanel: PanelId =
prevProps.Body && (prevProps.Body.type as any).panelId;
if (nextPanel !== prevPanel) {
const direction = this.getDirection(nextPanel, prevPanel);
const forceHeight = direction === 'Y' && nextPanel !== prevPanel ? 1 : 0;
this.props.clearErrors();
this.setState({
direction,
panelId: nextPanel,

View File

@ -1,5 +1,4 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet';
import { SlideMotion } from 'app/components/ui/motion';
@ -21,35 +20,30 @@ import messages from './ChangeEmail.intl.json';
const STEPS_TOTAL = 3;
export default class ChangeEmail extends Component {
static propTypes = {
onChangeStep: PropTypes.func,
lang: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
stepForms: PropTypes.arrayOf(
(propValue, key, componentName, location, propFullName) => {
if (propValue.length !== 3) {
return new Error(
`\`${propFullName}\` must be an array of 3 FormModel instances. Validation failed.`,
);
}
export type ChangeEmailStep = 0 | 1 | 2;
type HeightProp = 'step0Height' | 'step1Height' | 'step2Height';
type HeightDict = {
[K in HeightProp]?: number;
};
if (!(propValue[key] instanceof FormModel)) {
return new Error(
`Invalid prop \`${propFullName}\` supplied to \
\`${componentName}\`. Validation failed.`,
);
}
interface Props {
onChangeStep: (step: ChangeEmailStep) => void;
lang: string;
email: string;
stepForms: FormModel[];
onSubmit: (step: ChangeEmailStep, form: FormModel) => Promise<void>;
step: ChangeEmailStep;
code?: string;
}
return null;
},
),
onSubmit: PropTypes.func.isRequired,
step: PropTypes.oneOf([0, 1, 2]),
code: PropTypes.string,
};
interface State extends HeightDict {
newEmail: string | null;
activeStep: ChangeEmailStep;
code: string;
}
static get defaultProps() {
export default class ChangeEmail extends React.Component<Props, State> {
static get defaultProps(): Partial<Props> {
return {
stepForms: [new FormModel(), new FormModel(), new FormModel()],
onChangeStep() {},
@ -57,19 +51,18 @@ export default class ChangeEmail extends Component {
};
}
state = {
state: State = {
newEmail: null,
activeStep: this.props.step,
code: this.props.code || '',
};
componentWillReceiveProps(nextProps) {
this.setState({
static getDerivedStateFromProps(props: Props, state: State) {
return {
activeStep:
typeof nextProps.step === 'number'
? nextProps.step
: this.state.activeStep,
code: nextProps.code || '',
});
typeof props.step === 'number' ? props.step : state.activeStep,
code: props.code || '',
};
}
render() {
@ -272,8 +265,9 @@ export default class ChangeEmail extends Component {
);
}
onStepMeasure(step) {
return height =>
onStepMeasure(step: ChangeEmailStep) {
return (height: number) =>
// @ts-ignore
this.setState({
[`step${step}Height`]: height,
});
@ -290,11 +284,11 @@ export default class ChangeEmail extends Component {
if (nextStep < STEPS_TOTAL) {
this.setState({
activeStep: nextStep,
activeStep: nextStep as ChangeEmailStep,
newEmail,
});
this.props.onChangeStep(nextStep);
this.props.onChangeStep(nextStep as ChangeEmailStep);
}
}

View File

@ -32,7 +32,7 @@ interface State {
qrCodeSrc: string;
}
export default class MfaEnable extends React.Component<Props, State> {
export default class MfaEnable extends React.PureComponent<Props, State> {
static defaultProps = {
confirmationForm: new FormModel(),
step: 0,
@ -51,12 +51,22 @@ export default class MfaEnable extends React.Component<Props, State> {
confirmationFormEl: Form | null;
componentWillMount() {
componentDidMount() {
this.syncState(this.props);
}
componentWillReceiveProps(nextProps: Props) {
this.syncState(nextProps);
static getDerivedStateFromProps(props: Props, state: State) {
if (typeof props.step === 'number' && props.step !== state.activeStep) {
return {
activeStep: props.step,
};
}
return null;
}
componentDidUpdate() {
this.syncState(this.props);
}
render() {
@ -108,23 +118,23 @@ export default class MfaEnable extends React.Component<Props, State> {
return (
<SlideMotion activeStep={activeStep}>
{[
<Instructions key="step1" />,
<KeyForm key="step2" secret={secret} qrCodeSrc={qrCodeSrc} />,
<Confirmation
key="step3"
form={this.props.confirmationForm}
formRef={(el: Form) => (this.confirmationFormEl = el)}
onSubmit={this.onTotpSubmit}
onInvalid={() => this.forceUpdate()}
/>,
]}
<Instructions key="step1" />
<KeyForm key="step2" secret={secret} qrCodeSrc={qrCodeSrc} />
<Confirmation
key="step3"
form={this.props.confirmationForm}
formRef={(el: Form) => (this.confirmationFormEl = el)}
onSubmit={this.onTotpSubmit}
onInvalid={() => this.forceUpdate()}
/>
</SlideMotion>
);
}
syncState(props: Props) {
if (props.step === 1) {
const { isLoading, qrCodeSrc } = this.state;
if (props.step === 1 && !isLoading && !qrCodeSrc) {
this.setState({ isLoading: true });
getSecret(this.context.userId).then(resp => {
@ -135,11 +145,6 @@ export default class MfaEnable extends React.Component<Props, State> {
});
});
}
this.setState({
activeStep:
typeof props.step === 'number' ? props.step : this.state.activeStep,
});
}
nextStep() {

View File

@ -4,37 +4,38 @@ import MeasureHeight from 'app/components/MeasureHeight';
import styles from './collapse.scss';
type Props = {
interface Props {
isOpened?: boolean;
children: React.ReactNode;
onRest: () => void;
};
}
export default class Collapse extends Component<
Props,
{
height: number;
wasInitialized: boolean;
}
> {
interface State {
isOpened?: boolean; // just to track value for derived updates
height: number;
wasInitialized: boolean;
}
export default class Collapse extends Component<Props, State> {
state = {
isOpened: this.props.isOpened,
height: 0,
wasInitialized: false,
};
static defaultProps = {
static defaultProps: Partial<Props> = {
onRest: () => {},
};
componentWillReceiveProps(nextProps: Props) {
if (
this.props.isOpened !== nextProps.isOpened &&
!this.state.wasInitialized
) {
this.setState({
static getDerivedStateFromProps(props: Props, state: State) {
if (props.isOpened !== state.isOpened && !state.wasInitialized) {
return {
isOpened: props.isOpened,
wasInitialized: true,
});
};
}
return null;
}
render() {

View File

@ -14,6 +14,7 @@ interface Props {
children: React.ReactNode;
}
interface State {
id: string; // just to track value for derived updates
isTouched: boolean;
isLoading: boolean;
}
@ -27,7 +28,8 @@ export default class Form extends React.Component<Props, State> {
onInvalid() {},
};
state = {
state: State = {
id: this.props.id,
isTouched: false,
isLoading: this.props.isLoading || false,
};
@ -44,27 +46,36 @@ export default class Form extends React.Component<Props, State> {
this.mounted = true;
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.id !== this.props.id) {
this.setState({
isTouched: false,
});
}
static getDerivedStateFromProps(props: Props, state: State) {
const patch: Partial<State> = {};
if (
typeof nextProps.isLoading !== 'undefined' &&
nextProps.isLoading !== this.state.isLoading
typeof props.isLoading !== 'undefined' &&
props.isLoading !== state.isLoading
) {
this.setState({
isLoading: nextProps.isLoading,
});
patch.isLoading = props.isLoading;
}
const nextForm = nextProps.form;
if (props.id !== state.id) {
patch.id = props.id;
patch.isTouched = true;
}
if (nextForm && this.props.form && nextForm !== this.props.form) {
this.props.form.removeLoadingListener(this.onLoading);
nextForm.addLoadingListener(this.onLoading);
return patch;
}
componentDidUpdate(prevProps: Props) {
const nextForm = this.props.form;
const prevForm = prevProps.form;
if (nextForm !== prevForm) {
if (prevForm) {
prevForm.removeLoadingListener(this.onLoading);
}
if (nextForm) {
nextForm.addLoadingListener(this.onLoading);
}
}
}

View File

@ -15,11 +15,11 @@ export default class FormInputComponent<P, S = {}> extends FormComponent<
error?: Error;
}
> {
componentWillReceiveProps() {
componentDidUpdate() {
if (this.state && this.state.error) {
Reflect.deleteProperty(this.state, 'error');
this.setState(this.state);
this.setState({
error: undefined,
});
}
}

View File

@ -20,7 +20,7 @@ export default class ImageLoader extends React.Component<
isLoading: true,
};
componentWillMount() {
componentDidMount() {
this.preloadImage();
}

View File

@ -4,29 +4,37 @@ import MeasureHeight from 'app/components/MeasureHeight';
import styles from './slide-motion.scss';
interface State {
[stepHeight: string]: number;
version: number;
interface Props {
activeStep: number;
children: React.ReactNode;
}
class SlideMotion extends React.Component<
{
activeStep: number;
children: React.ReactNode;
},
State
> {
interface State {
// [stepHeight: string]: number;
version: string;
prevChildren: React.ReactNode | undefined;
}
class SlideMotion extends React.PureComponent<Props, State> {
state: State = {
version: 0,
prevChildren: undefined, // to track version updates
version: `${this.props.activeStep}.0`,
};
isHeightMeasured: boolean;
componentWillReceiveProps() {
static getDerivedStateFromProps(props: Props, state: State) {
let [, version] = state.version.split('.').map(Number);
if (props.children !== state.prevChildren) {
version++;
}
// mark this view as dirty to re-measure height
this.setState({
version: this.state.version + 1,
});
return {
prevChildren: props.children,
version: `${props.activeStep}.${version}`,
};
}
render() {
@ -90,6 +98,7 @@ class SlideMotion extends React.Component<
onStepMeasure(step: number) {
return (height: number) =>
// @ts-ignore
this.setState({
[`step${step}Height`]: height,
});

View File

@ -14,7 +14,7 @@ export class PopupStack extends React.Component<{
}> {
unlistenTransition: () => void;
componentWillMount() {
componentDidMount() {
document.addEventListener('keyup', this.onKeyPress);
this.unlistenTransition = browserHistory.listen(this.onRouteLeave);
}

View File

@ -6,6 +6,10 @@ import AuthFlowRouteContents from './AuthFlowRouteContents';
export default function AuthFlowRoute(props: RouteProps) {
const { component: Component, ...routeProps } = props;
if (!Component) {
throw new Error('props.component required');
}
return (
<Route
{...routeProps}

View File

@ -2,7 +2,6 @@ import React from 'react';
import sinon from 'sinon';
import expect from 'app/test/unexpected';
import { mount } from 'enzyme';
import authFlow from 'app/services/authFlow';
import AuthFlowRouteContents from './AuthFlowRouteContents';
@ -21,7 +20,7 @@ describe('AuthFlowRouteContents', () => {
}
it('should render component if route allowed', () => {
const request = {
const authRequest = {
path: '/path',
params: { foo: 1 },
query: new URLSearchParams(),
@ -29,13 +28,14 @@ describe('AuthFlowRouteContents', () => {
const routerProps = {
location: {
pathname: request.path,
query: request.query,
pathname: authRequest.path,
search: '',
query: new URLSearchParams(),
},
match: {
params: request.params,
params: authRequest.params,
},
};
} as any;
(authFlow.handleRequest as any).callsArg(2);
@ -46,7 +46,10 @@ describe('AuthFlowRouteContents', () => {
const component = wrapper.find(Component);
expect(authFlow.handleRequest, 'to have a call satisfying', [
request,
{
...authRequest,
query: expect.it('to be a', URLSearchParams),
},
expect.it('to be a function'),
expect.it('to be a function'),
]);

View File

@ -1,52 +1,70 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import authFlow from 'app/services/authFlow';
type ComponentProps = {
component: any;
routerProps: { [key: string]: any };
};
interface Props {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
routerProps: RouteComponentProps;
}
interface State {
access: null | 'rejected' | 'allowed';
component: React.ReactElement | null;
}
export default class AuthFlowRouteContents extends React.Component<
ComponentProps,
{
component: any;
}
Props,
State
> {
state: {
component: any;
} = {
state: State = {
access: null,
component: null,
};
_isMounted = false;
mounted = false;
shouldComponentUpdate(
{ routerProps: nextRoute, component: nextComponent }: Props,
state: State,
) {
const { component: prevComponent, routerProps: prevRoute } = this.props;
return (
prevRoute.location.pathname !== nextRoute.location.pathname ||
prevRoute.location.search !== nextRoute.location.search ||
prevComponent !== nextComponent ||
this.state.access !== state.access
);
}
componentDidMount() {
this._isMounted = true;
this.mounted = true;
this.handleProps(this.props);
}
componentWillReceiveProps(nextProps: ComponentProps) {
this.handleProps(nextProps);
componentDidUpdate() {
this.handleProps(this.props);
}
componentWillUnmount() {
this._isMounted = false;
this.mounted = false;
}
render() {
return this.state.component;
}
handleProps(props: ComponentProps) {
handleProps(props: Props) {
const { routerProps } = props;
authFlow.handleRequest(
{
path: routerProps.location.pathname,
params: routerProps.match.params,
query: routerProps.location.query,
query: new URLSearchParams(routerProps.location.search),
},
this.onRedirect.bind(this),
this.onRouteAllowed.bind(this, props),
@ -54,23 +72,25 @@ export default class AuthFlowRouteContents extends React.Component<
}
onRedirect(path: string) {
if (!this._isMounted) {
if (!this.mounted) {
return;
}
this.setState({
access: 'rejected',
component: <Redirect to={path} />,
});
}
onRouteAllowed(props: ComponentProps) {
const { component: Component } = props;
if (!this._isMounted) {
onRouteAllowed(props: Props) {
if (!this.mounted) {
return;
}
const { component: Component } = props;
this.setState({
access: 'allowed',
component: <Component {...props.routerProps} />,
});
}

View File

@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import { RouteComponentProps, Redirect } from 'react-router-dom';
import FormModel from 'app/components/ui/form/FormModel';
import ChangeEmail from 'app/components/profile/changeEmail/ChangeEmail';
import ChangeEmail, {
ChangeEmailStep,
} from 'app/components/profile/changeEmail/ChangeEmail';
import {
requestEmailChange,
setNewEmail,
@ -28,24 +30,20 @@ class ChangeEmailPage extends React.Component<Props> {
goToProfile: PropTypes.func.isRequired,
};
componentWillMount() {
const { step } = this.props.match.params;
render() {
const { step = 'step1', code } = this.props.match.params;
if (step && !/^step[123]$/.test(step)) {
// wrong param value
this.props.history.push('/404');
return <Redirect to="/404" />;
}
}
render() {
const { step = 'step1', code } = this.props.match.params;
return (
<ChangeEmail
onSubmit={this.onSubmit}
email={this.props.email}
lang={this.props.lang}
step={Number(step.slice(-1)) - 1}
step={(Number(step.slice(-1)) - 1) as ChangeEmailStep}
onChangeStep={this.onChangeStep}
code={code}
/>
@ -56,7 +54,7 @@ class ChangeEmailPage extends React.Component<Props> {
this.props.history.push(`/profile/change-email/step${++step}`);
};
onSubmit = (step: number, form: FormModel) => {
onSubmit = (step: number, form: FormModel): Promise<void> => {
return this.context
.onSubmit({
form,

View File

@ -7,8 +7,6 @@ import { changeUsername } from 'app/services/api/accounts';
import { FormModel } from 'app/components/ui/form';
import ChangeUsername from 'app/components/profile/changeUsername/ChangeUsername';
type OwnProps = {};
type Props = {
username: string;
updateUsername: (username: string) => void;
@ -23,11 +21,7 @@ class ChangeUsernamePage extends React.Component<Props> {
form = new FormModel();
actualUsername: string;
componentWillMount() {
this.actualUsername = this.props.username;
}
actualUsername: string = this.props.username;
componentWillUnmount() {
this.props.updateUsername(this.actualUsername);

View File

@ -1,5 +1,5 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { RouteComponentProps, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import MultiFactorAuth, {
@ -22,26 +22,24 @@ class MultiFactorAuthPage extends React.Component<Props> {
goToProfile: PropTypes.func.isRequired,
};
componentWillMount() {
const { step } = this.props.match.params;
const { user } = this.props;
render() {
const {
user,
match: {
params: { step },
},
} = this.props;
if (step) {
if (!/^[1-3]$/.test(step)) {
// wrong param value
this.props.history.push('/404');
return;
return <Redirect to="/404" />;
}
if (user.isOtpEnabled) {
this.props.history.push('/mfa');
return <Redirect to="/mfa" />;
}
}
}
render() {
const { user } = this.props;
return (
<MultiFactorAuth
@ -58,7 +56,7 @@ class MultiFactorAuthPage extends React.Component<Props> {
const step = Number(this.props.match.params.step) - 1;
if (step !== 0 && step !== 1 && step !== 2) {
return 1;
return 0;
}
return step;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { resetAuth } from 'app/components/auth/actions';
import { withRouter } from 'react-router-dom';
@ -25,7 +25,7 @@ import { RootState } from 'app/reducers';
import styles from './root.scss';
import messages from './RootPage.intl.json';
class RootPage extends Component<{
class RootPage extends React.PureComponent<{
account: Account | null;
user: User;
isPopupActive: boolean;
@ -88,8 +88,13 @@ class RootPage extends Component<{
<Route path="/404" component={PageNotFound} />
<Route path="/rules" component={RulesPage} />
<Route path="/dev" component={DevPage} />
<AuthFlowRoute exact path="/" component={ProfilePage} />
<AuthFlowRoute path="/" component={AuthPage} />
{user.isGuest ? (
<AuthFlowRoute path="/" component={AuthPage} />
) : (
<AuthFlowRoute exact path="/" component={ProfilePage} />
)}
<Route component={PageNotFound} />
</Switch>
</div>