From d8d2df070233078e7259da6e78341d08abc3ab56 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Sat, 7 Dec 2019 13:28:52 +0200 Subject: [PATCH] Migrate from flow to typescript --- .eslintrc.js | 73 ++--- .flowconfig | 20 -- @types/webpack-loaders.d.ts | 61 ++++ babel.config.js | 12 +- flow-typed/Promise.js | 29 -- flow-typed/npm/react-intl_v2.x.x.js | 278 ------------------ flow-typed/npm/react-motion_vx.x.x.js | 123 -------- flow-typed/npm/react-redux_v5.x.x.js | 276 ----------------- flow-typed/npm/react-router-dom_v5.x.x.js | 176 ----------- flow-typed/npm/redux_v4.x.x.js | 100 ------- jest/__mocks__/intlMock.js | 3 +- package.json | 16 +- postcss.config.js | 1 + scripts/build-dll.js | 1 + scripts/{i18n-collect.js => i18n-collect.ts} | 34 ++- scripts/{i18n-crowdin.js => i18n-crowdin.ts} | 67 +++-- src/{App.js => App.tsx} | 10 +- .../{MeasureHeight.js => MeasureHeight.tsx} | 28 +- ...AccountSwitcher.js => AccountSwitcher.tsx} | 51 ++-- .../{actions.test.js => actions.test.ts} | 122 ++++---- .../accounts/{actions.js => actions.ts} | 100 +++---- .../{pure-actions.js => pure-actions.ts} | 11 +- src/components/accounts/index.js | 3 - src/components/accounts/index.ts | 2 + .../{reducer.test.js => reducer.test.ts} | 32 +- .../accounts/{reducer.js => reducer.ts} | 40 ++- .../auth/{AuthTitle.js => AuthTitle.tsx} | 6 +- .../{BaseAuthBody.js => BaseAuthBody.tsx} | 20 +- ...PanelTransition.js => PanelTransition.tsx} | 213 ++++++-------- .../{RejectionLink.js => RejectionLink.tsx} | 31 +- .../{AcceptRules.js => AcceptRules.ts} | 3 +- .../auth/{actions.test.js => actions.test.ts} | 32 +- .../auth/{actions.js => actions.ts} | 184 ++++++------ .../{Activation.js => Activation.ts} | 3 +- .../auth/appInfo/{AppInfo.js => AppInfo.tsx} | 13 +- .../{ChooseAccount.js => ChooseAccount.ts} | 2 +- src/components/auth/factory.js | 33 --- src/components/auth/factory.tsx | 50 ++++ .../auth/finish/{Finish.js => Finish.tsx} | 56 ++-- .../{ForgotPassword.js => ForgotPassword.ts} | 3 +- src/components/auth/index.ts | 1 + .../auth/login/{Login.js => Login.ts} | 3 +- src/components/auth/mfa/{Mfa.js => Mfa.tsx} | 4 +- .../auth/mfa/{MfaBody.js => MfaBody.tsx} | 3 - .../password/{Password.js => Password.ts} | 3 +- .../{Permissions.js => Permissions.ts} | 2 +- ...{RecoverPassword.js => RecoverPassword.ts} | 3 +- .../auth/{reducer.test.js => reducer.test.ts} | 7 +- .../auth/{reducer.js => reducer.ts} | 62 +++- .../register/{Register.js => Register.ts} | 7 +- ...esendActivation.js => ResendActivation.ts} | 5 +- ...ntactForm.test.js => ContactForm.test.tsx} | 35 ++- .../{ContactForm.js => ContactForm.tsx} | 29 +- .../{ContactLink.js => ContactLink.tsx} | 11 +- src/components/contact/{index.js => index.ts} | 2 - ...icationsIndex.js => ApplicationsIndex.tsx} | 21 +- .../dev/apps/{actions.js => actions.ts} | 19 +- ...ApplicationForm.js => ApplicationForm.tsx} | 47 +-- ...witcher.js => ApplicationTypeSwitcher.tsx} | 13 +- ...tServerType.js => MinecraftServerType.tsx} | 9 +- .../{WebsiteType.js => WebsiteType.tsx} | 7 +- .../dev/apps/{index.js => index.ts} | 1 - ...ApplicationItem.js => ApplicationItem.tsx} | 44 +-- ...plicationsList.js => ApplicationsList.tsx} | 17 +- .../dev/apps/list/{index.js => index.ts} | 2 - .../dev/apps/{reducer.js => reducer.ts} | 16 +- .../{FooterMenu.js => FooterMenu.tsx} | 18 +- .../footerMenu/{index.js => index.ts} | 0 .../{IntlProvider.js => IntlProvider.tsx} | 16 +- .../i18n/{actions.js => actions.ts} | 5 +- src/components/i18n/{index.js => index.ts} | 1 - .../i18n/{localeFlags.js => localeFlags.ts} | 7 +- .../i18n/{reducer.js => reducer.ts} | 5 +- .../{LanguageList.js => LanguageList.tsx} | 18 +- ...nguageSwitcher.js => LanguageSwitcher.tsx} | 60 ++-- .../{LocaleItem.js => LocaleItem.tsx} | 5 +- ...LanguageLink.js => ChangeLanguageLink.tsx} | 17 +- .../languageSwitcher/{index.js => index.ts} | 1 - .../profile/{Profile.js => Profile.tsx} | 29 +- src/components/profile/ProfileField.js | 6 +- .../{ProfileForm.js => ProfileForm.tsx} | 8 +- .../profile/changeEmail/ChangeEmail.js | 3 +- .../{MfaDisable.js => MfaDisable.tsx} | 25 +- .../{MfaEnable.js => MfaEnable.tsx} | 23 +- ...MultiFactorAuth.js => MultiFactorAuth.tsx} | 21 +- .../{Confirmation.js => Confirmation.tsx} | 11 +- .../confirmation/{index.js => index.ts} | 1 - .../{MfaDisableForm.js => MfaDisableForm.tsx} | 8 +- .../profile/multiFactorAuth/index.js | 3 - .../profile/multiFactorAuth/index.ts | 2 + .../{Instructions.js => Instructions.tsx} | 24 +- .../{OsInstruction.js => OsInstruction.tsx} | 10 +- .../instructions/{OsTile.js => OsTile.tsx} | 10 +- .../instructions/{index.js => index.ts} | 1 - .../keyForm/{KeyForm.js => KeyForm.tsx} | 10 +- .../keyForm/{index.js => index.ts} | 1 - .../status/{MfaStatus.js => MfaStatus.tsx} | 6 +- src/components/ui/{Panel.js => Panel.tsx} | 52 ++-- .../ui/{RelativeTime.js => RelativeTime.tsx} | 3 +- src/components/ui/bsod/{BSoD.js => BSoD.tsx} | 23 +- src/components/ui/bsod/Box.js | 1 + src/components/ui/bsod/BoxesField.js | 10 +- src/components/ui/bsod/BsodMiddleware.js | 36 --- ...dleware.test.js => BsodMiddleware.test.ts} | 12 +- src/components/ui/bsod/BsodMiddleware.ts | 49 +++ src/components/ui/bsod/dispatchBsod.js | 2 +- .../ui/collapse/{Collapse.js => Collapse.tsx} | 14 +- .../ui/collapse/{index.js => index.ts} | 1 - .../ui/form/{Button.js => Button.tsx} | 42 ++- .../ui/form/{Captcha.js => Captcha.tsx} | 13 +- .../ui/form/{Checkbox.js => Checkbox.tsx} | 12 +- src/components/ui/form/Dropdown.js | 9 +- src/components/ui/form/{Form.js => Form.tsx} | 50 ++-- src/components/ui/form/FormComponent.js | 41 --- src/components/ui/form/FormComponent.tsx | 41 +++ .../ui/form/{FormError.js => FormError.tsx} | 15 +- ...putComponent.js => FormInputComponent.tsx} | 12 +- .../ui/form/{FormModel.js => FormModel.ts} | 32 +- .../ui/form/{Input.test.js => Input.test.tsx} | 3 +- .../ui/form/{Input.js => Input.tsx} | 82 ++++-- src/components/ui/form/LinkButton.js | 15 - src/components/ui/form/LinkButton.tsx | 18 ++ .../ui/form/{Radio.js => Radio.tsx} | 17 +- .../ui/form/{TextArea.js => TextArea.tsx} | 52 ++-- src/components/ui/form/{index.js => index.ts} | 0 src/components/ui/{index.js => index.ts} | 1 - ...ComponentLoader.js => ComponentLoader.tsx} | 9 +- .../{ImageLoader.js => ImageLoader.tsx} | 13 +- .../ui/loader/{index.js => index.ts} | 2 - .../{SlideMotion.js => SlideMotion.tsx} | 27 +- .../ui/motion/{index.js => index.ts} | 1 - src/components/ui/popup/PopupStack.test.js | 5 +- .../popup/{PopupStack.js => PopupStack.tsx} | 34 +-- src/components/ui/popup/actions.js | 29 -- src/components/ui/popup/actions.ts | 17 ++ src/components/ui/popup/reducer.js | 24 -- src/components/ui/popup/reducer.test.js | 17 +- src/components/ui/popup/reducer.ts | 36 +++ .../{ScrollIntoView.js => ScrollIntoView.tsx} | 12 +- .../ui/scroll/{index.js => index.ts} | 2 - .../ui/scroll/{scroll.js => scroll.ts} | 10 +- .../ui/stepper/{Stepper.js => Stepper.tsx} | 13 +- .../ui/stepper/{index.js => index.ts} | 0 src/components/user/{User.js => User.ts} | 0 .../user/{actions.js => actions.ts} | 36 ++- .../user/{factory.js => factory.ts} | 18 +- src/components/user/index.js | 2 - src/components/user/index.ts | 1 + ...test.js => bearerHeaderMiddleware.test.ts} | 52 ++-- ...iddleware.js => bearerHeaderMiddleware.ts} | 20 +- ...test.js => refreshTokenMiddleware.test.ts} | 26 +- ...iddleware.js => refreshTokenMiddleware.ts} | 22 +- .../user/{reducer.js => reducer.ts} | 37 ++- .../{LoggedInPanel.js => LoggedInPanel.tsx} | 19 +- .../userbar/{Userbar.js => Userbar.tsx} | 14 +- src/containers/AuthFlowRoute.js | 18 -- src/containers/AuthFlowRoute.tsx | 20 ++ ...test.js => AuthFlowRouteContents.test.tsx} | 4 +- ...eContents.js => AuthFlowRouteContents.tsx} | 15 +- src/containers/PrivateRoute.js | 35 --- src/containers/PrivateRoute.tsx | 29 ++ src/{functions.js => functions.ts} | 16 +- src/i18n/index.js | 86 ++++++ src/i18n/index.json | 86 ------ src/{index.js => index.tsx} | 33 ++- src/pages/auth/{AuthPage.js => AuthPage.tsx} | 29 +- ...ccessOauthPage.js => SuccessOauthPage.tsx} | 15 +- ...nsListPage.js => ApplicationsListPage.tsx} | 41 ++- ...onPage.js => CreateNewApplicationPage.tsx} | 18 +- src/pages/dev/{DevPage.js => DevPage.tsx} | 1 - ...ationPage.js => UpdateApplicationPage.tsx} | 43 ++- ...ChangeEmailPage.js => ChangeEmailPage.tsx} | 41 +-- ...PasswordPage.js => ChangePasswordPage.tsx} | 24 +- ...UsernamePage.js => ChangeUsernamePage.tsx} | 22 +- ...torAuthPage.js => MultiFactorAuthPage.tsx} | 43 +-- .../{ProfilePage.js => ProfilePage.tsx} | 36 ++- src/pages/root/{RootPage.js => RootPage.tsx} | 20 +- .../{RulesPage.test.js => RulesPage.test.tsx} | 6 +- .../rules/{RulesPage.js => RulesPage.tsx} | 18 +- src/reducers.js | 19 -- src/reducers.ts | 50 ++++ src/services/api/{accounts.js => accounts.ts} | 41 ++- .../{activate.test.js => activate.test.ts} | 4 +- ...ication.test.js => authentication.test.ts} | 44 +-- .../{authentication.js => authentication.ts} | 40 +-- src/services/api/{feedback.js => feedback.ts} | 0 src/services/api/{mfa.js => mfa.ts} | 16 +- src/services/api/{oauth.js => oauth.ts} | 117 ++++---- src/services/api/options.js | 18 -- .../api/{options.test.js => options.test.ts} | 2 +- src/services/api/options.ts | 23 ++ src/services/api/{signup.js => signup.ts} | 8 +- src/services/authFlow/AbstractState.js | 13 - src/services/authFlow/AbstractState.ts | 12 + ...State.test.js => AcceptRulesState.test.ts} | 0 ...cceptRulesState.js => AcceptRulesState.ts} | 0 ...nState.test.js => ActivationState.test.ts} | 0 ...{ActivationState.js => ActivationState.ts} | 5 +- ...al.test.js => AuthFlow.functional.test.ts} | 6 +- .../{AuthFlow.test.js => AuthFlow.test.ts} | 1 + .../authFlow/{AuthFlow.js => AuthFlow.ts} | 57 ++-- ...ate.test.js => ChooseAccountState.test.ts} | 0 ...eAccountState.js => ChooseAccountState.ts} | 0 ...eteState.test.js => CompleteState.test.ts} | 0 .../{CompleteState.js => CompleteState.ts} | 44 +-- ...inishState.test.js => FinishState.test.ts} | 0 .../{FinishState.js => FinishState.ts} | 0 ...te.test.js => ForgotPasswordState.test.ts} | 0 ...asswordState.js => ForgotPasswordState.ts} | 7 +- ...{LoginState.test.js => LoginState.test.ts} | 0 .../authFlow/{LoginState.js => LoginState.ts} | 6 +- .../{MfaState.test.js => MfaState.test.ts} | 0 .../authFlow/{MfaState.js => MfaState.ts} | 4 +- ...{OAuthState.test.js => OAuthState.test.ts} | 2 +- .../authFlow/{OAuthState.js => OAuthState.ts} | 0 ...ordState.test.js => PasswordState.test.ts} | 0 .../{PasswordState.js => PasswordState.ts} | 8 +- ...State.test.js => PermissionsState.test.ts} | 0 ...ermissionsState.js => PermissionsState.ts} | 0 ...e.test.js => RecoverPasswordState.test.ts} | 0 ...sswordState.js => RecoverPasswordState.ts} | 0 ...terState.test.js => RegisterState.test.ts} | 0 .../{RegisterState.js => RegisterState.ts} | 0 ....test.js => ResendActivationState.test.ts} | 0 ...ationState.js => ResendActivationState.ts} | 5 +- .../authFlow/{helpers.js => helpers.ts} | 6 +- src/services/authFlow/{index.js => index.ts} | 7 +- src/services/{captcha.js => captcha.ts} | 15 +- src/services/{copy.js => copy.ts} | 4 +- src/services/errorsDict/errorsDict.js | 2 +- src/services/{font.js => font.ts} | 11 +- src/services/history.js | 2 +- src/services/i18n/{i18n.js => i18n.ts} | 26 +- src/services/i18n/{index.js => index.ts} | 1 - .../i18n/{intlPolyfill.js => intlPolyfill.ts} | 4 +- src/services/loader.js | 18 +- src/services/localStorage.js | 22 +- src/services/logger/abbreviate.js | 6 +- src/services/logger/index.js | 3 - src/services/logger/index.ts | 1 + src/services/logger/{logger.js => logger.ts} | 20 +- src/services/request/InternalServerError.js | 32 -- src/services/request/InternalServerError.ts | 36 +++ .../request/PromiseMiddlewareLayer.js | 49 --- ...test.js => PromiseMiddlewareLayer.test.ts} | 51 ++-- .../request/PromiseMiddlewareLayer.ts | 98 ++++++ ...AbortedError.js => RequestAbortedError.ts} | 9 +- src/services/request/{index.js => index.ts} | 11 +- .../{request.test.js => request.test.ts} | 10 +- .../request/{request.js => request.ts} | 84 ++++-- src/storeFactory.js | 2 - src/test/unexpected.js | 1 + .../dev/{guest.test.js => guest.test.ts} | 1 + .../dev/{user.test.js => user.test.ts} | 4 +- ...n.test.js => invalid-refreshToken.test.ts} | 8 +- .../{sign-in.test.js => sign-in.test.ts} | 5 +- .../cypress/plugins/{index.js => index.ts} | 4 +- .../support/{commands.js => commands.ts} | 0 tests-e2e/cypress/support/index.d.ts | 14 + .../cypress/support/{index.js => index.ts} | 0 tests-e2e/cypress/tsconfig.json | 11 + tsconfig.json | 18 ++ webpack-utils/fontgen-loader.js | 2 + webpack-utils/intl-loader.js | 1 + webpack.config.js | 12 +- webpack.dll.config.js | 4 +- yarn.lock | 273 +++++++++-------- 267 files changed, 2806 insertions(+), 3535 deletions(-) delete mode 100644 .flowconfig create mode 100644 @types/webpack-loaders.d.ts delete mode 100644 flow-typed/Promise.js delete mode 100644 flow-typed/npm/react-intl_v2.x.x.js delete mode 100644 flow-typed/npm/react-motion_vx.x.x.js delete mode 100644 flow-typed/npm/react-redux_v5.x.x.js delete mode 100644 flow-typed/npm/react-router-dom_v5.x.x.js delete mode 100644 flow-typed/npm/redux_v4.x.x.js rename scripts/{i18n-collect.js => i18n-collect.ts} (89%) rename scripts/{i18n-crowdin.js => i18n-crowdin.ts} (86%) rename src/{App.js => App.tsx} (87%) rename src/components/{MeasureHeight.js => MeasureHeight.tsx} (78%) rename src/components/accounts/{AccountSwitcher.js => AccountSwitcher.tsx} (80%) rename src/components/accounts/{actions.test.js => actions.test.ts} (77%) rename src/components/accounts/{actions.js => actions.ts} (79%) rename src/components/accounts/actions/{pure-actions.js => pure-actions.ts} (93%) delete mode 100644 src/components/accounts/index.js create mode 100644 src/components/accounts/index.ts rename src/components/accounts/{reducer.test.js => reducer.test.ts} (82%) rename src/components/accounts/{reducer.js => reducer.ts} (80%) rename src/components/auth/{AuthTitle.js => AuthTitle.tsx} (59%) rename src/components/auth/{BaseAuthBody.js => BaseAuthBody.tsx} (75%) rename src/components/auth/{PanelTransition.js => PanelTransition.tsx} (79%) rename src/components/auth/{RejectionLink.js => RejectionLink.tsx} (56%) rename src/components/auth/acceptRules/{AcceptRules.js => AcceptRules.ts} (86%) rename src/components/auth/{actions.test.js => actions.test.ts} (85%) rename src/components/auth/{actions.js => actions.ts} (82%) rename src/components/auth/activation/{Activation.js => Activation.ts} (86%) rename src/components/auth/appInfo/{AppInfo.js => AppInfo.tsx} (90%) rename src/components/auth/chooseAccount/{ChooseAccount.js => ChooseAccount.ts} (85%) delete mode 100644 src/components/auth/factory.js create mode 100644 src/components/auth/factory.tsx rename src/components/auth/finish/{Finish.js => Finish.tsx} (73%) rename src/components/auth/forgotPassword/{ForgotPassword.js => ForgotPassword.ts} (86%) create mode 100644 src/components/auth/index.ts rename src/components/auth/login/{Login.js => Login.ts} (86%) rename src/components/auth/mfa/{Mfa.js => Mfa.tsx} (83%) rename src/components/auth/mfa/{MfaBody.js => MfaBody.tsx} (98%) rename src/components/auth/password/{Password.js => Password.ts} (85%) rename src/components/auth/permissions/{Permissions.js => Permissions.ts} (86%) rename src/components/auth/recoverPassword/{RecoverPassword.js => RecoverPassword.ts} (85%) rename src/components/auth/{reducer.test.js => reducer.test.ts} (88%) rename src/components/auth/{reducer.js => reducer.ts} (67%) rename src/components/auth/register/{Register.js => Register.ts} (64%) rename src/components/auth/resendActivation/{ResendActivation.js => ResendActivation.ts} (68%) rename src/components/contact/{ContactForm.test.js => ContactForm.test.tsx} (85%) rename src/components/contact/{ContactForm.js => ContactForm.tsx} (92%) rename src/components/contact/{ContactLink.js => ContactLink.tsx} (70%) rename src/components/contact/{index.js => index.ts} (84%) rename src/components/dev/apps/{ApplicationsIndex.js => ApplicationsIndex.tsx} (89%) rename src/components/dev/apps/{actions.js => actions.ts} (80%) rename src/components/dev/apps/applicationForm/{ApplicationForm.js => ApplicationForm.tsx} (80%) rename src/components/dev/apps/applicationForm/{ApplicationTypeSwitcher.js => ApplicationTypeSwitcher.tsx} (72%) rename src/components/dev/apps/applicationForm/{MinecraftServerType.js => MinecraftServerType.tsx} (92%) rename src/components/dev/apps/applicationForm/{WebsiteType.js => WebsiteType.tsx} (94%) rename src/components/dev/apps/{index.js => index.ts} (95%) rename src/components/dev/apps/list/{ApplicationItem.js => ApplicationItem.tsx} (90%) rename src/components/dev/apps/list/{ApplicationsList.js => ApplicationsList.tsx} (88%) rename src/components/dev/apps/list/{index.js => index.ts} (82%) rename src/components/dev/apps/{reducer.js => reducer.ts} (78%) rename src/components/footerMenu/{FooterMenu.js => FooterMenu.tsx} (79%) rename src/components/footerMenu/{index.js => index.ts} (100%) rename src/components/i18n/{IntlProvider.js => IntlProvider.tsx} (62%) rename src/components/i18n/{actions.js => actions.ts} (78%) rename src/components/i18n/{index.js => index.ts} (92%) rename src/components/i18n/{localeFlags.js => localeFlags.ts} (80%) rename src/components/i18n/{reducer.js => reducer.ts} (73%) rename src/components/languageSwitcher/{LanguageList.js => LanguageList.tsx} (93%) rename src/components/languageSwitcher/{LanguageSwitcher.js => LanguageSwitcher.tsx} (82%) rename src/components/languageSwitcher/{LocaleItem.js => LocaleItem.tsx} (94%) rename src/components/languageSwitcher/changeLanguageLink/{ChangeLanguageLink.js => ChangeLanguageLink.tsx} (82%) rename src/components/languageSwitcher/{index.js => index.ts} (93%) rename src/components/profile/{Profile.js => Profile.tsx} (93%) rename src/components/profile/{ProfileForm.js => ProfileForm.tsx} (92%) rename src/components/profile/multiFactorAuth/{MfaDisable.js => MfaDisable.tsx} (69%) rename src/components/profile/multiFactorAuth/{MfaEnable.js => MfaEnable.tsx} (90%) rename src/components/profile/multiFactorAuth/{MultiFactorAuth.js => MultiFactorAuth.tsx} (80%) rename src/components/profile/multiFactorAuth/confirmation/{Confirmation.js => Confirmation.tsx} (88%) rename src/components/profile/multiFactorAuth/confirmation/{index.js => index.ts} (82%) rename src/components/profile/multiFactorAuth/disableForm/{MfaDisableForm.js => MfaDisableForm.tsx} (89%) delete mode 100644 src/components/profile/multiFactorAuth/index.js create mode 100644 src/components/profile/multiFactorAuth/index.ts rename src/components/profile/multiFactorAuth/instructions/{Instructions.js => Instructions.tsx} (87%) rename src/components/profile/multiFactorAuth/instructions/{OsInstruction.js => OsInstruction.tsx} (96%) rename src/components/profile/multiFactorAuth/instructions/{OsTile.js => OsTile.tsx} (79%) rename src/components/profile/multiFactorAuth/instructions/{index.js => index.ts} (82%) rename src/components/profile/multiFactorAuth/keyForm/{KeyForm.js => KeyForm.tsx} (96%) rename src/components/profile/multiFactorAuth/keyForm/{index.js => index.ts} (80%) rename src/components/profile/multiFactorAuth/status/{MfaStatus.js => MfaStatus.tsx} (93%) rename src/components/ui/{Panel.js => Panel.tsx} (64%) rename src/components/ui/{RelativeTime.js => RelativeTime.tsx} (86%) rename src/components/ui/bsod/{BSoD.js => BSoD.tsx} (86%) delete mode 100644 src/components/ui/bsod/BsodMiddleware.js rename src/components/ui/bsod/{BsodMiddleware.test.js => BsodMiddleware.test.ts} (75%) create mode 100644 src/components/ui/bsod/BsodMiddleware.ts rename src/components/ui/collapse/{Collapse.js => Collapse.tsx} (91%) rename src/components/ui/collapse/{index.js => index.ts} (80%) rename src/components/ui/form/{Button.js => Button.tsx} (51%) rename src/components/ui/form/{Captcha.js => Captcha.tsx} (90%) rename src/components/ui/form/{Checkbox.js => Checkbox.tsx} (84%) rename src/components/ui/form/{Form.js => Form.tsx} (79%) delete mode 100644 src/components/ui/form/FormComponent.js create mode 100644 src/components/ui/form/FormComponent.tsx rename src/components/ui/form/{FormError.js => FormError.tsx} (71%) rename src/components/ui/form/{FormInputComponent.js => FormInputComponent.tsx} (75%) rename src/components/ui/form/{FormModel.js => FormModel.ts} (89%) rename src/components/ui/form/{Input.test.js => Input.test.tsx} (96%) rename src/components/ui/form/{Input.js => Input.tsx} (61%) delete mode 100644 src/components/ui/form/LinkButton.js create mode 100644 src/components/ui/form/LinkButton.tsx rename src/components/ui/form/{Radio.js => Radio.tsx} (81%) rename src/components/ui/form/{TextArea.js => TextArea.tsx} (59%) rename src/components/ui/form/{index.js => index.ts} (100%) rename src/components/ui/{index.js => index.ts} (99%) rename src/components/ui/loader/{ComponentLoader.js => ComponentLoader.tsx} (74%) rename src/components/ui/loader/{ImageLoader.js => ImageLoader.tsx} (90%) rename src/components/ui/loader/{index.js => index.ts} (92%) rename src/components/ui/motion/{SlideMotion.js => SlideMotion.tsx} (86%) rename src/components/ui/motion/{index.js => index.ts} (86%) rename src/components/ui/popup/{PopupStack.js => PopupStack.tsx} (76%) delete mode 100644 src/components/ui/popup/actions.js create mode 100644 src/components/ui/popup/actions.ts delete mode 100644 src/components/ui/popup/reducer.js create mode 100644 src/components/ui/popup/reducer.ts rename src/components/ui/scroll/{ScrollIntoView.js => ScrollIntoView.tsx} (70%) rename src/components/ui/scroll/{index.js => index.ts} (86%) rename src/components/ui/scroll/{scroll.js => scroll.ts} (95%) rename src/components/ui/stepper/{Stepper.js => Stepper.tsx} (76%) rename src/components/ui/stepper/{index.js => index.ts} (100%) rename src/components/user/{User.js => User.ts} (100%) rename src/components/user/{actions.js => actions.ts} (67%) rename src/components/user/{factory.js => factory.ts} (76%) delete mode 100644 src/components/user/index.js create mode 100644 src/components/user/index.ts rename src/components/user/middlewares/{bearerHeaderMiddleware.test.js => bearerHeaderMiddleware.test.ts} (52%) rename src/components/user/middlewares/{bearerHeaderMiddleware.js => bearerHeaderMiddleware.ts} (66%) rename src/components/user/middlewares/{refreshTokenMiddleware.test.js => refreshTokenMiddleware.test.ts} (92%) rename src/components/user/middlewares/{refreshTokenMiddleware.js => refreshTokenMiddleware.ts} (77%) rename src/components/user/{reducer.js => reducer.ts} (73%) rename src/components/userbar/{LoggedInPanel.js => LoggedInPanel.tsx} (90%) rename src/components/userbar/{Userbar.js => Userbar.tsx} (80%) delete mode 100644 src/containers/AuthFlowRoute.js create mode 100644 src/containers/AuthFlowRoute.tsx rename src/containers/{AuthFlowRouteContents.test.js => AuthFlowRouteContents.test.tsx} (92%) rename src/containers/{AuthFlowRouteContents.js => AuthFlowRouteContents.tsx} (86%) delete mode 100644 src/containers/PrivateRoute.js create mode 100644 src/containers/PrivateRoute.tsx rename src/{functions.js => functions.ts} (88%) create mode 100644 src/i18n/index.js delete mode 100644 src/i18n/index.json rename src/{index.js => index.tsx} (84%) rename src/pages/auth/{AuthPage.js => AuthPage.tsx} (92%) rename src/pages/auth/{SuccessOauthPage.js => SuccessOauthPage.tsx} (90%) rename src/pages/dev/{ApplicationsListPage.js => ApplicationsListPage.tsx} (68%) rename src/pages/dev/{CreateNewApplicationPage.js => CreateNewApplicationPage.tsx} (87%) rename src/pages/dev/{DevPage.js => DevPage.tsx} (99%) rename src/pages/dev/{UpdateApplicationPage.js => UpdateApplicationPage.tsx} (76%) rename src/pages/profile/{ChangeEmailPage.js => ChangeEmailPage.tsx} (79%) rename src/pages/profile/{ChangePasswordPage.js => ChangePasswordPage.tsx} (73%) rename src/pages/profile/{ChangeUsernamePage.js => ChangeUsernamePage.tsx} (86%) rename src/pages/profile/{MultiFactorAuthPage.js => MultiFactorAuthPage.tsx} (58%) rename src/pages/profile/{ProfilePage.js => ProfilePage.tsx} (90%) rename src/pages/root/{RootPage.js => RootPage.tsx} (90%) rename src/pages/rules/{RulesPage.test.js => RulesPage.test.tsx} (93%) rename src/pages/rules/{RulesPage.js => RulesPage.tsx} (96%) delete mode 100644 src/reducers.js create mode 100644 src/reducers.ts rename src/services/api/{accounts.js => accounts.ts} (76%) rename src/services/api/{activate.test.js => activate.test.ts} (95%) rename src/services/api/{authentication.test.js => authentication.test.ts} (86%) rename src/services/api/{authentication.js => authentication.ts} (86%) rename src/services/api/{feedback.js => feedback.ts} (100%) rename src/services/api/{mfa.js => mfa.ts} (70%) rename src/services/api/{oauth.js => oauth.ts} (67%) delete mode 100644 src/services/api/options.js rename src/services/api/{options.test.js => options.test.ts} (96%) create mode 100644 src/services/api/options.ts rename src/services/api/{signup.js => signup.ts} (76%) delete mode 100644 src/services/authFlow/AbstractState.js create mode 100644 src/services/authFlow/AbstractState.ts rename src/services/authFlow/{AcceptRulesState.test.js => AcceptRulesState.test.ts} (100%) rename src/services/authFlow/{AcceptRulesState.js => AcceptRulesState.ts} (100%) rename src/services/authFlow/{ActivationState.test.js => ActivationState.test.ts} (100%) rename src/services/authFlow/{ActivationState.js => ActivationState.ts} (86%) rename src/services/authFlow/{AuthFlow.functional.test.js => AuthFlow.functional.test.ts} (97%) rename src/services/authFlow/{AuthFlow.test.js => AuthFlow.test.ts} (99%) rename src/services/authFlow/{AuthFlow.js => AuthFlow.ts} (83%) rename src/services/authFlow/{ChooseAccountState.test.js => ChooseAccountState.test.ts} (100%) rename src/services/authFlow/{ChooseAccountState.js => ChooseAccountState.ts} (100%) rename src/services/authFlow/{CompleteState.test.js => CompleteState.test.ts} (100%) rename src/services/authFlow/{CompleteState.js => CompleteState.ts} (79%) rename src/services/authFlow/{FinishState.test.js => FinishState.test.ts} (100%) rename src/services/authFlow/{FinishState.js => FinishState.ts} (100%) rename src/services/authFlow/{ForgotPasswordState.test.js => ForgotPasswordState.test.ts} (100%) rename src/services/authFlow/{ForgotPasswordState.js => ForgotPasswordState.ts} (90%) rename src/services/authFlow/{LoginState.test.js => LoginState.test.ts} (100%) rename src/services/authFlow/{LoginState.js => LoginState.ts} (94%) rename src/services/authFlow/{MfaState.test.js => MfaState.test.ts} (100%) rename src/services/authFlow/{MfaState.js => MfaState.ts} (94%) rename src/services/authFlow/{OAuthState.test.js => OAuthState.test.ts} (98%) rename src/services/authFlow/{OAuthState.js => OAuthState.ts} (100%) rename src/services/authFlow/{PasswordState.test.js => PasswordState.test.ts} (100%) rename src/services/authFlow/{PasswordState.js => PasswordState.ts} (95%) rename src/services/authFlow/{PermissionsState.test.js => PermissionsState.test.ts} (100%) rename src/services/authFlow/{PermissionsState.js => PermissionsState.ts} (100%) rename src/services/authFlow/{RecoverPasswordState.test.js => RecoverPasswordState.test.ts} (100%) rename src/services/authFlow/{RecoverPasswordState.js => RecoverPasswordState.ts} (100%) rename src/services/authFlow/{RegisterState.test.js => RegisterState.test.ts} (100%) rename src/services/authFlow/{RegisterState.js => RegisterState.ts} (100%) rename src/services/authFlow/{ResendActivationState.test.js => ResendActivationState.test.ts} (100%) rename src/services/authFlow/{ResendActivationState.js => ResendActivationState.ts} (87%) rename src/services/authFlow/{helpers.js => helpers.ts} (90%) rename src/services/authFlow/{index.js => index.ts} (88%) rename src/services/{captcha.js => captcha.ts} (84%) rename src/services/{copy.js => copy.ts} (78%) rename src/services/{font.js => font.ts} (69%) rename src/services/i18n/{i18n.js => i18n.ts} (77%) rename src/services/i18n/{index.js => index.ts} (79%) rename src/services/i18n/{intlPolyfill.js => intlPolyfill.ts} (96%) delete mode 100644 src/services/logger/index.js create mode 100644 src/services/logger/index.ts rename src/services/logger/{logger.js => logger.ts} (87%) delete mode 100644 src/services/request/InternalServerError.js create mode 100644 src/services/request/InternalServerError.ts delete mode 100644 src/services/request/PromiseMiddlewareLayer.js rename src/services/request/{PromiseMiddlewareLayer.test.js => PromiseMiddlewareLayer.test.ts} (74%) create mode 100644 src/services/request/PromiseMiddlewareLayer.ts rename src/services/request/{RequestAbortedError.js => RequestAbortedError.ts} (87%) rename src/services/request/{index.js => index.ts} (56%) rename src/services/request/{request.test.js => request.test.ts} (89%) rename src/services/request/{request.js => request.ts} (71%) rename tests-e2e/cypress/integration/dev/{guest.test.js => guest.test.ts} (95%) rename tests-e2e/cypress/integration/dev/{user.test.js => user.test.ts} (91%) rename tests-e2e/cypress/integration/{invalid-refreshToken.test.js => invalid-refreshToken.test.ts} (97%) rename tests-e2e/cypress/integration/{sign-in.test.js => sign-in.test.ts} (93%) rename tests-e2e/cypress/plugins/{index.js => index.ts} (87%) rename tests-e2e/cypress/support/{commands.js => commands.ts} (100%) create mode 100644 tests-e2e/cypress/support/index.d.ts rename tests-e2e/cypress/support/{index.js => index.ts} (100%) create mode 100644 tests-e2e/cypress/tsconfig.json create mode 100644 tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index d769e00..a64b75d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - parser: 'babel-eslint', + parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -7,12 +7,14 @@ module.exports = { extends: [ 'eslint:recommended', - 'plugin:flowtype/recommended', - 'plugin:prettier/recommended', 'plugin:jsdoc/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier/@typescript-eslint', + 'plugin:prettier/recommended', ], - plugins: ['react', 'flowtype'], + plugins: ['react'], env: { browser: true, @@ -56,7 +58,7 @@ module.exports = { // @see: http://eslint.org/docs/rules/ rules: { - 'no-prototype-builtins': ['warn'], // temporary set to warn + 'no-prototype-builtins': 'warn', // temporary set to warn 'no-restricted-globals': [ 'error', 'localStorage', @@ -67,7 +69,7 @@ module.exports = { 'error', { min: 2, exceptions: ['x', 'y', 'i', 'k', 'l', 'm', 'n', '$', '_'] }, ], - 'require-atomic-updates': ['warn'], + 'require-atomic-updates': 'warn', 'guard-for-in': ['error'], 'no-var': ['error'], 'prefer-const': ['error'], @@ -76,25 +78,18 @@ module.exports = { 'no-multi-assign': ['error'], eqeqeq: ['error'], 'prefer-rest-params': ['error'], - 'prefer-object-spread': ['warn'], - 'prefer-destructuring': ['warn'], - 'no-bitwise': ['warn'], - 'no-negated-condition': ['warn'], - 'no-nested-ternary': ['warn'], - 'no-unneeded-ternary': ['warn'], - 'no-shadow': ['warn'], - 'no-else-return': ['warn'], - radix: ['warn'], - 'prefer-promise-reject-errors': ['warn'], - 'no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'after-used', - argsIgnorePattern: '^_', - }, - ], - 'object-shorthand': ['warn'], + 'prefer-object-spread': 'warn', + 'prefer-destructuring': 'warn', + 'no-bitwise': 'warn', + 'no-negated-condition': 'warn', + 'no-nested-ternary': 'warn', + 'no-unneeded-ternary': 'warn', + 'no-shadow': 'warn', + 'no-else-return': 'warn', + radix: 'warn', + 'prefer-promise-reject-errors': 'warn', + 'object-shorthand': 'warn', + 'require-atomic-updates': 'off', // force extra lines around if, else, for, while, switch, return etc 'padding-line-between-statements': [ @@ -134,9 +129,8 @@ module.exports = { 'warn', { eventHandlerPrefix: 'on', eventHandlerPropPrefix: 'on' }, ], - 'react/jsx-indent-props': 'warn', 'react/jsx-key': 'warn', - 'react/jsx-max-props-per-line': ['warn', { maximum: 3 }], + 'react/jsx-max-props-per-line': 'off', 'react/jsx-no-bind': 'off', 'react/jsx-no-duplicate-props': 'warn', 'react/jsx-no-literals': 'off', @@ -156,17 +150,24 @@ module.exports = { 'react/no-string-refs': 'warn', 'react/no-unknown-property': 'warn', 'react/prefer-es6-class': 'warn', - 'react/prop-types': 'off', // using flowtype for this task + 'react/prop-types': 'off', // using ts for this task 'react/self-closing-comp': 'warn', - 'react/sort-comp': [ - 'off', - { order: ['lifecycle', 'render', 'everything-else'] }, - ], + 'react/sort-comp': 'off', - 'flowtype/space-after-type-colon': 'off', - 'flowtype/no-unused-expressions': [ - 'warn', - { allowShortCircuit: true, allowTernary: true }, + // ts + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'after-used', + argsIgnorePattern: '^_', + }, ], }, }; diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index 24d5741..0000000 --- a/.flowconfig +++ /dev/null @@ -1,20 +0,0 @@ -[ignore] -.*/node_modules/fbjs/lib/.* -.*/node_modules/react-motion/lib/.* -.*/tests-e2e/.* - -[include] - -[libs] -./flow-typed - -[options] -module.system.node.resolve_dirname=node_modules -module.system.node.resolve_dirname=src -module.file_ext=.js -module.file_ext=.json -module.file_ext=.jsx -module.file_ext=.css -module.file_ext=.scss -module.ignore_non_literal_requires=true -esproposal.optional_chaining=enable diff --git a/@types/webpack-loaders.d.ts b/@types/webpack-loaders.d.ts new file mode 100644 index 0000000..2948cb9 --- /dev/null +++ b/@types/webpack-loaders.d.ts @@ -0,0 +1,61 @@ +declare module '*.html' { + const url: string; + export = url; +} + +declare module '*.svg' { + const url: string; + export = url; +} + +declare module '*.png' { + const url: string; + export = url; +} + +declare module '*.gif' { + const url: string; + export = url; +} + +declare module '*.jpg' { + const url: string; + export = url; +} + +declare module '*.intl.json' { + import { MessageDescriptor } from 'react-intl'; + + const descriptor: { + [key: string]: MessageDescriptor; + }; + + export = descriptor; +} + +declare module '*.json' { + const jsonContents: { + [key: string]: any; + }; + + export = jsonContents; +} + +declare module '*.scss' { + // TODO: replace with: + // https://www.npmjs.com/package/css-modules-typescript-loader + // https://github.com/Jimdo/typings-for-css-modules-loader + const classNames: { + [className: string]: string; + }; + + export = classNames; +} + +declare module '*.css' { + const classNames: { + [className: string]: string; + }; + + export = classNames; +} diff --git a/babel.config.js b/babel.config.js index 563aed5..e1a0b0c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,9 @@ module.exports = { - presets: ['@babel/preset-react', '@babel/preset-flow', ['@babel/preset-env']], + presets: [ + '@babel/preset-react', + '@babel/preset-typescript', + ['@babel/preset-env'], + ], plugins: [ '@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-function-bind', @@ -28,11 +32,5 @@ module.exports = { ], ], }, - development: { - presets: [], - }, - test: { - presets: [], - }, }, }; diff --git a/flow-typed/Promise.js b/flow-typed/Promise.js deleted file mode 100644 index 4bf09fe..0000000 --- a/flow-typed/Promise.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This is a copypasted declaration from - * https://github.com/facebook/flow/blob/master/lib/core.js - * with addition of finally method - */ -declare class Promise<+R> { - constructor(callback: ( - resolve: (result: Promise | R) => void, - reject: (error: any) => void - ) => mixed): void; - - then( - onFulfill?: (value: R) => Promise | U, - onReject?: (error: any) => Promise | U - ): Promise; - - catch( - onReject?: (error: any) => Promise | U - ): Promise; - - static resolve(object: Promise | T): Promise; - static reject(error?: any): Promise; - static all>(promises: T): Promise<$TupleMap>; - static race | T>(promises: Array): Promise; - - finally( - onSettled?: ?(value: any) => Promise | T - ): Promise; -} diff --git a/flow-typed/npm/react-intl_v2.x.x.js b/flow-typed/npm/react-intl_v2.x.x.js deleted file mode 100644 index 38d6c10..0000000 --- a/flow-typed/npm/react-intl_v2.x.x.js +++ /dev/null @@ -1,278 +0,0 @@ -// flow-typed signature: 3902298e28ed22d8cd8d49828801a760 -// flow-typed version: eb50783110/react-intl_v2.x.x/flow_>=v0.63.x - -/** - * Original implementation of this file by @marudor at https://github.com/marudor/flowInterfaces - * Copied here based on intention to merge with flow-typed expressed here: - * https://github.com/marudor/flowInterfaces/issues/6 - */ -// Mostly from https://github.com/yahoo/react-intl/wiki/API#react-intl-api -declare module "react-intl" { - import type { Element, ChildrenArray } from "react"; - - declare type $npm$ReactIntl$LocaleData = { - locale: string, - [key: string]: any - }; - - declare type $npm$ReactIntl$MessageDescriptor = { - id: string, - description?: string, - defaultMessage?: string - }; - - declare type $npm$ReactIntl$IntlConfig = { - locale: string, - formats: Object, - messages: { [id: string]: string }, - - defaultLocale?: string, - defaultFormats?: Object - }; - - declare type $npm$ReactIntl$IntlProviderConfig = { - locale?: string, - formats?: Object, - messages?: { [id: string]: string }, - - defaultLocale?: string, - defaultFormats?: Object - }; - - declare type $npm$ReactIntl$IntlFormat = { - formatDate: (value: any, options?: Object) => string, - formatTime: (value: any, options?: Object) => string, - formatRelativeTime: (value: number, options?: $npm$ReactIntl$RelativeFormatOptions & { - format: string - }) => string, - formatNumber: (value: any, options?: Object) => string, - formatPlural: (value: any, options?: Object) => string, - formatMessage: ( - messageDescriptor: $npm$ReactIntl$MessageDescriptor, - values?: Object - ) => string, - formatHTMLMessage: ( - messageDescriptor: $npm$ReactIntl$MessageDescriptor, - values?: Object - ) => string - }; - - declare type $npm$ReactIntl$IntlShape = $npm$ReactIntl$IntlConfig & - $npm$ReactIntl$IntlFormat & { now: () => number }; - - declare type $npm$ReactIntl$DateTimeFormatOptions = { - localeMatcher?: "best fit" | "lookup", - formatMatcher?: "basic" | "best fit", - - timeZone?: string, - hour12?: boolean, - - weekday?: "narrow" | "short" | "long", - era?: "narrow" | "short" | "long", - year?: "numeric" | "2-digit", - month?: "numeric" | "2-digit" | "narrow" | "short" | "long", - day?: "numeric" | "2-digit", - hour?: "numeric" | "2-digit", - minute?: "numeric" | "2-digit", - second?: "numeric" | "2-digit", - timeZoneName?: "short" | "long" - }; - - declare type $npm$ReactIntl$RelativeFormatOptions = { - numeric?: "auto" | "always", - style?: "short" | "narrow" | "numeric", - unit?: "second" | "minute" | "hour" | "day" | "month" | "year" - }; - - declare type $npm$ReactIntl$NumberFormatOptions = { - localeMatcher?: "best fit" | "lookup", - - style?: "decimal" | "currency" | "percent", - - currency?: string, - currencyDisplay?: "symbol" | "code" | "name", - - useGrouping?: boolean, - - minimumIntegerDigits?: number, - minimumFractionDigits?: number, - maximumFractionDigits?: number, - minimumSignificantDigits?: number, - maximumSignificantDigits?: number - }; - - declare type $npm$ReactIntl$PluralFormatOptions = { - style?: "cardinal" | "ordinal" - }; - - declare type $npm$ReactIntl$PluralCategoryString = - | "zero" - | "one" - | "two" - | "few" - | "many" - | "other"; - - declare type $npm$ReactIntl$DateParseable = number | string | Date; - // PropType checker - declare function intlShape( - props: Object, - propName: string, - componentName: string - ): void; - declare function addLocaleData( - data: $npm$ReactIntl$LocaleData | Array<$npm$ReactIntl$LocaleData> - ): void; - declare function defineMessages< - T: { [key: string]: $Exact<$npm$ReactIntl$MessageDescriptor> } - >( - messageDescriptors: T - ): T; - - declare type InjectIntlProvidedProps = { - intl: $npm$ReactIntl$IntlShape - } - - declare type InjectIntlVoidProps = { - intl: $npm$ReactIntl$IntlShape | void - } - - declare type ComponentWithDefaultProps = - | React$ComponentType - | React$StatelessFunctionalComponent - | ChildrenArray>; - - declare type InjectIntlOptions = { - intlPropName?: string, - withRef?: boolean - } - - declare class IntlInjectedComponent extends React$Component { - static WrappedComponent: Class>, - static defaultProps: TDefaultProps, - props: TOwnProps - } - - declare type IntlInjectedComponentClass = Class< - IntlInjectedComponent - >; - - declare function injectIntl>( - WrappedComponent: Component, - options?: InjectIntlOptions, - ): React$ComponentType< - $Diff, InjectIntlVoidProps> - >; - - declare type IntlCache = any; - - declare function createIntlCache(): IntlCache; - - declare function createIntl(options: { - locale: string, - messages?: {[key: string]: string} - }, cache?: IntlCache): IntlShape; - - declare function formatMessage( - messageDescriptor: $npm$ReactIntl$MessageDescriptor, - values?: Object - ): string; - declare function formatHTMLMessage( - messageDescriptor: $npm$ReactIntl$MessageDescriptor, - values?: Object - ): string; - declare function formatDate( - value: any, - options?: $npm$ReactIntl$DateTimeFormatOptions & { format: string } - ): string; - declare function formatTime( - value: any, - options?: $npm$ReactIntl$DateTimeFormatOptions & { format: string } - ): string; - declare function formatRelativeTime( - value: number, // delta - options?: $npm$ReactIntl$RelativeFormatOptions & { - format: string - } - ): string; - declare function formatNumber( - value: any, - options?: $npm$ReactIntl$NumberFormatOptions & { format: string } - ): string; - declare function formatPlural( - value: any, - options?: $npm$ReactIntl$PluralFormatOptions - ): $npm$ReactIntl$PluralCategoryString; - - declare class FormattedMessage extends React$Component< - $npm$ReactIntl$MessageDescriptor & { - values?: Object, - tagName?: string, - children?: - | ((...formattedMessage: Array) => React$Node) - | (string => React$Node) - } - > {} - declare class FormattedHTMLMessage extends React$Component< - $npm$ReactIntl$DateTimeFormatOptions & { - values?: Object, - tagName?: string, - children?: (...formattedMessage: Array) => React$Node - } - > {} - declare class FormattedDate extends React$Component< - $npm$ReactIntl$DateTimeFormatOptions & { - value: $npm$ReactIntl$DateParseable, - format?: string, - children?: (formattedDate: string) => React$Node - } - > {} - declare class FormattedTime extends React$Component< - $npm$ReactIntl$DateTimeFormatOptions & { - value: $npm$ReactIntl$DateParseable, - format?: string, - children?: (formattedDate: string) => React$Node - } - > {} - declare class FormattedRelativeTime extends React$Component< - $npm$ReactIntl$RelativeFormatOptions & { - value: number, // delta - format?: string, - updateIntervalInSeconds?: number, - children?: (formattedDate: string) => React$Node - } - > {} - declare class FormattedNumber extends React$Component< - $npm$ReactIntl$NumberFormatOptions & { - value: number | string, - format?: string, - children?: (formattedNumber: string) => React$Node - } - > {} - declare class FormattedPlural extends React$Component< - $npm$ReactIntl$PluralFormatOptions & { - value: number | string, - other: React$Node, - zero?: React$Node, - one?: React$Node, - two?: React$Node, - few?: React$Node, - many?: React$Node, - children?: (formattedPlural: React$Node) => React$Node - } - > {} - declare class IntlProvider extends React$Component< - $npm$ReactIntl$IntlProviderConfig & { - children?: React$Node, - initialNow?: $npm$ReactIntl$DateParseable - } - > {} - declare class RawIntlProvider extends React$Component< - {| - children?: React$Node, - value?: IntlShape - |} - > {} - declare type IntlShape = $npm$ReactIntl$IntlShape; - declare type MessageDescriptor = $npm$ReactIntl$MessageDescriptor; -} diff --git a/flow-typed/npm/react-motion_vx.x.x.js b/flow-typed/npm/react-motion_vx.x.x.js deleted file mode 100644 index eb2876d..0000000 --- a/flow-typed/npm/react-motion_vx.x.x.js +++ /dev/null @@ -1,123 +0,0 @@ -// flow-typed signature: f7ed1ad96a453a021e6d98c1d144ef43 -// flow-typed version: <>/react-motion_v0.5.x/flow_v0.53.1 - -/** - * This is an autogenerated libdef stub for: - * - * 'react-motion' - * - * Fill this stub out by replacing all the `any` types. - * - * Once filled out, we encourage you to share your work with the - * community by sending a pull request to: - * https://github.com/flowtype/flow-typed - */ - -declare module 'react-motion' { - declare module.exports: any; -} - -/** - * We include stubs for each file inside this npm package in case you need to - * require those files directly. Feel free to delete any files that aren't - * needed. - */ -declare module 'react-motion/build/react-motion' { - declare module.exports: any; -} - -declare module 'react-motion/lib/mapToZero' { - declare module.exports: any; -} - -declare module 'react-motion/lib/mergeDiff' { - declare module.exports: any; -} - -declare module 'react-motion/lib/Motion' { - declare module.exports: any; -} - -declare module 'react-motion/lib/presets' { - declare module.exports: any; -} - -declare module 'react-motion/lib/react-motion' { - declare module.exports: any; -} - -declare module 'react-motion/lib/reorderKeys' { - declare module.exports: any; -} - -declare module 'react-motion/lib/shouldStopAnimation' { - declare module.exports: any; -} - -declare module 'react-motion/lib/spring' { - declare module.exports: any; -} - -declare module 'react-motion/lib/StaggeredMotion' { - declare module.exports: any; -} - -declare module 'react-motion/lib/stepper' { - declare module.exports: any; -} - -declare module 'react-motion/lib/stripStyle' { - declare module.exports: any; -} - -declare module 'react-motion/lib/TransitionMotion' { - declare module.exports: any; -} - -declare module 'react-motion/lib/Types' { - declare module.exports: any; -} - -// Filename aliases -declare module 'react-motion/build/react-motion.js' { - declare module.exports: $Exports<'react-motion/build/react-motion'>; -} -declare module 'react-motion/lib/mapToZero.js' { - declare module.exports: $Exports<'react-motion/lib/mapToZero'>; -} -declare module 'react-motion/lib/mergeDiff.js' { - declare module.exports: $Exports<'react-motion/lib/mergeDiff'>; -} -declare module 'react-motion/lib/Motion.js' { - declare module.exports: $Exports<'react-motion/lib/Motion'>; -} -declare module 'react-motion/lib/presets.js' { - declare module.exports: $Exports<'react-motion/lib/presets'>; -} -declare module 'react-motion/lib/react-motion.js' { - declare module.exports: $Exports<'react-motion/lib/react-motion'>; -} -declare module 'react-motion/lib/reorderKeys.js' { - declare module.exports: $Exports<'react-motion/lib/reorderKeys'>; -} -declare module 'react-motion/lib/shouldStopAnimation.js' { - declare module.exports: $Exports<'react-motion/lib/shouldStopAnimation'>; -} -declare module 'react-motion/lib/spring.js' { - declare module.exports: $Exports<'react-motion/lib/spring'>; -} -declare module 'react-motion/lib/StaggeredMotion.js' { - declare module.exports: $Exports<'react-motion/lib/StaggeredMotion'>; -} -declare module 'react-motion/lib/stepper.js' { - declare module.exports: $Exports<'react-motion/lib/stepper'>; -} -declare module 'react-motion/lib/stripStyle.js' { - declare module.exports: $Exports<'react-motion/lib/stripStyle'>; -} -declare module 'react-motion/lib/TransitionMotion.js' { - declare module.exports: $Exports<'react-motion/lib/TransitionMotion'>; -} -declare module 'react-motion/lib/Types.js' { - declare module.exports: $Exports<'react-motion/lib/Types'>; -} diff --git a/flow-typed/npm/react-redux_v5.x.x.js b/flow-typed/npm/react-redux_v5.x.x.js deleted file mode 100644 index eeeab17..0000000 --- a/flow-typed/npm/react-redux_v5.x.x.js +++ /dev/null @@ -1,276 +0,0 @@ -// flow-typed signature: f06f00c3ad0cfedb90c0c6de04b219f3 -// flow-typed version: 3a6d556e4b/react-redux_v5.x.x/flow_>=v0.89.x - -/** -The order of type arguments for connect() is as follows: - -connect(…) - -In Flow v0.89 only the first two are mandatory to specify. Other 4 can be repaced with the new awesome type placeholder: - -connect(…) - -But beware, in case of weird type errors somewhere in random places -just type everything and get to a green field and only then try to -remove the definitions you see bogus. - -Decrypting the abbreviations: - WC = Component being wrapped - S = State - D = Dispatch - OP = OwnProps - SP = StateProps - DP = DispatchProps - MP = Merge props - RSP = Returned state props - RDP = Returned dispatch props - RMP = Returned merge props - CP = Props for returned component - Com = React Component - ST = Static properties of Com - EFO = Extra factory options (used only in connectAdvanced) -*/ - -declare module "react-redux" { - // ------------------------------------------------------------ - // Typings for connect() - // ------------------------------------------------------------ - - declare export type Options = {| - pure?: boolean, - withRef?: boolean, - areStatesEqual?: (next: S, prev: S) => boolean, - areOwnPropsEqual?: (next: OP, prev: OP) => boolean, - areStatePropsEqual?: (next: SP, prev: SP) => boolean, - areMergedPropsEqual?: (next: MP, prev: MP) => boolean, - storeKey?: string, - |}; - - declare type MapStateToProps<-S, -OP, +SP> = - | ((state: S, ownProps: OP) => SP) - // If you want to use the factory function but get a strange error - // like "function is not an object" then just type the factory function - // like this: - // const factory: (State, OwnProps) => (State, OwnProps) => StateProps - // and provide the StateProps type to the SP type parameter. - | ((state: S, ownProps: OP) => (state: S, ownProps: OP) => SP); - - declare type Bind = ((...A) => R) => (...A) => $Call; - - declare type MapDispatchToPropsFn = - | ((dispatch: D, ownProps: OP) => DP) - // If you want to use the factory function but get a strange error - // like "function is not an object" then just type the factory function - // like this: - // const factory: (Dispatch, OwnProps) => (Dispatch, OwnProps) => DispatchProps - // and provide the DispatchProps type to the DP type parameter. - | ((dispatch: D, ownProps: OP) => (dispatch: D, ownProps: OP) => DP); - - declare class ConnectedComponent extends React$Component { - static +WrappedComponent: WC; - getWrappedInstance(): React$ElementRef; - } - // The connection of the Wrapped Component and the Connected Component - // happens here in `MP: P`. It means that type wise MP belongs to P, - // so to say MP >= P. - declare type Connector = >( - WC, - ) => Class> & WC; - - // No `mergeProps` argument - - // Got error like inexact OwnProps is incompatible with exact object type? - // Just make the OP parameter for `connect()` an exact object. - declare type MergeOP = {| ...$Exact, dispatch: D |}; - declare type MergeOPSP = {| ...$Exact, ...SP, dispatch: D |}; - declare type MergeOPDP = {| ...$Exact, ...DP |}; - declare type MergeOPSPDP = {| ...$Exact, ...SP, ...DP |}; - - declare export function connect<-P, -OP, -SP, -DP, -S, -D>( - mapStateToProps?: null | void, - mapDispatchToProps?: null | void, - mergeProps?: null | void, - options?: ?Options>, - ): Connector>; - - declare export function connect<-P, -OP, -SP, -DP, -S, -D>( - // If you get error here try adding return type to your mapStateToProps function - mapStateToProps: MapStateToProps, - mapDispatchToProps?: null | void, - mergeProps?: null | void, - options?: ?Options>, - ): Connector>; - - // In this case DP is an object of functions which has been bound to dispatch - // by the given mapDispatchToProps function. - declare export function connect<-P, -OP, -SP, -DP, S, D>( - mapStateToProps: null | void, - mapDispatchToProps: MapDispatchToPropsFn, - mergeProps?: null | void, - options?: ?Options>, - ): Connector>; - - // In this case DP is an object of action creators not yet bound to dispatch, - // this difference is not important in the vanila redux, - // but in case of usage with redux-thunk, the return type may differ. - declare export function connect<-P, -OP, -SP, -DP, S, D>( - mapStateToProps: null | void, - mapDispatchToProps: DP, - mergeProps?: null | void, - options?: ?Options>, - ): Connector>>>; - - declare export function connect<-P, -OP, -SP, -DP, S, D>( - // If you get error here try adding return type to your mapStateToProps function - mapStateToProps: MapStateToProps, - mapDispatchToProps: MapDispatchToPropsFn, - mergeProps?: null | void, - options?: ?Options, - ): Connector; - - declare export function connect<-P, -OP, -SP, -DP, S, D>( - // If you get error here try adding return type to your mapStateToProps function - mapStateToProps: MapStateToProps, - mapDispatchToProps: DP, - mergeProps?: null | void, - options?: ?Options>, - ): Connector>>>; - - // With `mergeProps` argument - - declare type MergeProps<+P, -OP, -SP, -DP> = ( - stateProps: SP, - dispatchProps: DP, - ownProps: OP, - ) => P; - - declare export function connect<-P, -OP, -SP: {||}, -DP: {||}, S, D>( - mapStateToProps: null | void, - mapDispatchToProps: null | void, - // If you get error here try adding return type to you mapStateToProps function - mergeProps: MergeProps, - options?: ?Options, - ): Connector; - - declare export function connect<-P, -OP, -SP, -DP: {||}, S, D>( - mapStateToProps: MapStateToProps, - mapDispatchToProps: null | void, - // If you get error here try adding return type to you mapStateToProps function - mergeProps: MergeProps, - options?: ?Options, - ): Connector; - - // In this case DP is an object of functions which has been bound to dispatch - // by the given mapDispatchToProps function. - declare export function connect<-P, -OP, -SP: {||}, -DP, S, D>( - mapStateToProps: null | void, - mapDispatchToProps: MapDispatchToPropsFn, - mergeProps: MergeProps, - options?: ?Options, - ): Connector; - - // In this case DP is an object of action creators not yet bound to dispatch, - // this difference is not important in the vanila redux, - // but in case of usage with redux-thunk, the return type may differ. - declare export function connect<-P, -OP, -SP: {||}, -DP, S, D>( - mapStateToProps: null | void, - mapDispatchToProps: DP, - mergeProps: MergeProps>>, - options?: ?Options, - ): Connector; - - // In this case DP is an object of functions which has been bound to dispatch - // by the given mapDispatchToProps function. - declare export function connect<-P, -OP, -SP, -DP, S, D>( - mapStateToProps: MapStateToProps, - mapDispatchToProps: MapDispatchToPropsFn, - mergeProps: MergeProps, - options?: ?Options, - ): Connector; - - // In this case DP is an object of action creators not yet bound to dispatch, - // this difference is not important in the vanila redux, - // but in case of usage with redux-thunk, the return type may differ. - declare export function connect<-P, -OP, -SP, -DP, S, D>( - mapStateToProps: MapStateToProps, - mapDispatchToProps: DP, - mergeProps: MergeProps>>, - options?: ?Options, - ): Connector; - - // ------------------------------------------------------------ - // Typings for Provider - // ------------------------------------------------------------ - - declare export class Provider extends React$Component<{ - store: Store, - children?: React$Node, - }> {} - - declare export function createProvider( - storeKey?: string, - subKey?: string, - ): Class>; - - // ------------------------------------------------------------ - // Typings for connectAdvanced() - // ------------------------------------------------------------ - - declare type ConnectAdvancedOptions = { - getDisplayName?: (name: string) => string, - methodName?: string, - renderCountProp?: string, - shouldHandleStateChanges?: boolean, - storeKey?: string, - withRef?: boolean, - }; - - declare type SelectorFactoryOptions = { - getDisplayName: (name: string) => string, - methodName: string, - renderCountProp: ?string, - shouldHandleStateChanges: boolean, - storeKey: string, - withRef: boolean, - displayName: string, - wrappedComponentName: string, - WrappedComponent: Com, - }; - - declare type MapStateToPropsEx = ( - state: S, - props: SP, - ) => RSP; - - declare type SelectorFactory< - Com: React$ComponentType<*>, - Dispatch, - S: Object, - OP: Object, - EFO: Object, - CP: Object, - > = ( - dispatch: Dispatch, - factoryOptions: SelectorFactoryOptions & EFO, - ) => MapStateToPropsEx; - - declare export function connectAdvanced< - Com: React$ComponentType<*>, - D, - S: Object, - OP: Object, - CP: Object, - EFO: Object, - ST: { [_: $Keys]: any }, - >( - selectorFactory: SelectorFactory, - connectAdvancedOptions: ?(ConnectAdvancedOptions & EFO), - ): (component: Com) => React$ComponentType & $Shape; - - declare export default { - Provider: typeof Provider, - createProvider: typeof createProvider, - connect: typeof connect, - connectAdvanced: typeof connectAdvanced, - }; -} diff --git a/flow-typed/npm/react-router-dom_v5.x.x.js b/flow-typed/npm/react-router-dom_v5.x.x.js deleted file mode 100644 index 048fc19..0000000 --- a/flow-typed/npm/react-router-dom_v5.x.x.js +++ /dev/null @@ -1,176 +0,0 @@ -// flow-typed signature: 9987f80c12a2cad7dfa2b08cc14d2edc -// flow-typed version: 2973a15489/react-router-dom_v5.x.x/flow_>=v0.98.x - -declare module "react-router-dom" { - declare export var BrowserRouter: React$ComponentType<{| - basename?: string, - forceRefresh?: boolean, - getUserConfirmation?: GetUserConfirmation, - keyLength?: number, - children?: React$Node - |}> - - declare export var HashRouter: React$ComponentType<{| - basename?: string, - getUserConfirmation?: GetUserConfirmation, - hashType?: "slash" | "noslash" | "hashbang", - children?: React$Node - |}> - - declare export var Link: React$ComponentType<{ - className?: string, - to: string | LocationShape, - replace?: boolean, - children?: React$Node - }> - - declare export var NavLink: React$ComponentType<{ - to: string | LocationShape, - activeClassName?: string, - className?: string, - activeStyle?: { +[string]: mixed }, - style?: { +[string]: mixed }, - isActive?: (match: Match, location: Location) => boolean, - children?: React$Node, - exact?: boolean, - strict?: boolean - }> - - // NOTE: Below are duplicated from react-router. If updating these, please - // update the react-router and react-router-native types as well. - declare export type Location = { - pathname: string, - search: string, - hash: string, - state?: any, - key?: string - }; - - declare export type LocationShape = { - pathname?: string, - search?: string, - hash?: string, - state?: any - }; - - declare export type HistoryAction = "PUSH" | "REPLACE" | "POP"; - - declare export type RouterHistory = { - length: number, - location: Location, - action: HistoryAction, - listen( - callback: (location: Location, action: HistoryAction) => void - ): () => void, - push(path: string | LocationShape, state?: any): void, - replace(path: string | LocationShape, state?: any): void, - go(n: number): void, - goBack(): void, - goForward(): void, - canGo?: (n: number) => boolean, - block( - callback: string | (location: Location, action: HistoryAction) => ?string - ): () => void, - // createMemoryHistory - index?: number, - entries?: Array - }; - - declare export type Match = { - params: { [key: string]: ?string }, - isExact: boolean, - path: string, - url: string - }; - - declare export type ContextRouter = {| - history: RouterHistory, - location: Location, - match: Match, - staticContext?: StaticRouterContext - |}; - - declare type ContextRouterVoid = { - history: RouterHistory | void, - location: Location | void, - match: Match | void, - staticContext?: StaticRouterContext | void - }; - - declare export type GetUserConfirmation = ( - message: string, - callback: (confirmed: boolean) => void - ) => void; - - declare export type StaticRouterContext = { - url?: string - }; - - declare export var StaticRouter: React$ComponentType<{| - basename?: string, - location?: string | Location, - context: StaticRouterContext, - children?: React$Node - |}> - - declare export var MemoryRouter: React$ComponentType<{| - initialEntries?: Array, - initialIndex?: number, - getUserConfirmation?: GetUserConfirmation, - keyLength?: number, - children?: React$Node - |}> - - declare export var Router: React$ComponentType<{| - history: RouterHistory, - children?: React$Node - |}> - - declare export var Prompt: React$ComponentType<{| - message: string | ((location: Location) => string | boolean), - when?: boolean - |}> - - declare export var Redirect: React$ComponentType<{| - to: string | LocationShape, - push?: boolean, - from?: string, - exact?: boolean, - strict?: boolean - |}> - - declare export var Route: React$ComponentType<{| - component?: React$ComponentType<*>, - render?: (router: ContextRouter) => React$Node, - children?: React$ComponentType | React$Node, - path?: string | Array, - exact?: boolean, - strict?: boolean, - location?: LocationShape, - sensitive?: boolean - |}> - - declare export var Switch: React$ComponentType<{| - children?: React$Node, - location?: Location - |}> - - declare export function withRouter>( - WrappedComponent: Component - ): React$ComponentType<$Diff, ContextRouterVoid>>; - - declare type MatchPathOptions = { - path?: string, - exact?: boolean, - sensitive?: boolean, - strict?: boolean - }; - - declare export function matchPath( - pathname: string, - options?: MatchPathOptions | string, - parent?: Match - ): null | Match; - - declare export function generatePath(pattern?: string, params?: { +[string]: mixed }): string; -} diff --git a/flow-typed/npm/redux_v4.x.x.js b/flow-typed/npm/redux_v4.x.x.js deleted file mode 100644 index 2ba2c8b..0000000 --- a/flow-typed/npm/redux_v4.x.x.js +++ /dev/null @@ -1,100 +0,0 @@ -// flow-typed signature: a49a6c96fe8a8bb3330cce2028588f4c -// flow-typed version: de5b3a01c6/redux_v4.x.x/flow_>=v0.89.x - -declare module 'redux' { - /* - - S = State - A = Action - D = Dispatch - - */ - - declare export type Action = { - type: T - } - - declare export type DispatchAPI = (action: A) => A; - - declare export type Dispatch = DispatchAPI; - - declare export type MiddlewareAPI> = { - dispatch: D, - getState(): S, - }; - - declare export type Store> = { - // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) - dispatch: D, - getState(): S, - subscribe(listener: () => void): () => void, - replaceReducer(nextReducer: Reducer): void, - }; - - declare export type Reducer = (state: S | void, action: A) => S; - - declare export type CombinedReducer = ( - state: ($Shape & {}) | void, - action: A - ) => S; - - declare export type Middleware> = ( - api: MiddlewareAPI - ) => (next: D) => D; - - declare export type StoreCreator> = { - (reducer: Reducer, enhancer?: StoreEnhancer): Store, - ( - reducer: Reducer, - preloadedState: S, - enhancer?: StoreEnhancer - ): Store, - }; - - declare export type StoreEnhancer> = ( - next: StoreCreator - ) => StoreCreator; - - declare export function createStore( - reducer: Reducer, - enhancer?: StoreEnhancer - ): Store; - declare export function createStore( - reducer: Reducer, - preloadedState?: S, - enhancer?: StoreEnhancer - ): Store; - - declare export function applyMiddleware( - ...middlewares: Array> - ): StoreEnhancer; - - declare export type ActionCreator = (...args: Array) => A; - declare export type ActionCreators = { - [key: K]: ActionCreator, - }; - - declare export function bindActionCreators< - A, - C: ActionCreator, - D: DispatchAPI - >( - actionCreator: C, - dispatch: D - ): C; - declare export function bindActionCreators< - A, - K, - C: ActionCreators, - D: DispatchAPI - >( - actionCreators: C, - dispatch: D - ): C; - - declare export function combineReducers( - reducers: O - ): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; - - declare export var compose: $Compose; -} diff --git a/jest/__mocks__/intlMock.js b/jest/__mocks__/intlMock.js index 77e9e1c..733a57f 100644 --- a/jest/__mocks__/intlMock.js +++ b/jest/__mocks__/intlMock.js @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-env node */ const path = require('path'); const { transform } = require('../../webpack-utils/intl-loader'); @@ -11,7 +13,6 @@ module.exports = { * * @returns {string} */ - // eslint-disable-next-line no-unused-vars process(src, filename, config, options) { return transform(src, filename, path.resolve(`${__dirname}/../../..`)); }, diff --git a/package.json b/package.json index b65551d..009874f 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "lint:check": "eslint --quiet .", "prettier": "prettier --write \"{src/**/*,scripts/**/*,tests-e2e/**/*,webpack-utils/**/*,jest/**/*,config/**/*,*}.{js,ts,tsx,json,md,scss,css}\"", "prettier:check": "prettier --check \"{src/**/*,scripts/**/*,tests-e2e/**/*,webpack-utils/**/*,jest/**/*,config/**/*,*}.{js,ts,tsx,json,md,scss,css}\"", - "flow:check": "flow", - "ci:check": "yarn lint:check && yarn flow:check && yarn test", + "ts:check": "tsc", + "ci:check": "yarn lint:check && yarn ts:check && yarn test", "analyze": "yarn run clean && yarn run build:webpack --analyze", "i18n:collect": "babel-node ./scripts/i18n-collect.js", "i18n:push": "babel-node ./scripts/i18n-crowdin.js push", @@ -90,6 +90,8 @@ "@formatjs/intl-pluralrules": "^1.3.7", "@formatjs/intl-relativetimeformat": "^4.4.6", "@hot-loader/react-dom": "^16.11.0", + "@types/react-redux": "^7.1.5", + "@types/react-router-dom": "^5.1.3", "classnames": "^2.2.6", "copy-to-clipboard": "^3.0.8", "debounce": "^1.0.0", @@ -138,10 +140,13 @@ "@babel/plugin-syntax-import-meta": "^7.7.4", "@babel/plugin-transform-runtime": "^7.7.4", "@babel/preset-env": "^7.7.4", - "@babel/preset-flow": "^7.7.4", "@babel/preset-react": "^7.7.4", + "@babel/preset-typescript": "^7.7.4", "@babel/runtime-corejs3": "^7.7.4", - "babel-eslint": "^10.0.3", + "@types/jest": "^24.0.23", + "@types/webpack-env": "^1.14.1", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-loader": "^8.0.0", "babel-plugin-react-intl": "^5.1.8", "core-js": "3.4.3", @@ -154,13 +159,11 @@ "enzyme-adapter-react-16": "^1.15.1", "eslint": "^6.7.1", "eslint-config-prettier": "^6.7.0", - "eslint-plugin-flowtype": "^4.5.2", "eslint-plugin-jsdoc": "^18.1.5", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-react": "^7.16.0", "exports-loader": "^0.7.0", "file-loader": "^4.2.0", - "flow-bin": "~0.102.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "husky": "^3.1.0", @@ -184,6 +187,7 @@ "sitemap-webpack-plugin": "^0.6.0", "speed-measure-webpack-plugin": "^1.3.1", "style-loader": "^1.0.0", + "typescript": "^3.7.2", "unexpected": "^11.8.1", "unexpected-sinon": "^10.5.1", "url-loader": "^2.2.0", diff --git a/postcss.config.js b/postcss.config.js index 58deefe..3d8d4d8 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-env node */ const path = require('path'); const loaderUtils = require('loader-utils'); diff --git a/scripts/build-dll.js b/scripts/build-dll.js index 57b3a22..71afbb8 100644 --- a/scripts/build-dll.js +++ b/scripts/build-dll.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-env node */ /* eslint-disable no-console */ diff --git a/scripts/i18n-collect.js b/scripts/i18n-collect.ts similarity index 89% rename from scripts/i18n-collect.js rename to scripts/i18n-collect.ts index 4349129..a2befc4 100644 --- a/scripts/i18n-collect.js +++ b/scripts/i18n-collect.ts @@ -6,7 +6,7 @@ import { sync as mkdirpSync } from 'mkdirp'; import chalk from 'chalk'; import prompt from 'prompt'; -import localesMap from './../src/i18n/index.json'; +import localesMap from './../src/i18n'; const MESSAGES_PATTERN = `${__dirname}/../dist/messages/**/*.json`; const LANG_DIR = `${__dirname}/../src/i18n`; @@ -19,8 +19,8 @@ const SUPPORTED_LANGS = [DEFAULT_LOCALE, ...Object.keys(localesMap)]; * there are messages in different components that use the same `id`. The result * is a flat collection of `id: message` pairs for the app's default locale. */ -let idToFileMap = {}; -let duplicateIds = []; +let idToFileMap: { [key: string]: string[] } = {}; +let duplicateIds: string[] = []; const collectedMessages = globSync(MESSAGES_PATTERN) .map(filename => [filename, JSON.parse(fs.readFileSync(filename, 'utf8'))]) .reduce((collection, [file, descriptors]) => { @@ -42,23 +42,24 @@ if (duplicateIds.length) { console.log(`${chalk.yellow(id)}:\n - ${idToFileMap[id].join('\n - ')}\n`), ); console.log(chalk.red('Please correct the errors above to proceed further!')); - process.exit(); + + process.exit(1); } -duplicateIds = null; -idToFileMap = null; +duplicateIds = []; +idToFileMap = {}; /** * Making a diff with the previous DEFAULT_LOCALE version */ const defaultMessagesPath = `${LANG_DIR}/${DEFAULT_LOCALE}.json`; -let keysToUpdate = []; -let keysToAdd = []; -let keysToRemove = []; -const keysToRename = []; -const isNotMarked = value => value.slice(0, 2) !== '--'; +let keysToUpdate: string[] = []; +let keysToAdd: string[] = []; +let keysToRemove: string[] = []; +const keysToRename: Array<[string, string]> = []; +const isNotMarked = (value: string) => value.slice(0, 2) !== '--'; -const prevMessages = readJSON(defaultMessagesPath); +const prevMessages: { [key: string]: string } = readJSON(defaultMessagesPath); const prevMessagesMap = Object.entries(prevMessages).reduce( (acc, [key, value]) => { if (acc[value]) { @@ -69,8 +70,9 @@ const prevMessagesMap = Object.entries(prevMessages).reduce( return acc; }, - {}, + {} as { [key: string]: string[] }, ); + keysToAdd = Object.keys(collectedMessages).filter(key => !prevMessages[key]); keysToRemove = Object.keys(prevMessages) .filter(key => !collectedMessages[key]) @@ -80,13 +82,13 @@ keysToUpdate = Object.entries(prevMessages).reduce( acc.concat( collectedMessages[key] && collectedMessages[key] !== message ? key : [], ), - [], + [] as string[], ); // detect keys to rename, mutating keysToAdd and keysToRemove -[].concat(keysToAdd).forEach(toKey => { +[...keysToAdd].forEach(toKey => { const keys = prevMessagesMap[collectedMessages[toKey]] || []; - const fromKey = keys.find(fromKey => keysToRemove.indexOf(fromKey) > -1); + const fromKey = keys.find(key => keysToRemove.indexOf(key) > -1); if (fromKey) { keysToRename.push([fromKey, toKey]); diff --git a/scripts/i18n-crowdin.js b/scripts/i18n-crowdin.ts similarity index 86% rename from scripts/i18n-crowdin.js rename to scripts/i18n-crowdin.ts index b9f0efd..9cd07b3 100644 --- a/scripts/i18n-crowdin.js +++ b/scripts/i18n-crowdin.ts @@ -1,4 +1,3 @@ -// @flow /* eslint-env node */ /* eslint-disable no-console */ @@ -22,7 +21,7 @@ const PROJECT_KEY = config.crowdinApiKey; const CROWDIN_FILE_PATH = 'accounts/site.json'; const SOURCE_LANG = 'en'; const LANG_DIR = path.resolve(`${__dirname}/../src/i18n`); -const INDEX_FILE_NAME = 'index.json'; +const INDEX_FILE_NAME = 'index.js'; const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published const crowdin = new CrowdinApi({ apiKey: PROJECT_KEY }); @@ -81,8 +80,13 @@ function toInternalLocale(code: string): string { * @param {object} translates * @returns {string} */ -function serializeToFormattedJson(translates: Object): string { - return JSON.stringify(sortByKeys(translates), null, 4) + '\n'; // eslint-disable-line prefer-template +function serializeToFormattedJson( + translates: { [key: string]: any }, + { asModule = false }: { asModule?: boolean } = {}, +): string { + const src = JSON.stringify(sortByKeys(translates), null, 2); + + return asModule ? `module.exports = ${src};\n` : `${src}\n`; } /** @@ -91,7 +95,7 @@ function serializeToFormattedJson(translates: Object): string { * @param {object} object * @returns {object} */ -function sortByKeys(object: Object): Object { +function sortByKeys(object: { [key: string]: any }): { [key: string]: any } { return Object.keys(object) .sort() .reduce((result, key) => { @@ -121,32 +125,32 @@ interface ProjectInfoDirectory { interface ProjectInfoResponse { details: { source_language: { - name: string, - code: string, - }, - name: string, - identifier: string, - created: string, - description: string, - join_policy: string, - last_build: string | null, - last_activity: string, - participants_count: string, // it's number, but string in the response - logo_url: string | null, - total_strings_count: string, // it's number, but string in the response - total_words_count: string, // it's number, but string in the response - duplicate_strings_count: number, - duplicate_words_count: number, + name: string; + code: string; + }; + name: string; + identifier: string; + created: string; + description: string; + join_policy: string; + last_build: string | null; + last_activity: string; + participants_count: string; // it's number, but string in the response + logo_url: string | null; + total_strings_count: string; // it's number, but string in the response + total_words_count: string; // it's number, but string in the response + duplicate_strings_count: number; + duplicate_words_count: number; invite_url: { - translator: string, - proofreader: string, - }, + translator: string; + proofreader: string; + }; }; languages: Array<{ - name: string, // English language name - code: string, - can_translate: 0 | 1, - can_approve: 0 | 1, + name: string; // English language name + code: string; + can_translate: 0 | 1; + can_approve: 0 | 1; }>; files: Array; } @@ -277,7 +281,7 @@ async function pull() { console.log('Locales are downloaded. Writing them to file system.'); - const indexFileEntries: { [string]: IndexFileEntry } = { + const indexFileEntries: { [key: string]: IndexFileEntry } = { en: { code: 'en', name: 'English', @@ -286,7 +290,6 @@ async function pull() { isReleased: true, }, }; - // $FlowFixMe await Promise.all( results.map( result => @@ -328,7 +331,7 @@ async function pull() { fs.writeFileSync( path.join(LANG_DIR, INDEX_FILE_NAME), - serializeToFormattedJson(indexFileEntries), + serializeToFormattedJson(indexFileEntries, { asModule: true }), ); console.log(ch.green('The index file was successfully written')); @@ -364,7 +367,7 @@ function push() { [CROWDIN_FILE_PATH]: path.join(LANG_DIR, `${SOURCE_LANG}.json`), }, { - // eslint-disable-next-line camelcase + // eslint-disable-next-line @typescript-eslint/camelcase update_option: disapprove ? 'update_as_unapproved' : 'update_without_changes', diff --git a/src/App.js b/src/App.tsx similarity index 87% rename from src/App.js rename to src/App.tsx index 3ba4a64..830cc2c 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,14 +1,20 @@ -// @flow import React from 'react'; import { hot } from 'react-hot-loader/root'; import { Provider as ReduxProvider } from 'react-redux'; import { Router, Route, Switch } from 'react-router-dom'; import { IntlProvider } from 'components/i18n'; +import { Store } from 'redux'; import AuthFlowRoute from 'containers/AuthFlowRoute'; import RootPage from 'pages/root/RootPage'; import SuccessOauthPage from 'pages/auth/SuccessOauthPage'; -const App = ({ store, browserHistory }) => ( +const App = ({ + store, + browserHistory, +}: { + store: Store; + browserHistory: any; +}) => ( diff --git a/src/components/MeasureHeight.js b/src/components/MeasureHeight.tsx similarity index 78% rename from src/components/MeasureHeight.js rename to src/components/MeasureHeight.tsx index 6de4bbd..bf55a6f 100644 --- a/src/components/MeasureHeight.js +++ b/src/components/MeasureHeight.tsx @@ -1,6 +1,4 @@ -// @flow -import React, { PureComponent } from 'react'; - +import React from 'react'; import { omit, debounce } from 'functions'; /** @@ -23,20 +21,22 @@ import { omit, debounce } from 'functions'; * */ -type ChildState = mixed; +type ChildState = any; -export default class MeasureHeight extends PureComponent<{ - shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean, - onMeasure: (height: number) => void, - state: ChildState, -}> { +export default class MeasureHeight extends React.PureComponent< + { + shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean; + onMeasure: (height: number) => void; + state: ChildState; + } & React.HTMLAttributes +> { static defaultProps = { shouldMeasure: (prevState: ChildState, newState: ChildState) => prevState !== newState, - onMeasure: (height: number) => {}, // eslint-disable-line + onMeasure: () => {}, }; - el: ?HTMLDivElement; + el: HTMLDivElement | null = null; componentDidMount() { // we want to measure height immediately on first mount to avoid ui laggs @@ -55,11 +55,7 @@ export default class MeasureHeight extends PureComponent<{ } render() { - const props: Object = omit(this.props, [ - 'shouldMeasure', - 'onMeasure', - 'state', - ]); + const props = omit(this.props, ['shouldMeasure', 'onMeasure', 'state']); return
(this.el = el)} />; } diff --git a/src/components/accounts/AccountSwitcher.js b/src/components/accounts/AccountSwitcher.tsx similarity index 80% rename from src/components/accounts/AccountSwitcher.js rename to src/components/accounts/AccountSwitcher.tsx index a34e1d3..b6389d8 100644 --- a/src/components/accounts/AccountSwitcher.js +++ b/src/components/accounts/AccountSwitcher.tsx @@ -1,34 +1,37 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; import { FormattedMessage as Message } from 'react-intl'; import loader from 'services/loader'; -import { skins, SKIN_DARK, COLOR_WHITE } from 'components/ui'; +import { SKIN_DARK, COLOR_WHITE, Skin } from 'components/ui'; import { Button } from 'components/ui/form'; import { authenticate, revoke } from 'components/accounts/actions'; -import { getActiveAccount } from 'components/accounts/reducer'; +import { getActiveAccount, Account } from 'components/accounts/reducer'; +import { RootState } from 'reducers'; import styles from './accountSwitcher.scss'; import messages from './AccountSwitcher.intl.json'; -export class AccountSwitcher extends Component { - static displayName = 'AccountSwitcher'; +interface Props { + switchAccount: (account: Account) => Promise; + removeAccount: (account: Account) => Promise; + // called after each action performed + onAfterAction: () => void; + // called after switching an account. The active account will be passed as arg + onSwitch: (account: Account) => void; + accounts: RootState['accounts']; + skin: Skin; + // whether active account should be expanded and shown on the top + highlightActiveAccount: boolean; + // whether to show logout icon near each account + allowLogout: boolean; + // whether to show add account button + allowAdd: boolean; +} - static propTypes = { - switchAccount: PropTypes.func.isRequired, - removeAccount: PropTypes.func.isRequired, - onAfterAction: PropTypes.func, // called after each action performed - onSwitch: PropTypes.func, // called after switching an account. The active account will be passed as arg - accounts: PropTypes.object, // eslint-disable-line - skin: PropTypes.oneOf(skins), - highlightActiveAccount: PropTypes.bool, // whether active account should be expanded and shown on the top - allowLogout: PropTypes.bool, // whether to show logout icon near each account - allowAdd: PropTypes.bool, // whether to show add account button - }; - - static defaultProps = { +export class AccountSwitcher extends React.Component { + static defaultProps: Partial = { skin: SKIN_DARK, highlightActiveAccount: true, allowLogout: true, @@ -47,6 +50,10 @@ export class AccountSwitcher extends Component { } = this.props; const activeAccount = getActiveAccount({ accounts }); + if (!activeAccount) { + throw new Error('Can not find active account'); + } + let { available } = accounts; if (highlightActiveAccount) { @@ -157,7 +164,7 @@ export class AccountSwitcher extends Component { ); } - onSwitch = account => event => { + onSwitch = (account: Account) => (event: React.MouseEvent) => { event.preventDefault(); loader.show(); @@ -172,7 +179,7 @@ export class AccountSwitcher extends Component { .finally(() => loader.hide()); }; - onRemove = account => event => { + onRemove = (account: Account) => (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -181,7 +188,7 @@ export class AccountSwitcher extends Component { } export default connect( - ({ accounts }) => ({ + ({ accounts }: RootState) => ({ accounts, }), { diff --git a/src/components/accounts/actions.test.js b/src/components/accounts/actions.test.ts similarity index 77% rename from src/components/accounts/actions.test.js rename to src/components/accounts/actions.test.ts index 01cea29..72ab522 100644 --- a/src/components/accounts/actions.test.js +++ b/src/components/accounts/actions.test.ts @@ -1,8 +1,6 @@ import expect from 'test/unexpected'; import sinon from 'sinon'; - import { browserHistory } from 'services/history'; - import { InternalServerError } from 'services/request'; import { sessionStorage } from 'services/localStorage'; import * as authentication from 'services/api/authentication'; @@ -21,9 +19,11 @@ import { reset, } from 'components/accounts/actions/pure-actions'; import { SET_LOCALE } from 'components/i18n/actions'; - import { updateUser, setUser } from 'components/user/actions'; import { setLogin, setAccountSwitcher } from 'components/auth/actions'; +import { Dispatch, RootState } from 'reducers'; + +import { Account } from './reducer'; const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbHl8MSJ9.pRJ7vakt2eIscjqwG__KhSxKb3qwGsdBBeDbBffJs_I'; @@ -46,8 +46,8 @@ const user = { }; describe('components/accounts/actions', () => { - let dispatch; - let getState; + let dispatch: Dispatch; + let getState: () => RootState; beforeEach(() => { dispatch = sinon @@ -55,7 +55,7 @@ describe('components/accounts/actions', () => { .named('store.dispatch'); getState = sinon.stub().named('store.getState'); - getState.returns({ + (getState as any).returns({ accounts: { available: [], active: null, @@ -72,8 +72,8 @@ describe('components/accounts/actions', () => { sinon.stub(browserHistory, 'push').named('browserHistory.push'); sinon.stub(authentication, 'logout').named('authentication.logout'); - authentication.logout.returns(Promise.resolve()); - authentication.validateToken.returns( + (authentication.logout as any).returns(Promise.resolve()); + (authentication.validateToken as any).returns( Promise.resolve({ token: account.token, refreshToken: account.refreshToken, @@ -83,14 +83,14 @@ describe('components/accounts/actions', () => { }); afterEach(() => { - authentication.validateToken.restore(); - authentication.logout.restore(); - browserHistory.push.restore(); + (authentication.validateToken as any).restore(); + (authentication.logout as any).restore(); + (browserHistory.push as any).restore(); }); describe('#authenticate()', () => { it('should request user state using token', () => - authenticate(account)(dispatch, getState).then(() => + authenticate(account)(dispatch, getState, undefined).then(() => expect(authentication.validateToken, 'to have a call satisfying', [ account.id, account.token, @@ -99,7 +99,11 @@ describe('components/accounts/actions', () => { )); it('should request user by extracting id from token', () => - authenticate({ token })(dispatch, getState).then(() => + authenticate({ token } as Account)( + dispatch, + getState, + undefined, + ).then(() => expect(authentication.validateToken, 'to have a call satisfying', [ 1, token, @@ -108,7 +112,11 @@ describe('components/accounts/actions', () => { )); it('should request user by extracting id from legacy token', () => - authenticate({ token: legacyToken })(dispatch, getState).then(() => + authenticate({ token: legacyToken } as Account)( + dispatch, + getState, + undefined, + ).then(() => expect(authentication.validateToken, 'to have a call satisfying', [ 1, legacyToken, @@ -117,39 +125,39 @@ describe('components/accounts/actions', () => { )); it(`dispatches ${ADD} action`, () => - authenticate(account)(dispatch, getState).then(() => + authenticate(account)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [add(account)]), )); it(`dispatches ${ACTIVATE} action`, () => - authenticate(account)(dispatch, getState).then(() => + authenticate(account)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [activate(account)]), )); it(`dispatches ${SET_LOCALE} action`, () => - authenticate(account)(dispatch, getState).then(() => + authenticate(account)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [ { type: SET_LOCALE, payload: { locale: 'be' } }, ]), )); it('should update user state', () => - authenticate(account)(dispatch, getState).then(() => + authenticate(account)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [ updateUser({ ...user, isGuest: false }), ]), )); it('resolves with account', () => - authenticate(account)(dispatch, getState).then(resp => + authenticate(account)(dispatch, getState, undefined).then(resp => expect(resp, 'to equal', account), )); it('rejects when bad auth data', () => { - authentication.validateToken.returns(Promise.reject({})); + (authentication.validateToken as any).returns(Promise.reject({})); return expect( - authenticate(account)(dispatch, getState), + authenticate(account)(dispatch, getState, undefined), 'to be rejected', ).then(() => { expect(dispatch, 'to have a call satisfying', [ @@ -161,12 +169,12 @@ describe('components/accounts/actions', () => { }); it('rejects when 5xx without logouting', () => { - const resp = new InternalServerError(null, { status: 500 }); + const resp = new InternalServerError('500', { status: 500 }); - authentication.validateToken.rejects(resp); + (authentication.validateToken as any).rejects(resp); return expect( - authenticate(account)(dispatch, getState), + authenticate(account)(dispatch, getState, undefined), 'to be rejected with', resp, ).then(() => @@ -178,14 +186,14 @@ describe('components/accounts/actions', () => { it('marks user as stranger, if there is no refreshToken', () => { const expectedKey = `stranger${account.id}`; - authentication.validateToken.resolves({ + (authentication.validateToken as any).resolves({ token: account.token, user, }); sessionStorage.removeItem(expectedKey); - return authenticate(account)(dispatch, getState).then(() => { + return authenticate(account)(dispatch, getState, undefined).then(() => { expect(sessionStorage.getItem(expectedKey), 'not to be null'); sessionStorage.removeItem(expectedKey); }); @@ -193,7 +201,7 @@ describe('components/accounts/actions', () => { describe('when user authenticated during oauth', () => { beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { available: [], active: null, @@ -209,7 +217,7 @@ describe('components/accounts/actions', () => { }); it('should dispatch setAccountSwitcher', () => - authenticate(account)(dispatch, getState).then(() => + authenticate(account)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [ setAccountSwitcher(false), ]), @@ -218,7 +226,7 @@ describe('components/accounts/actions', () => { describe('when one account available', () => { beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { active: account.id, available: [account], @@ -231,10 +239,12 @@ describe('components/accounts/actions', () => { }); it('should activate account before auth api call', () => { - authentication.validateToken.returns(Promise.reject({ error: 'foo' })); + (authentication.validateToken as any).returns( + Promise.reject({ error: 'foo' }), + ); return expect( - authenticate(account)(dispatch, getState), + authenticate(account)(dispatch, getState, undefined), 'to be rejected with', { error: 'foo' }, ).then(() => @@ -247,7 +257,7 @@ describe('components/accounts/actions', () => { describe('#revoke()', () => { describe('when one account available', () => { beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { active: account.id, available: [account], @@ -260,19 +270,19 @@ describe('components/accounts/actions', () => { }); it('should dispatch reset action', () => - revoke(account)(dispatch, getState).then(() => + revoke(account)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [reset()]), )); it('should call logout api method in background', () => - revoke(account)(dispatch, getState).then(() => + revoke(account)(dispatch, getState, undefined).then(() => expect(authentication.logout, 'to have a call satisfying', [ account.token, ]), )); it('should update user state', () => - revoke(account)(dispatch, getState).then( + revoke(account)(dispatch, getState, undefined).then( () => expect(dispatch, 'to have a call satisfying', [ setUser({ isGuest: true }), @@ -289,7 +299,7 @@ describe('components/accounts/actions', () => { const account2 = { ...account, id: 2 }; beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { active: account2.id, available: [account, account2], @@ -299,17 +309,17 @@ describe('components/accounts/actions', () => { }); it('should switch to the next account', () => - revoke(account2)(dispatch, getState).then(() => + revoke(account2)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [activate(account)]), )); it('should remove current account', () => - revoke(account2)(dispatch, getState).then(() => + revoke(account2)(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [remove(account2)]), )); it('should call logout api method in background', () => - revoke(account2)(dispatch, getState).then(() => + revoke(account2)(dispatch, getState, undefined).then(() => expect(authentication.logout, 'to have a call satisfying', [ account2.token, ]), @@ -321,7 +331,7 @@ describe('components/accounts/actions', () => { const account2 = { ...account, id: 2 }; beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { active: account2.id, available: [account, account2], @@ -334,7 +344,7 @@ describe('components/accounts/actions', () => { }); it('should call logout api method for each account', () => { - logoutAll()(dispatch, getState); + logoutAll()(dispatch, getState, undefined); expect(authentication.logout, 'to have calls satisfying', [ [account.token], @@ -343,18 +353,18 @@ describe('components/accounts/actions', () => { }); it('should dispatch reset', () => { - logoutAll()(dispatch, getState); + logoutAll()(dispatch, getState, undefined); expect(dispatch, 'to have a call satisfying', [reset()]); }); it('should redirect to /login', () => - logoutAll()(dispatch, getState).then(() => { + logoutAll()(dispatch, getState, undefined).then(() => { expect(browserHistory.push, 'to have a call satisfying', ['/login']); })); it('should change user to guest', () => - logoutAll()(dispatch, getState).then(() => { + logoutAll()(dispatch, getState, undefined).then(() => { expect(dispatch, 'to have a call satisfying', [ setUser({ lang: user.lang, @@ -368,7 +378,7 @@ describe('components/accounts/actions', () => { const foreignAccount = { ...account, id: 2, - refreshToken: undefined, + refreshToken: null, }; const foreignAccount2 = { @@ -377,7 +387,7 @@ describe('components/accounts/actions', () => { }; beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { active: foreignAccount.id, available: [account, foreignAccount, foreignAccount2], @@ -387,14 +397,14 @@ describe('components/accounts/actions', () => { }); it('should remove stranger accounts', () => { - logoutStrangers()(dispatch, getState); + logoutStrangers()(dispatch, getState, undefined); expect(dispatch, 'to have a call satisfying', [remove(foreignAccount)]); expect(dispatch, 'to have a call satisfying', [remove(foreignAccount2)]); }); it('should logout stranger accounts', () => { - logoutStrangers()(dispatch, getState); + logoutStrangers()(dispatch, getState, undefined); expect(authentication.logout, 'to have calls satisfying', [ [foreignAccount.token], @@ -403,12 +413,12 @@ describe('components/accounts/actions', () => { }); it('should activate another account if available', () => - logoutStrangers()(dispatch, getState).then(() => + logoutStrangers()(dispatch, getState, undefined).then(() => expect(dispatch, 'to have a call satisfying', [activate(account)]), )); it('should not activate another account if active account is already not a stranger', () => { - getState.returns({ + (getState as any).returns({ accounts: { active: account.id, available: [account, foreignAccount], @@ -416,13 +426,13 @@ describe('components/accounts/actions', () => { user, }); - return logoutStrangers()(dispatch, getState).then(() => + return logoutStrangers()(dispatch, getState, undefined).then(() => expect(dispatch, 'not to have calls satisfying', [activate(account)]), ); }); it('should not dispatch if no strangers', () => { - getState.returns({ + (getState as any).returns({ accounts: { active: account.id, available: [account], @@ -430,14 +440,14 @@ describe('components/accounts/actions', () => { user, }); - return logoutStrangers()(dispatch, getState).then(() => + return logoutStrangers()(dispatch, getState, undefined).then(() => expect(dispatch, 'was not called'), ); }); describe('when all accounts are strangers', () => { beforeEach(() => { - getState.returns({ + (getState as any).returns({ accounts: { active: foreignAccount.id, available: [foreignAccount, foreignAccount2], @@ -448,7 +458,7 @@ describe('components/accounts/actions', () => { user, }); - logoutStrangers()(dispatch, getState); + logoutStrangers()(dispatch, getState, undefined); }); it('logouts all accounts', () => { @@ -471,7 +481,7 @@ describe('components/accounts/actions', () => { beforeEach(() => { sessionStorage.setItem(key, 1); - logoutStrangers()(dispatch, getState); + logoutStrangers()(dispatch, getState, undefined); }); afterEach(() => { diff --git a/src/components/accounts/actions.js b/src/components/accounts/actions.ts similarity index 79% rename from src/components/accounts/actions.js rename to src/components/accounts/actions.ts index eb383f9..35ca252 100644 --- a/src/components/accounts/actions.js +++ b/src/components/accounts/actions.ts @@ -1,5 +1,3 @@ -// @flow -import type { Account, State as AccountsState } from './reducer'; import { getJwtPayloads } from 'functions'; import { sessionStorage } from 'services/localStorage'; import { @@ -13,7 +11,9 @@ import { setLocale } from 'components/i18n/actions'; import { setAccountSwitcher } from 'components/auth/actions'; import { getActiveAccount } from 'components/accounts/reducer'; import logger from 'services/logger'; +import { ThunkAction } from 'reducers'; +import { Account } from './reducer'; import { add, remove, @@ -22,17 +22,6 @@ import { updateToken, } from './actions/pure-actions'; -type Dispatch = (action: Object) => Promise<*>; - -type State = { - accounts: AccountsState, - auth: { - oauth?: { - clientId?: string, - }, - }, -}; - export { updateToken, activate, remove }; /** @@ -46,20 +35,17 @@ export function authenticate( account: | Account | { - token: string, - refreshToken: ?string, + token: string; + refreshToken: string | null; }, -) { +): ThunkAction> { const { token, refreshToken } = account; - const email = account.email || null; + const email = 'email' in account ? account.email : null; - return async ( - dispatch: Dispatch, - getState: () => State, - ): Promise => { + return async (dispatch, getState) => { let accountId: number; - if (typeof account.id === 'number') { + if ('id' in account && typeof account.id === 'number') { accountId = account.id; } else { accountId = findAccountIdFromToken(token); @@ -80,18 +66,17 @@ export function authenticate( token: newToken, refreshToken: newRefreshToken, user, - // $FlowFixMe have no idea why it's causes error about missing properties } = await validateToken(accountId, token, refreshToken); const { auth } = getState(); - const account: Account = { + const newAccount: Account = { id: user.id, username: user.username, email: user.email, token: newToken, refreshToken: newRefreshToken, }; - dispatch(add(account)); - dispatch(activate(account)); + dispatch(add(newAccount)); + dispatch(activate(newAccount)); dispatch( updateUser({ isGuest: false, @@ -104,7 +89,7 @@ export function authenticate( if (!newRefreshToken) { // mark user as stranger (user does not want us to remember his account) - sessionStorage.setItem(`stranger${account.id}`, 1); + sessionStorage.setItem(`stranger${newAccount.id}`, 1); } if (auth && auth.oauth && auth.oauth.clientId) { @@ -117,7 +102,7 @@ export function authenticate( await dispatch(setLocale(user.lang)); - return account; + return newAccount; } catch (resp) { // all the logic to get the valid token was failed, // looks like we have some problems with token @@ -156,13 +141,13 @@ function findAccountIdFromToken(token: string): number { * * @returns {Function} */ -export function ensureToken() { - return (dispatch: Dispatch, getState: () => State): Promise => { +export function ensureToken(): ThunkAction> { + return (dispatch, getState) => { const { token } = getActiveAccount(getState()) || {}; try { const SAFETY_FACTOR = 300; // ask new token earlier to overcome time desynchronization problem - const { exp } = getJwtPayloads(token); + const { exp } = getJwtPayloads(token as any); if (exp - SAFETY_FACTOR < Date.now() / 1000) { return dispatch(requestNewToken()); @@ -192,12 +177,12 @@ export function ensureToken() { * @returns {Function} */ export function recoverFromTokenError( - error: ?{ - status: number, - message: string, - }, -) { - return (dispatch: Dispatch, getState: () => State): Promise => { + error: { + status: number; + message: string; + } | void, +): ThunkAction> { + return (dispatch, getState) => { if (error && error.status === 401) { const activeAccount = getActiveAccount(getState()); @@ -234,8 +219,8 @@ export function recoverFromTokenError( * * @returns {Function} */ -export function requestNewToken() { - return (dispatch: Dispatch, getState: () => State): Promise => { +export function requestNewToken(): ThunkAction> { + return (dispatch, getState) => { const { refreshToken } = getActiveAccount(getState()) || {}; if (!refreshToken) { @@ -266,14 +251,13 @@ export function requestNewToken() { * * @returns {Function} */ -export function revoke(account: Account) { - return (dispatch: Dispatch, getState: () => State): Promise => { - const accountToReplace: ?Account = getState().accounts.available.find( - ({ id }) => id !== account.id, - ); +export function revoke(account: Account): ThunkAction> { + return async (dispatch, getState) => { + const accountToReplace: Account | null = + getState().accounts.available.find(({ id }) => id !== account.id) || null; if (accountToReplace) { - return dispatch(authenticate(accountToReplace)) + await dispatch(authenticate(accountToReplace)) .finally(() => { // we need to logout user, even in case, when we can // not authenticate him with new account @@ -285,14 +269,16 @@ export function revoke(account: Account) { .catch(() => { // we don't care }); + + return; } return dispatch(logoutAll()); }; } -export function relogin(email?: string) { - return (dispatch: Dispatch, getState: () => State) => { +export function relogin(email?: string): ThunkAction> { + return async (dispatch, getState) => { const activeAccount = getActiveAccount(getState()); if (!email && activeAccount) { @@ -303,8 +289,8 @@ export function relogin(email?: string) { }; } -export function logoutAll() { - return (dispatch: Dispatch, getState: () => State): Promise => { +export function logoutAll(): ThunkAction> { + return (dispatch, getState) => { dispatch(setGuest()); const { @@ -332,8 +318,8 @@ export function logoutAll() { * * @returns {Function} */ -export function logoutStrangers() { - return (dispatch: Dispatch, getState: () => State): Promise => { +export function logoutStrangers(): ThunkAction> { + return async (dispatch, getState) => { const { accounts: { available }, } = getState(); @@ -343,9 +329,7 @@ export function logoutStrangers() { !refreshToken && !sessionStorage.getItem(`stranger${id}`); if (available.some(isStranger)) { - const accountToReplace = available.filter( - account => !isStranger(account), - )[0]; + const accountToReplace = available.find(account => !isStranger(account)); if (accountToReplace) { available.filter(isStranger).forEach(account => { @@ -354,10 +338,14 @@ export function logoutStrangers() { }); if (activeAccount && isStranger(activeAccount)) { - return dispatch(authenticate(accountToReplace)); + await dispatch(authenticate(accountToReplace)); + + return; } } else { - return dispatch(logoutAll()); + await dispatch(logoutAll()); + + return; } } diff --git a/src/components/accounts/actions/pure-actions.js b/src/components/accounts/actions/pure-actions.ts similarity index 93% rename from src/components/accounts/actions/pure-actions.js rename to src/components/accounts/actions/pure-actions.ts index 9f41daf..3859f5c 100644 --- a/src/components/accounts/actions/pure-actions.js +++ b/src/components/accounts/actions/pure-actions.ts @@ -1,5 +1,4 @@ -// @flow -import type { +import { Account, AddAction, RemoveAction, @@ -10,7 +9,7 @@ import type { export const ADD = 'accounts:add'; /** - * @api private + * @private * * @param {Account} account * @@ -25,7 +24,7 @@ export function add(account: Account): AddAction { export const REMOVE = 'accounts:remove'; /** - * @api private + * @private * * @param {Account} account * @@ -40,7 +39,7 @@ export function remove(account: Account): RemoveAction { export const ACTIVATE = 'accounts:activate'; /** - * @api private + * @private * * @param {Account} account * @@ -55,7 +54,7 @@ export function activate(account: Account): ActivateAction { export const RESET = 'accounts:reset'; /** - * @api private + * @private * * @returns {object} - action definition */ diff --git a/src/components/accounts/index.js b/src/components/accounts/index.js deleted file mode 100644 index 79ee918..0000000 --- a/src/components/accounts/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -export type { State as AccountsState, Account } from './reducer'; -export { default as AccountSwitcher } from './AccountSwitcher'; diff --git a/src/components/accounts/index.ts b/src/components/accounts/index.ts new file mode 100644 index 0000000..6ed2b18 --- /dev/null +++ b/src/components/accounts/index.ts @@ -0,0 +1,2 @@ +export { State as AccountsState, Account } from './reducer'; +export { default as AccountSwitcher } from './AccountSwitcher'; diff --git a/src/components/accounts/reducer.test.js b/src/components/accounts/reducer.test.ts similarity index 82% rename from src/components/accounts/reducer.test.js rename to src/components/accounts/reducer.test.ts index bae53d8..d70151b 100644 --- a/src/components/accounts/reducer.test.js +++ b/src/components/accounts/reducer.test.ts @@ -1,7 +1,6 @@ import expect from 'test/unexpected'; -import accounts from 'components/accounts/reducer'; -import { updateToken } from 'components/accounts/actions'; +import { updateToken } from './actions'; import { add, remove, @@ -12,30 +11,33 @@ import { ACTIVATE, UPDATE_TOKEN, RESET, -} from 'components/accounts/actions/pure-actions'; +} from './actions/pure-actions'; +import accounts, { Account } from './reducer'; -const account = { +const account: Account = { id: 1, username: 'username', email: 'email@test.com', token: 'foo', -}; +} as Account; describe('Accounts reducer', () => { let initial; beforeEach(() => { - initial = accounts(undefined, {}); + initial = accounts(undefined, {} as any); }); it('should be empty', () => - expect(accounts(undefined, {}), 'to equal', { + expect(accounts(undefined, {} as any), 'to equal', { active: null, available: [], })); it('should return last state if unsupported action', () => - expect(accounts({ state: 'foo' }, {}), 'to equal', { state: 'foo' })); + expect(accounts({ state: 'foo' } as any, {} as any), 'to equal', { + state: 'foo', + })); describe(ACTIVATE, () => { it('sets active account', () => { @@ -92,7 +94,12 @@ describe('Accounts reducer', () => { it('throws, when account is invalid', () => { expect( - () => accounts(initial, add()), + () => + accounts( + initial, + // @ts-ignore + add(), + ), 'to throw', 'Invalid or empty payload passed for accounts.add', ); @@ -109,7 +116,12 @@ describe('Accounts reducer', () => { it('throws, when account is invalid', () => { expect( - () => accounts(initial, remove()), + () => + accounts( + initial, + // @ts-ignore + remove(), + ), 'to throw', 'Invalid or empty payload passed for accounts.remove', ); diff --git a/src/components/accounts/reducer.js b/src/components/accounts/reducer.ts similarity index 80% rename from src/components/accounts/reducer.js rename to src/components/accounts/reducer.ts index 1b15353..c18bd16 100644 --- a/src/components/accounts/reducer.js +++ b/src/components/accounts/reducer.ts @@ -1,23 +1,22 @@ -// @flow export type Account = { - id: number, - username: string, - email: string, - token: string, - refreshToken: ?string, + id: number; + username: string; + email: string; + token: string; + refreshToken: string | null; }; export type State = { - active: ?number, - available: Array, + active: number | null; + available: Account[]; }; -export type AddAction = { type: 'accounts:add', payload: Account }; -export type RemoveAction = { type: 'accounts:remove', payload: Account }; -export type ActivateAction = { type: 'accounts:activate', payload: Account }; +export type AddAction = { type: 'accounts:add'; payload: Account }; +export type RemoveAction = { type: 'accounts:remove'; payload: Account }; +export type ActivateAction = { type: 'accounts:activate'; payload: Account }; export type UpdateTokenAction = { - type: 'accounts:updateToken', - payload: string, + type: 'accounts:updateToken'; + payload: string; }; export type ResetAction = { type: 'accounts:reset' }; @@ -28,14 +27,16 @@ type Action = | UpdateTokenAction | ResetAction; -export function getActiveAccount(state: { accounts: State }): ?Account { +export function getActiveAccount(state: { accounts: State }): Account | null { const accountId = state.accounts.active; - return state.accounts.available.find(account => account.id === accountId); + return ( + state.accounts.available.find(account => account.id === accountId) || null + ); } export function getAvailableAccounts(state: { - accounts: State, + accounts: State; }): Array { return state.accounts.available; } @@ -129,10 +130,7 @@ export default function accounts( }), }; } - - default: - (action: empty); - - return state; } + + return state; } diff --git a/src/components/auth/AuthTitle.js b/src/components/auth/AuthTitle.tsx similarity index 59% rename from src/components/auth/AuthTitle.js rename to src/components/auth/AuthTitle.tsx index 0641f75..2b3a9a4 100644 --- a/src/components/auth/AuthTitle.js +++ b/src/components/auth/AuthTitle.tsx @@ -1,10 +1,8 @@ -// @flow import React from 'react'; - import Helmet from 'react-helmet'; -import { FormattedMessage as Message } from 'react-intl'; +import { FormattedMessage as Message, MessageDescriptor } from 'react-intl'; -export default function AuthTitle({ title }: { title: { id: string } }) { +export default function AuthTitle({ title }: { title: MessageDescriptor }) { return ( {msg => ( diff --git a/src/components/auth/BaseAuthBody.js b/src/components/auth/BaseAuthBody.tsx similarity index 75% rename from src/components/auth/BaseAuthBody.js rename to src/components/auth/BaseAuthBody.tsx index b1430ef..1fd073f 100644 --- a/src/components/auth/BaseAuthBody.js +++ b/src/components/auth/BaseAuthBody.tsx @@ -2,13 +2,16 @@ * Helps with form fields binding, form serialization and errors rendering */ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; - +import React from 'react'; import AuthError from 'components/auth/authError/AuthError'; import { userShape } from 'components/user/User'; import { FormModel } from 'components/ui/form'; +import { RouteComponentProps } from 'react-router-dom'; -export default class BaseAuthBody extends Component { +export default class BaseAuthBody extends React.Component< + // TODO: this may be converted to generic type RouteComponentProps + RouteComponentProps<{ [key: string]: any }> +> { static contextTypes = { clearErrors: PropTypes.func.isRequired, resolve: PropTypes.func.isRequired, @@ -26,6 +29,8 @@ export default class BaseAuthBody extends Component { user: userShape, }; + autoFocusField: string | null = ''; + componentWillReceiveProps(nextProps, nextContext) { if (nextContext.auth.error !== this.context.auth.error) { this.form.setErrors(nextContext.auth.error || {}); @@ -33,12 +38,9 @@ export default class BaseAuthBody extends Component { } renderErrors() { - return this.form.hasErrors() ? ( - - ) : null; + const error = this.form.getFirstError(); + + return error && ; } onFormSubmit() { diff --git a/src/components/auth/PanelTransition.js b/src/components/auth/PanelTransition.tsx similarity index 79% rename from src/components/auth/PanelTransition.js rename to src/components/auth/PanelTransition.tsx index dae4b90..e676baf 100644 --- a/src/components/auth/PanelTransition.js +++ b/src/components/auth/PanelTransition.tsx @@ -1,13 +1,10 @@ -// @flow -import type { User } from 'components/user'; -import type { AccountsState } from 'components/accounts'; -import type { Element } from 'react'; -import React, { Component } from 'react'; +import React from 'react'; +import { AccountsState } from 'components/accounts'; +import { AuthState } from 'components/auth'; +import { User } from 'components/user'; import PropTypes from 'prop-types'; - import { connect } from 'react-redux'; import { TransitionMotion, spring } from 'react-motion'; - import { Panel, PanelBody, @@ -17,13 +14,14 @@ import { import { getLogin } from 'components/auth/reducer'; import { Form } from 'components/ui/form'; import MeasureHeight from 'components/MeasureHeight'; -import { helpLinks as helpLinksStyles } from 'components/auth/helpLinks.scss'; +import defaultHelpLinksStyles from 'components/auth/helpLinks.scss'; import panelStyles from 'components/ui/panel.scss'; import icons from 'components/ui/icons.scss'; import authFlow from 'services/authFlow'; import { userShape } from 'components/user/User'; import * as actions from './actions'; +import { RootState } from 'reducers'; const opacitySpringConfig = { stiffness: 300, damping: 20 }; const transformSpringConfig = { stiffness: 500, damping: 50, precision: 0.5 }; @@ -33,6 +31,8 @@ const changeContextSpringConfig = { precision: 0.5, }; +const { helpLinksStyles } = defaultHelpLinksStyles; + type PanelId = string; /** @@ -78,98 +78,56 @@ if (process.env.NODE_ENV !== 'production') { type ValidationError = | string | { - type: string, - payload: { [key: string]: any }, + type: string; + payload: { [key: string]: any }; }; -type AnimationProps = {| - opacitySpring: number, - transformSpring: number, -|}; +type AnimationProps = { + opacitySpring: number; + transformSpring: number; +}; type AnimationContext = { - key: PanelId, - style: AnimationProps, + key: PanelId; + style: AnimationProps; data: { - Title: Element, - Body: Element, - Footer: Element, - Links: Element, - hasBackButton: boolean, - }, + Title: React.ReactElement; + Body: React.ReactElement; + Footer: React.ReactElement; + Links: React.ReactElement; + hasBackButton: boolean | ((props: Props) => boolean); + }; }; -type OwnProps = {| - Title: Element, - Body: Element, - Footer: Element, - Links: Element, - children?: Element, -|}; +type OwnProps = { + Title: React.ReactElement; + Body: React.ReactElement; + Footer: React.ReactElement; + Links: React.ReactElement; + children?: React.ReactElement; +}; -type Props = { - ...OwnProps, +interface Props extends OwnProps { // context props - auth: { - error: - | string - | { - type: string, - payload: { [key: string]: any }, - }, - isLoading: boolean, - login: string, - }, - user: User, - accounts: AccountsState, - setErrors: ({ [key: string]: ValidationError }) => void, - clearErrors: () => void, - resolve: () => void, - reject: () => void, -}; + auth: AuthState; + user: User; + accounts: AccountsState; + setErrors: (errors: { [key: string]: ValidationError }) => void; + clearErrors: () => void; + resolve: () => void; + reject: () => void; +} type State = { - contextHeight: number, - panelId: PanelId | void, - prevPanelId: PanelId | void, - isHeightDirty: boolean, - forceHeight: 1 | 0, - direction: 'X' | 'Y', + contextHeight: number; + panelId: PanelId | void; + prevPanelId: PanelId | void; + isHeightDirty: boolean; + forceHeight: 1 | 0; + direction: 'X' | 'Y'; }; -class PanelTransition extends Component { - static displayName = 'PanelTransition'; - - static propTypes = { - // context props - auth: PropTypes.shape({ - error: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - type: PropTypes.string, - payload: PropTypes.object, - }), - ]), - isLoading: PropTypes.bool, - login: PropTypes.string, - }).isRequired, - user: userShape.isRequired, - accounts: PropTypes.shape({ - available: PropTypes.array, - }), - setErrors: PropTypes.func.isRequired, - clearErrors: PropTypes.func.isRequired, - resolve: PropTypes.func.isRequired, - reject: PropTypes.func.isRequired, - - // local props - Title: PropTypes.element, - Body: PropTypes.element, - Footer: PropTypes.element, - Links: PropTypes.element, - children: PropTypes.element, - }; - +class PanelTransition extends React.Component { static childContextTypes = { auth: PropTypes.shape({ error: PropTypes.oneOfType([ @@ -191,23 +149,23 @@ class PanelTransition extends Component { reject: PropTypes.func, }; - state = { + state: State = { contextHeight: 0, - panelId: this.props.Body && (this.props.Body: any).type.panelId, + panelId: this.props.Body && (this.props.Body.type as any).panelId, isHeightDirty: false, - forceHeight: 0, - direction: 'X', + forceHeight: 0 as const, + direction: 'X' as const, prevPanelId: undefined, }; isHeightMeasured: boolean = false; wasAutoFocused: boolean = false; body: null | { - autoFocus: () => void, - onFormSubmit: () => void, + autoFocus: () => void; + onFormSubmit: () => void; } = null; - timerIds = []; // this is a list of a probably running timeouts to clean on unmount + timerIds: NodeJS.Timeout[] = []; // this is a list of a probably running timeouts to clean on unmount getChildContext() { return { @@ -230,9 +188,9 @@ class PanelTransition extends Component { componentWillReceiveProps(nextProps: Props) { const nextPanel: PanelId = - nextProps.Body && (nextProps.Body: any).type.panelId; + nextProps.Body && (nextProps.Body.type as any).panelId; const prevPanel: PanelId = - this.props.Body && (this.props.Body: any).type.panelId; + this.props.Body && (this.props.Body.type as any).panelId; if (nextPanel !== prevPanel) { const direction = this.getDirection(nextPanel, prevPanel); @@ -276,9 +234,9 @@ class PanelTransition extends Component { panelId, hasGoBack, }: { - panelId: PanelId, - hasGoBack: boolean, - } = (Body: any).type; + panelId: PanelId; + hasGoBack: boolean; + } = Body.type as any; const formHeight = this.state[`formHeight${panelId}`] || 0; @@ -315,7 +273,7 @@ class PanelTransition extends Component { > {items => { const panels = items.filter(({ key }) => key !== 'common'); - const common = items.filter(({ key }) => key === 'common')[0]; + const [common] = items.filter(({ key }) => key === 'common'); const contentHeight = { overflow: 'hidden', @@ -327,7 +285,7 @@ class PanelTransition extends Component { this.tryToAutoFocus(panels.length); const bodyHeight = { - position: 'relative', + position: 'relative' as const, height: `${common.style.heightSpring}px`, }; @@ -394,10 +352,10 @@ class PanelTransition extends Component { getTransitionStyles( { key }: AnimationContext, options: { isLeave?: boolean } = {}, - ): {| - transformSpring: number, - opacitySpring: number, - |} { + ): { + transformSpring: number; + opacitySpring: number; + } { const { isLeave = false } = options; const { panelId, prevPanelId } = this.state; @@ -411,6 +369,8 @@ class PanelTransition extends Component { } let sign = + prevPanelId && + panelId && currentContext.indexOf(panelId) > currentContext.indexOf(prevPanelId) ? fromRight : fromLeft; @@ -430,7 +390,7 @@ class PanelTransition extends Component { } getDirection(next: PanelId, prev: PanelId): 'X' | 'Y' { - const context = contexts.find(context => context.includes(prev)); + const context = contexts.find(item => item.includes(prev)); if (!context) { throw new Error(`Can not find context for transition ${prev} -> ${next}`); @@ -442,6 +402,7 @@ class PanelTransition extends Component { onUpdateHeight = (height: number, key: PanelId) => { const heightKey = `formHeight${key}`; + // @ts-ignore this.setState({ [heightKey]: height, }); @@ -453,7 +414,7 @@ class PanelTransition extends Component { }); }; - onGoBack = (event: SyntheticEvent) => { + onGoBack = (event: React.MouseEvent) => { event.preventDefault(); authFlow.goBack(); @@ -482,7 +443,6 @@ class PanelTransition extends Component { shouldMeasureHeight() { const errorString = Object.values(this.props.auth.error || {}).reduce( - // $FlowFixMe (acc, item: ValidationError) => { if (typeof item === 'string') { return acc + item; @@ -519,7 +479,7 @@ class PanelTransition extends Component { const scrollStyle = this.translate(transformSpring, 'Y'); const sideScrollStyle = { - position: 'relative', + position: 'relative' as const, zIndex: 2, ...this.translate(-Math.abs(transformSpring)), }; @@ -549,7 +509,10 @@ class PanelTransition extends Component { const { transformSpring } = style; const { direction } = this.state; - let transform = this.translate(transformSpring, direction); + let transform: { [key: string]: string } = this.translate( + transformSpring, + direction, + ); let verticalOrigin = 'top'; if (direction === 'Y') { @@ -613,15 +576,15 @@ class PanelTransition extends Component { */ getDefaultTransitionStyles( key: string, - { opacitySpring }: $ReadOnly, - ): {| - position: string, - top: number, - left: number, - width: string, - opacity: number, - pointerEvents: string, - |} { + { opacitySpring }: Readonly, + ): { + position: 'absolute'; + top: number; + left: number; + width: string; + opacity: number; + pointerEvents: 'none' | 'auto'; + } { return { position: 'absolute', top: 0, @@ -632,14 +595,6 @@ class PanelTransition extends Component { }; } - /** - * @param {number} value - * @param {string} direction='X' - X|Y - * @param direction - * @param unit - * @param {string} unit='%' - %|px etc - * @returns {object} - */ translate(value: number, direction: 'X' | 'Y' = 'X', unit: '%' | 'px' = '%') { return { WebkitTransform: `translate${direction}(${value}${unit})`, @@ -648,8 +603,8 @@ class PanelTransition extends Component { } } -export default connect( - state => { +export default connect( + (state: RootState) => { const login = getLogin(state); let user = { ...state.user, diff --git a/src/components/auth/RejectionLink.js b/src/components/auth/RejectionLink.tsx similarity index 56% rename from src/components/auth/RejectionLink.js rename to src/components/auth/RejectionLink.tsx index e3912ff..0385f80 100644 --- a/src/components/auth/RejectionLink.js +++ b/src/components/auth/RejectionLink.tsx @@ -1,11 +1,23 @@ import PropTypes from 'prop-types'; import React from 'react'; - -import { FormattedMessage as Message } from 'react-intl'; - +import { FormattedMessage as Message, MessageDescriptor } from 'react-intl'; +import { User } from 'components/user'; import { userShape } from 'components/user/User'; -export default function RejectionLink(props, context) { +interface Props { + isAvailable?: (context: Context) => boolean; + payload?: { [key: string]: any }; + label: MessageDescriptor; +} + +export type RejectionLinkProps = Props; + +interface Context { + reject: (payload: { [key: string]: any } | undefined) => void; + user: User; +} + +function RejectionLink(props: Props, context: Context) { if (props.isAvailable && !props.isAvailable(context)) { // TODO: if want to properly support multiple links, we should control // the dividers ' | ' rendered from factory too @@ -26,16 +38,9 @@ export default function RejectionLink(props, context) { ); } -RejectionLink.displayName = 'RejectionLink'; -RejectionLink.propTypes = { - isAvailable: PropTypes.func, // a function from context to allow link visibility control - // eslint-disable-next-line react/forbid-prop-types - payload: PropTypes.object, // Custom payload for active state - label: PropTypes.shape({ - id: PropTypes.string, - }).isRequired, -}; RejectionLink.contextTypes = { reject: PropTypes.func.isRequired, user: userShape, }; + +export default RejectionLink; diff --git a/src/components/auth/acceptRules/AcceptRules.js b/src/components/auth/acceptRules/AcceptRules.ts similarity index 86% rename from src/components/auth/acceptRules/AcceptRules.js rename to src/components/auth/acceptRules/AcceptRules.ts index de70848..0325b82 100644 --- a/src/components/auth/acceptRules/AcceptRules.js +++ b/src/components/auth/acceptRules/AcceptRules.ts @@ -1,5 +1,4 @@ -import factory from 'components/auth/factory'; - +import factory from '../factory'; import Body from './AcceptRulesBody'; import messages from './AcceptRules.intl.json'; diff --git a/src/components/auth/actions.test.js b/src/components/auth/actions.test.ts similarity index 85% rename from src/components/auth/actions.test.js rename to src/components/auth/actions.test.ts index f0854bc..746a2fa 100644 --- a/src/components/auth/actions.test.js +++ b/src/components/auth/actions.test.ts @@ -53,8 +53,8 @@ describe('components/auth/actions', () => { }); afterEach(() => { - request.get.restore(); - request.post.restore(); + (request.get as any).restore(); + (request.post as any).restore(); }); describe('#oAuthValidate()', () => { @@ -69,7 +69,7 @@ describe('components/auth/actions', () => { }, }; - request.get.returns(Promise.resolve(resp)); + (request.get as any).returns(Promise.resolve(resp)); }); it('should send get request to an api', () => @@ -106,7 +106,7 @@ describe('components/auth/actions', () => { }); it('should post to api/oauth2/complete', () => { - request.post.returns( + (request.post as any).returns( Promise.resolve({ redirectUri: '', }), @@ -126,7 +126,7 @@ describe('components/auth/actions', () => { redirectUri: 'static_page?code=123&state=', }; - request.post.returns(Promise.resolve(resp)); + (request.post as any).returns(Promise.resolve(resp)); return callThunk(oAuthComplete).then(() => { expectDispatchCalls([ @@ -141,20 +141,20 @@ describe('components/auth/actions', () => { }); }); - it('should resolve to with success false and redirectUri for access_denied', () => { + it('should resolve to with success false and redirectUri for access_denied', async () => { const resp = { statusCode: 401, error: 'access_denied', redirectUri: 'redirectUri', }; - request.post.returns(Promise.reject(resp)); + (request.post as any).returns(Promise.reject(resp)); - return callThunk(oAuthComplete).then(resp => { - expect(resp, 'to equal', { - success: false, - redirectUri: 'redirectUri', - }); + const data = await callThunk(oAuthComplete); + + expect(data, 'to equal', { + success: false, + redirectUri: 'redirectUri', }); }); @@ -164,10 +164,10 @@ describe('components/auth/actions', () => { error: 'accept_required', }; - request.post.returns(Promise.reject(resp)); + (request.post as any).returns(Promise.reject(resp)); - return callThunk(oAuthComplete).catch(resp => { - expect(resp.acceptRequired, 'to be true'); + return callThunk(oAuthComplete).catch(error => { + expect(error.acceptRequired, 'to be true'); expectDispatchCalls([[requirePermissionsAccept()]]); }); }); @@ -176,7 +176,7 @@ describe('components/auth/actions', () => { describe('#login()', () => { describe('when correct login was entered', () => { beforeEach(() => { - request.post.returns( + (request.post as any).returns( Promise.reject({ errors: { password: 'error.password_required', diff --git a/src/components/auth/actions.js b/src/components/auth/actions.ts similarity index 82% rename from src/components/auth/actions.js rename to src/components/auth/actions.ts index 00c6d38..445fea1 100644 --- a/src/components/auth/actions.js +++ b/src/components/auth/actions.ts @@ -1,6 +1,3 @@ -// @flow -import type { OauthData, Client, Scope } from 'services/api/oauth'; -import type { OAuthResponse } from 'services/api/authentication'; import { browserHistory } from 'services/history'; import logger from 'services/logger'; import localStorage from 'services/localStorage'; @@ -16,20 +13,22 @@ import { login as loginEndpoint, forgotPassword as forgotPasswordEndpoint, recoverPassword as recoverPasswordEndpoint, + OAuthResponse, } from 'services/api/authentication'; -import oauth from 'services/api/oauth'; +import oauth, { OauthData, Client, Scope } from 'services/api/oauth'; import signup from 'services/api/signup'; import dispatchBsod from 'components/ui/bsod/dispatchBsod'; import { create as createPopup } from 'components/ui/popup/actions'; import ContactForm from 'components/contact/ContactForm'; +import { ThunkAction, Dispatch } from 'reducers'; import { getCredentials } from './reducer'; type ValidationError = | string | { - type: string, - payload: { [key: string]: any }, + type: string; + payload: { [key: string]: any }; }; export { updateUser } from 'components/user/actions'; @@ -39,6 +38,7 @@ export { remove as removeAccount, activate as activateAccount, } from 'components/accounts/actions'; +import { Account } from 'components/accounts/reducer'; /** * Reoutes user to the previous page if it is possible @@ -62,7 +62,7 @@ export function goBack(options: { fallbackUrl?: string }) { }; } -export function redirect(url: string): () => Promise<*> { +export function redirect(url: string): () => Promise { loader.show(); return () => @@ -84,10 +84,10 @@ export function login({ totp, rememberMe = false, }: { - login: string, - password?: string, - totp?: string, - rememberMe?: boolean, + login: string; + password?: string; + totp?: string; + rememberMe?: boolean; }) { return wrapInLoader(dispatch => loginEndpoint({ login, password, totp, rememberMe }) @@ -129,8 +129,8 @@ export function forgotPassword({ login = '', captcha = '', }: { - login: string, - captcha: string, + login: string; + captcha: string; }) { return wrapInLoader((dispatch, getState) => forgotPasswordEndpoint(login, captcha) @@ -150,9 +150,9 @@ export function recoverPassword({ newPassword = '', newRePassword = '', }: { - key: string, - newPassword: string, - newRePassword: string, + key: string; + newPassword: string; + newRePassword: string; }) { return wrapInLoader(dispatch => recoverPasswordEndpoint(key, newPassword, newRePassword) @@ -169,12 +169,12 @@ export function register({ captcha = '', rulesAgreement = false, }: { - email: string, - username: string, - password: string, - rePassword: string, - captcha: string, - rulesAgreement: boolean, + email: string; + username: string; + password: string; + rePassword: string; + captcha: string; + rulesAgreement: boolean; }) { return wrapInLoader((dispatch, getState) => signup @@ -203,7 +203,11 @@ export function register({ ); } -export function activate({ key = '' }: { key: string }) { +export function activate({ + key = '', +}: { + key: string; +}): ThunkAction> { return wrapInLoader(dispatch => signup .activate({ key }) @@ -216,8 +220,8 @@ export function resendActivation({ email = '', captcha, }: { - email: string, - captcha: string, + email: string; + captcha: string; }) { return wrapInLoader(dispatch => signup @@ -249,7 +253,7 @@ export const SET_CREDENTIALS = 'auth:setCredentials'; * * @returns {object} */ -export function setLogin(login: ?string) { +export function setLogin(login: string | null) { return { type: SET_CREDENTIALS, payload: login @@ -260,8 +264,8 @@ export function setLogin(login: ?string) { }; } -export function relogin(login: ?string) { - return (dispatch: (Function | Object) => void, getState: () => Object) => { +export function relogin(login: string | null): ThunkAction { + return (dispatch, getState) => { const credentials = getCredentials(getState()); const returnUrl = credentials.returnUrl || location.pathname + location.search; @@ -284,11 +288,11 @@ function requestTotp({ password, rememberMe, }: { - login: string, - password: string, - rememberMe: boolean, -}) { - return (dispatch: (Function | Object) => void, getState: () => Object) => { + login: string; + password: string; + rememberMe: boolean; +}): ThunkAction { + return (dispatch, getState) => { // merging with current credentials to propogate returnUrl const credentials = getCredentials(getState()); @@ -314,7 +318,7 @@ export function setAccountSwitcher(isOn: boolean) { } export const ERROR = 'auth:error'; -export function setErrors(errors: ?{ [key: string]: ValidationError }) { +export function setErrors(errors: { [key: string]: ValidationError } | null) { return { type: ERROR, payload: errors, @@ -363,7 +367,7 @@ export function oAuthValidate(oauthData: OauthData) { ); let prompt = (oauthData.prompt || 'none') .split(',') - .map(item => item.trim); + .map(item => item.trim()); if (prompt.includes('none')) { prompt = ['none']; @@ -404,15 +408,29 @@ export function oAuthValidate(oauthData: OauthData) { * @returns {Promise} */ export function oAuthComplete(params: { accept?: boolean } = {}) { - return wrapInLoader((dispatch, getState) => - oauth.complete(getState().auth.oauth, params).then( - resp => { + return wrapInLoader( + async ( + dispatch, + getState, + ): Promise<{ + success: boolean; + redirectUri: string; + }> => { + const oauthData = getState().auth.oauth; + + if (!oauthData) { + throw new Error('Can not complete oAuth. Oauth data does not exist'); + } + + try { + const resp = await oauth.complete(oauthData, params); localStorage.removeItem('oauthData'); if (resp.redirectUri.startsWith('static_page')) { - const code = (resp.redirectUri.match(/code=(.+)&/) || [])[1]; const displayCode = resp.redirectUri === 'static_page_with_code'; - resp.redirectUri = (resp.redirectUri.match(/^(.+)\?/) || [])[1]; + + const [, code] = resp.redirectUri.match(/code=(.+)&/) || []; + [, resp.redirectUri] = resp.redirectUri.match(/^(.+)\?/) || []; dispatch( setOAuthCode({ @@ -424,34 +442,32 @@ export function oAuthComplete(params: { accept?: boolean } = {}) { } return resp; - }, - ( - resp: + } catch (error) { + const resp: | { - acceptRequired: boolean, + acceptRequired: boolean; } | { - unauthorized: boolean, - }, - ) => { - if (resp.acceptRequired) { + unauthorized: boolean; + } = error; + + if ('acceptRequired' in resp) { dispatch(requirePermissionsAccept()); return Promise.reject(resp); } return handleOauthParamsValidation(resp); - }, - ), + } + }, ); } function handleOauthParamsValidation( - resp: - | { - userMessage?: string, - } - | Object = {}, + resp: { + [key: string]: any; + userMessage?: string; + } = {}, ) { dispatchBsod(); localStorage.removeItem('oauthData'); @@ -470,8 +486,8 @@ export function setClient({ id, name, description }: Client) { }; } -export function resetOAuth() { - return (dispatch: (Function | Object) => void) => { +export function resetOAuth(): ThunkAction { + return (dispatch): void => { localStorage.removeItem('oauthData'); dispatch(setOAuthRequest({})); }; @@ -479,25 +495,20 @@ export function resetOAuth() { /** * Resets all temporary state related to auth - * - * @returns {Function} */ -export function resetAuth() { - return ( - dispatch: (Function | Object) => void, - getSate: () => Object, - ): Promise => { +export function resetAuth(): ThunkAction { + return (dispatch, getSate): Promise => { dispatch(setLogin(null)); dispatch(resetOAuth()); // ensure current account is valid const activeAccount = getActiveAccount(getSate()); if (activeAccount) { - return Promise.resolve(dispatch(authenticate(activeAccount))).catch( - () => { + return Promise.resolve(dispatch(authenticate(activeAccount))) + .then(() => {}) + .catch(() => { // its okay. user will be redirected to an appropriate place - }, - ); + }); } return Promise.resolve(); @@ -506,13 +517,13 @@ export function resetAuth() { export const SET_OAUTH = 'set_oauth'; export function setOAuthRequest(oauth: { - client_id?: string, - redirect_uri?: string, - response_type?: string, - scope?: string, - prompt?: string, - loginHint?: string, - state?: string, + client_id?: string; + redirect_uri?: string; + response_type?: string; + scope?: string; + prompt?: string; + loginHint?: string; + state?: string; }) { return { type: SET_OAUTH, @@ -530,9 +541,9 @@ export function setOAuthRequest(oauth: { export const SET_OAUTH_RESULT = 'set_oauth_result'; export function setOAuthCode(oauth: { - success: boolean, - code: string, - displayCode: boolean, + success: boolean; + code: string; + displayCode: boolean; }) { return { type: SET_OAUTH_RESULT, @@ -571,12 +582,12 @@ export function setLoadingState(isLoading: boolean) { }; } -function wrapInLoader(fn) { - return (dispatch: (Function | Object) => void, getState: Object) => { +function wrapInLoader(fn: ThunkAction>): ThunkAction> { + return (dispatch, getState) => { dispatch(setLoadingState(true)); const endLoading = () => dispatch(setLoadingState(false)); - return Reflect.apply(fn, null, [dispatch, getState]).then( + return fn(dispatch, getState, undefined).then( resp => { endLoading(); @@ -598,12 +609,12 @@ function needActivation() { }); } -function authHandler(dispatch) { - return (resp: OAuthResponse) => +function authHandler(dispatch: Dispatch) { + return (resp: OAuthResponse): Promise => dispatch( authenticate({ token: resp.access_token, - refreshToken: resp.refresh_token, + refreshToken: resp.refresh_token || null, }), ).then(resp => { dispatch(setLogin(null)); @@ -612,10 +623,7 @@ function authHandler(dispatch) { }); } -function validationErrorsHandler( - dispatch: (Function | Object) => void, - repeatUrl?: string, -) { +function validationErrorsHandler(dispatch: Dispatch, repeatUrl?: string) { return resp => { if (resp.errors) { const firstError = Object.keys(resp.errors)[0]; diff --git a/src/components/auth/activation/Activation.js b/src/components/auth/activation/Activation.ts similarity index 86% rename from src/components/auth/activation/Activation.js rename to src/components/auth/activation/Activation.ts index 35b724f..d43e1cf 100644 --- a/src/components/auth/activation/Activation.js +++ b/src/components/auth/activation/Activation.ts @@ -1,5 +1,4 @@ -import factory from 'components/auth/factory'; - +import factory from '../factory'; import messages from './Activation.intl.json'; import Body from './ActivationBody'; diff --git a/src/components/auth/appInfo/AppInfo.js b/src/components/auth/appInfo/AppInfo.tsx similarity index 90% rename from src/components/auth/appInfo/AppInfo.js rename to src/components/auth/appInfo/AppInfo.tsx index 4038445..a4345c2 100644 --- a/src/components/auth/appInfo/AppInfo.js +++ b/src/components/auth/appInfo/AppInfo.tsx @@ -1,18 +1,15 @@ -// @flow -import React, { Component } from 'react'; - +import React from 'react'; import { FormattedMessage as Message } from 'react-intl'; - import { Button } from 'components/ui/form'; import { FooterMenu } from 'components/footerMenu'; import styles from './appInfo.scss'; import messages from './AppInfo.intl.json'; -export default class AppInfo extends Component<{ - name?: string, - description?: string, - onGoToAuth: () => void, +export default class AppInfo extends React.Component<{ + name?: string; + description?: string; + onGoToAuth: () => void; }> { render() { const { name, description, onGoToAuth } = this.props; diff --git a/src/components/auth/chooseAccount/ChooseAccount.js b/src/components/auth/chooseAccount/ChooseAccount.ts similarity index 85% rename from src/components/auth/chooseAccount/ChooseAccount.js rename to src/components/auth/chooseAccount/ChooseAccount.ts index 68721a9..be84023 100644 --- a/src/components/auth/chooseAccount/ChooseAccount.js +++ b/src/components/auth/chooseAccount/ChooseAccount.ts @@ -1,4 +1,4 @@ -import factory from 'components/auth/factory'; +import factory from '../factory'; import messages from './ChooseAccount.intl.json'; import Body from './ChooseAccountBody'; diff --git a/src/components/auth/factory.js b/src/components/auth/factory.js deleted file mode 100644 index ef14470..0000000 --- a/src/components/auth/factory.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { Button } from 'components/ui/form'; -import RejectionLink from 'components/auth/RejectionLink'; -import AuthTitle from 'components/auth/AuthTitle'; - -/** - * @param {object} options - * @param {string|object} options.title - panel title - * @param {ReactElement} options.body - * @param {object} options.footer - config for footer Button - * @param {Array|object|null} options.links - link config or an array of link configs - * - * @returns {object} - structure, required for auth panel to work - */ -export default function(options) { - return () => ({ - Title: () => , - Body: options.body, - Footer: () => ); } - if (title) { + if (titleText) { title = ( {icon} - {title} + {titleText} ); } @@ -42,7 +40,7 @@ export function Panel(props: { ); } -export function PanelHeader(props: { children: * }) { +export function PanelHeader(props: { children: React.ReactNode }) { return (
{props.children} @@ -50,7 +48,7 @@ export function PanelHeader(props: { children: * }) { ); } -export function PanelBody(props: { children: * }) { +export function PanelBody(props: { children: React.ReactNode }) { return (
{props.children} @@ -58,7 +56,7 @@ export function PanelBody(props: { children: * }) { ); } -export function PanelFooter(props: { children: * }) { +export function PanelFooter(props: { children: React.ReactNode }) { return (
{props.children} @@ -66,18 +64,18 @@ export function PanelFooter(props: { children: * }) { ); } -export class PanelBodyHeader extends Component< +export class PanelBodyHeader extends React.Component< { - type: 'default' | 'error', - onClose: Function, - children: *, + type?: 'default' | 'error'; + onClose?: () => void; + children: React.ReactNode; }, { - isClosed: boolean, - }, + isClosed: boolean; + } > { state: { - isClosed: boolean, + isClosed: boolean; } = { isClosed: false, }; @@ -105,12 +103,16 @@ export class PanelBodyHeader extends Component< ); } - onClose = (event: MouseEvent) => { + onClose = (event: React.MouseEvent) => { event.preventDefault(); + const { onClose } = this.props; + this.setState({ isClosed: true }); - this.props.onClose(); + if (onClose) { + onClose(); + } }; } diff --git a/src/components/ui/RelativeTime.js b/src/components/ui/RelativeTime.tsx similarity index 86% rename from src/components/ui/RelativeTime.js rename to src/components/ui/RelativeTime.tsx index 277df88..56545d9 100644 --- a/src/components/ui/RelativeTime.js +++ b/src/components/ui/RelativeTime.tsx @@ -1,10 +1,9 @@ -// @flow import React from 'react'; import { FormattedRelativeTime } from 'react-intl'; import { selectUnit } from '@formatjs/intl-utils'; function RelativeTime({ timestamp }: { timestamp: number }) { - const { unit, value }: { unit: any, value: number } = selectUnit(timestamp); + const { unit, value }: { unit: any; value: number } = selectUnit(timestamp); return ( { - state = {}; +class BSoD extends React.Component<{}, State> { + state: State = {}; componentDidMount() { // poll for event id @@ -37,7 +33,6 @@ export default class BSoD extends React.Component< } render() { - const { store } = this.props; const { lastEventId } = this.state; let emailUrl = 'mailto:support@ely.by'; @@ -47,11 +42,11 @@ export default class BSoD extends React.Component< } return ( - +
el && new BoxesField(el)} + ref={(el: HTMLCanvasElement | null) => el && new BoxesField(el)} />
@@ -76,3 +71,5 @@ export default class BSoD extends React.Component< ); } } + +export default BSoD; diff --git a/src/components/ui/bsod/Box.js b/src/components/ui/bsod/Box.js index 4c6b949..ed04b6d 100644 --- a/src/components/ui/bsod/Box.js +++ b/src/components/ui/bsod/Box.js @@ -2,6 +2,7 @@ export default class Box { constructor({ size, startX, startY, startRotate, color, shadowColor }) { this.color = color; this.shadowColor = shadowColor; + this.halfSize = 0; this.setSize(size); this.x = startX; this.y = startY; diff --git a/src/components/ui/bsod/BoxesField.js b/src/components/ui/bsod/BoxesField.js index edc45d5..7c279ca 100644 --- a/src/components/ui/bsod/BoxesField.js +++ b/src/components/ui/bsod/BoxesField.js @@ -5,7 +5,7 @@ import Box from './Box'; */ export default class BoxesField { /** - * @param {Node} elem - canvas DOM node + * @param {HTMLCanvasElement} elem - canvas DOM node * @param {object} params */ constructor( @@ -28,7 +28,13 @@ export default class BoxesField { }, ) { this.elem = elem; - this.ctx = elem.getContext('2d'); + const ctx = elem.getContext('2d'); + + if (!ctx) { + throw new Error('Can not get canvas 2d context'); + } + + this.ctx = ctx; this.params = params; this.light = { diff --git a/src/components/ui/bsod/BsodMiddleware.js b/src/components/ui/bsod/BsodMiddleware.js deleted file mode 100644 index 8bc36ce..0000000 --- a/src/components/ui/bsod/BsodMiddleware.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow -import { InternalServerError } from 'services/request'; - -import type { Resp } from 'services/request'; -import type { logger as Logger } from 'services/logger'; - -const ABORT_ERR = 20; - -export default function BsodMiddleware(dispatchBsod: Function, logger: Logger) { - return { - catch | InternalServerError | Error>(resp?: T): Promise { - const originalResponse: Object = (resp && resp.originalResponse) || {}; - - if ( - resp && - ((resp instanceof InternalServerError && - resp.error.code !== ABORT_ERR) || - (originalResponse && /5\d\d/.test((originalResponse.status: string)))) - ) { - dispatchBsod(); - - if (!resp.message || !/NetworkError/.test(resp.message)) { - let message = 'Unexpected response (BSoD)'; - - if (resp.message) { - message = `BSoD: ${resp.message}`; - } - - logger.warn(message, { resp }); - } - } - - return Promise.reject(resp); - }, - }; -} diff --git a/src/components/ui/bsod/BsodMiddleware.test.js b/src/components/ui/bsod/BsodMiddleware.test.ts similarity index 75% rename from src/components/ui/bsod/BsodMiddleware.test.js rename to src/components/ui/bsod/BsodMiddleware.test.ts index fd0d683..0f49e5b 100644 --- a/src/components/ui/bsod/BsodMiddleware.test.js +++ b/src/components/ui/bsod/BsodMiddleware.test.ts @@ -10,14 +10,14 @@ describe('BsodMiddleware', () => { originalResponse: { status: code }, }; - const dispatch = sinon.spy(); + const dispatchBsod = sinon.spy(); const logger = { warn: sinon.spy() }; - const middleware = new BsodMiddleware(dispatch, logger); + const middleware = new BsodMiddleware(dispatchBsod, logger as any); return expect(middleware.catch(resp), 'to be rejected with', resp).then( () => { - expect(dispatch, 'was called'); + expect(dispatchBsod, 'was called'); expect(logger.warn, 'to have a call satisfying', [ 'Unexpected response (BSoD)', { resp }, @@ -33,14 +33,14 @@ describe('BsodMiddleware', () => { originalResponse: { status: code }, }; - const dispatch = sinon.spy(); + const dispatchBsod = sinon.spy(); const logger = { warn: sinon.spy() }; - const middleware = new BsodMiddleware(dispatch, logger); + const middleware = new BsodMiddleware(dispatchBsod, logger as any); return expect(middleware.catch(resp), 'to be rejected with', resp).then( () => { - expect(dispatch, 'was not called'); + expect(dispatchBsod, 'was not called'); expect(logger.warn, 'was not called'); }, ); diff --git a/src/components/ui/bsod/BsodMiddleware.ts b/src/components/ui/bsod/BsodMiddleware.ts new file mode 100644 index 0000000..428e1c1 --- /dev/null +++ b/src/components/ui/bsod/BsodMiddleware.ts @@ -0,0 +1,49 @@ +import { InternalServerError } from 'services/request'; +import { Resp, Middleware } from 'services/request'; +import defaultLogger from 'services/logger'; + +type Logger = typeof defaultLogger; + +const ABORT_ERR = 20; + +class BsodMiddleware implements Middleware { + dispatchBsod: () => any; + logger: Logger; + + constructor(dispatchBsod: () => any, logger: Logger = defaultLogger) { + this.dispatchBsod = dispatchBsod; + this.logger = logger; + } + + async catch>( + resp?: T | InternalServerError | Error, + ): Promise { + const { originalResponse }: { originalResponse?: Resp } = (resp || + {}) as InternalServerError; + + if ( + resp && + ((resp instanceof InternalServerError && + (resp.error as any).code !== ABORT_ERR) || + (originalResponse && /5\d\d/.test(originalResponse.status))) + ) { + this.dispatchBsod(); + + const { message: errorMessage } = resp as { [key: string]: any }; + + if (!errorMessage || !/NetworkError/.test(errorMessage)) { + let message = 'Unexpected response (BSoD)'; + + if (errorMessage) { + message = `BSoD: ${errorMessage}`; + } + + this.logger.warn(message, { resp }); + } + } + + return Promise.reject(resp); + } +} + +export default BsodMiddleware; diff --git a/src/components/ui/bsod/dispatchBsod.js b/src/components/ui/bsod/dispatchBsod.js index c59d2b6..41e8085 100644 --- a/src/components/ui/bsod/dispatchBsod.js +++ b/src/components/ui/bsod/dispatchBsod.js @@ -11,7 +11,7 @@ export default function dispatchBsod(store = injectedStore) { store.dispatch(bsod()); onBsod && onBsod(); - ReactDOM.render(, document.getElementById('app')); + ReactDOM.render(, document.getElementById('app')); } export function inject(store, stopLoading) { diff --git a/src/components/ui/collapse/Collapse.js b/src/components/ui/collapse/Collapse.tsx similarity index 91% rename from src/components/ui/collapse/Collapse.js rename to src/components/ui/collapse/Collapse.tsx index cc200c6..89cd659 100644 --- a/src/components/ui/collapse/Collapse.js +++ b/src/components/ui/collapse/Collapse.tsx @@ -1,5 +1,3 @@ -// @flow -import type { Node } from 'react'; import React, { Component } from 'react'; import { Motion, spring } from 'react-motion'; import MeasureHeight from 'components/MeasureHeight'; @@ -7,17 +5,17 @@ import MeasureHeight from 'components/MeasureHeight'; import styles from './collapse.scss'; type Props = { - isOpened?: boolean, - children: Node, - onRest: () => void, + isOpened?: boolean; + children: React.ReactNode; + onRest: () => void; }; export default class Collapse extends Component< Props, { - height: number, - wasInitialized: boolean, - }, + height: number; + wasInitialized: boolean; + } > { state = { height: 0, diff --git a/src/components/ui/collapse/index.js b/src/components/ui/collapse/index.ts similarity index 80% rename from src/components/ui/collapse/index.js rename to src/components/ui/collapse/index.ts index 940f5df..12bcb63 100644 --- a/src/components/ui/collapse/index.js +++ b/src/components/ui/collapse/index.ts @@ -1,2 +1 @@ -// @flow export { default } from './Collapse'; diff --git a/src/components/ui/form/Button.js b/src/components/ui/form/Button.tsx similarity index 51% rename from src/components/ui/form/Button.js rename to src/components/ui/form/Button.tsx index cfc8416..db22a0a 100644 --- a/src/components/ui/form/Button.js +++ b/src/components/ui/form/Button.tsx @@ -1,39 +1,35 @@ -// @flow -import type { MessageDescriptor } from 'react-intl'; -import type { ComponentType } from 'react'; -import type { Color } from 'components/ui'; import React from 'react'; import classNames from 'classnames'; import buttons from 'components/ui/buttons.scss'; import { COLOR_GREEN } from 'components/ui'; +import { MessageDescriptor } from 'react-intl'; +import { Color } from 'components/ui'; import FormComponent from './FormComponent'; -export default class Button extends FormComponent<{ - label: string | MessageDescriptor, - block?: boolean, - small?: boolean, - loading?: boolean, - className?: string, - color: Color, - disabled?: boolean, - component: string | ComponentType, -}> { - static defaultProps = { - color: COLOR_GREEN, - component: 'button', - }; - +export default class Button extends FormComponent< + { + // TODO: drop MessageDescriptor support. It should be React.ReactNode only + label: string | MessageDescriptor | React.ReactElement; + block?: boolean; + small?: boolean; + loading?: boolean; + className?: string; + color?: Color; + disabled?: boolean; + component?: string | React.ComponentType; + } & React.ButtonHTMLAttributes +> { render() { const { - color, + color = COLOR_GREEN, block, small, disabled, className, loading, label, - component: ComponentProp, + component: ComponentProp = 'button', ...restProps } = this.props; @@ -52,7 +48,9 @@ export default class Button extends FormComponent<{ disabled={disabled} {...restProps} > - {this.formatMessage(label)} + {typeof label === 'object' && React.isValidElement(label) + ? label + : this.formatMessage(label)} ); } diff --git a/src/components/ui/form/Captcha.js b/src/components/ui/form/Captcha.tsx similarity index 90% rename from src/components/ui/form/Captcha.js rename to src/components/ui/form/Captcha.tsx index 9c99b49..e6e19a8 100644 --- a/src/components/ui/form/Captcha.js +++ b/src/components/ui/form/Captcha.tsx @@ -1,8 +1,7 @@ -// @flow import React from 'react'; import classNames from 'classnames'; -import type { CaptchaID } from 'services/captcha'; -import type { Skin } from 'components/ui'; +import { CaptchaID } from 'services/captcha'; +import { Skin } from 'components/ui'; import captcha from 'services/captcha'; import logger from 'services/logger'; import { ComponentLoader } from 'components/ui/loader'; @@ -12,12 +11,12 @@ import FormInputComponent from './FormInputComponent'; export default class Captcha extends FormInputComponent< { - delay: number, - skin: Skin, + delay: number; + skin: Skin; }, { - code: string, - }, + code: string; + } > { elRef = React.createRef(); captchaId: CaptchaID; diff --git a/src/components/ui/form/Checkbox.js b/src/components/ui/form/Checkbox.tsx similarity index 84% rename from src/components/ui/form/Checkbox.js rename to src/components/ui/form/Checkbox.tsx index 9b67e63..8d1a34d 100644 --- a/src/components/ui/form/Checkbox.js +++ b/src/components/ui/form/Checkbox.tsx @@ -1,18 +1,16 @@ -// @flow -import type { Color, Skin } from 'components/ui'; -import type { MessageDescriptor } from 'react-intl'; import React from 'react'; +import { MessageDescriptor } from 'react-intl'; import classNames from 'classnames'; -import { SKIN_DARK, COLOR_GREEN } from 'components/ui'; +import { SKIN_DARK, COLOR_GREEN, Color, Skin } from 'components/ui'; import { omit } from 'functions'; import styles from './form.scss'; import FormInputComponent from './FormInputComponent'; export default class Checkbox extends FormInputComponent<{ - color: Color, - skin: Skin, - label: string | MessageDescriptor, + color: Color; + skin: Skin; + label: string | MessageDescriptor; }> { static defaultProps = { color: COLOR_GREEN, diff --git a/src/components/ui/form/Dropdown.js b/src/components/ui/form/Dropdown.js index 3d5a83a..5a612d1 100644 --- a/src/components/ui/form/Dropdown.js +++ b/src/components/ui/form/Dropdown.js @@ -108,7 +108,7 @@ export default class Dropdown extends FormInputComponent { getActiveItem() { const { items } = this.props; - let { activeItem } = this.state; + let { activeItem } = /** @type {any} */ (this.state); if (!activeItem) { activeItem = { @@ -117,10 +117,11 @@ export default class Dropdown extends FormInputComponent { }; if (!activeItem.label) { - const firstItem = Object.entries(items)[0]; + const [[value, label]] = Object.entries(items); + activeItem = { - label: firstItem[1], - value: firstItem[0], + label, + value, }; } } diff --git a/src/components/ui/form/Form.js b/src/components/ui/form/Form.tsx similarity index 79% rename from src/components/ui/form/Form.js rename to src/components/ui/form/Form.tsx index 6828f79..26fb186 100644 --- a/src/components/ui/form/Form.js +++ b/src/components/ui/form/Form.tsx @@ -1,27 +1,25 @@ -// @flow -import type { Node } from 'react'; -import React, { Component } from 'react'; +import React from 'react'; import classNames from 'classnames'; import logger from 'services/logger'; -import type FormModel from './FormModel'; +import FormModel from './FormModel'; import styles from './form.scss'; -type Props = {| - id: string, - isLoading: boolean, - form?: FormModel, - onSubmit: (form: FormModel | FormData) => void | Promise, - onInvalid: (errors: { [errorKey: string]: string }) => void, - children: Node, -|}; -type State = { - isTouched: boolean, - isLoading: boolean, -}; +interface Props { + id: string; + isLoading: boolean; + form?: FormModel; + onSubmit: (form: FormModel | FormData) => void | Promise; + onInvalid: (errors: { [errorKey: string]: string }) => void; + children: React.ReactNode; +} +interface State { + isTouched: boolean; + isLoading: boolean; +} type InputElement = HTMLInputElement | HTMLTextAreaElement; -export default class Form extends Component { +export default class Form extends React.Component { static defaultProps = { id: 'default', isLoading: false, @@ -34,7 +32,7 @@ export default class Form extends Component { isLoading: this.props.isLoading || false, }; - formEl: ?HTMLFormElement; + formEl: HTMLFormElement | null; mounted = false; @@ -88,7 +86,7 @@ export default class Form extends Component { [styles.formTouched]: this.state.isTouched, })} onSubmit={this.onFormSubmit} - ref={(el: ?HTMLFormElement) => (this.formEl = el)} + ref={(el: HTMLFormElement | null) => (this.formEl = el)} noValidate > {this.props.children} @@ -124,17 +122,17 @@ export default class Form extends Component { .finally(() => this.mounted && this.setState({ isLoading: false })); } } else { - const invalidEls: NodeList = (form.querySelectorAll( + const invalidEls: NodeListOf = form.querySelectorAll( ':invalid', - ): any); + ); const errors = {}; invalidEls[0].focus(); // focus on first error - Array.from(invalidEls).reduce((errors, el: InputElement) => { + Array.from(invalidEls).reduce((acc, el: InputElement) => { if (!el.name) { logger.warn('Found an element without name', { el }); - return errors; + return acc; } let errorMessage = el.validationMessage; @@ -145,9 +143,9 @@ export default class Form extends Component { errorMessage = `error.${el.name}_invalid`; } - errors[el.name] = errorMessage; + acc[el.name] = errorMessage; - return errors; + return acc; }, errors); this.setErrors(errors); @@ -159,7 +157,7 @@ export default class Form extends Component { this.props.onInvalid(errors); } - onFormSubmit = (event: Event) => { + onFormSubmit = (event: React.FormEvent) => { event.preventDefault(); this.submit(); diff --git a/src/components/ui/form/FormComponent.js b/src/components/ui/form/FormComponent.js deleted file mode 100644 index 8ffc054..0000000 --- a/src/components/ui/form/FormComponent.js +++ /dev/null @@ -1,41 +0,0 @@ -// @flow -import type { MessageDescriptor } from 'react-intl'; -import type { Element } from 'react'; -import { Component } from 'react'; -import i18n from 'services/i18n'; - -class FormComponent extends Component { - /** - * Formats message resolving intl translations - * - * @param {string|object} message - message string, or intl message descriptor with an `id` field - * - * @returns {string} - */ - formatMessage( - message: string | MessageDescriptor | Element, - ): string | Element { - if (typeof message === 'string') { - return message; - } - - if (message && message.id) { - return i18n.getIntl().formatMessage(message); - } - - return ((message: any): Element); - } - - /** - * Focuses this field - */ - focus() {} - - /** - * A hook, that called, when the form was submitted with invalid data - * This is usefull for the cases, when some field needs to be refreshed e.g. captcha - */ - onFormInvalid() {} -} - -export default FormComponent; diff --git a/src/components/ui/form/FormComponent.tsx b/src/components/ui/form/FormComponent.tsx new file mode 100644 index 0000000..23f728b --- /dev/null +++ b/src/components/ui/form/FormComponent.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { MessageDescriptor } from 'react-intl'; +import i18n from 'services/i18n'; + +class FormComponent extends React.Component { + /** + * Formats message resolving intl translations + * + * @param {string|object} message - message string, or intl message descriptor with an `id` field + * + * @returns {string} + */ + formatMessage(message: string | MessageDescriptor): string { + if (!message) { + throw new Error('A message is required'); + } + + if (typeof message === 'string') { + return message; + } + + if (!message.id) { + throw new Error(`Invalid message format: ${JSON.stringify(message)}`); + } + + return i18n.getIntl().formatMessage(message); + } + + /** + * Focuses this field + */ + focus() {} + + /** + * A hook, that called, when the form was submitted with invalid data + * This is useful for the cases, when some field needs to be refreshed e.g. captcha + */ + onFormInvalid() {} +} + +export default FormComponent; diff --git a/src/components/ui/form/FormError.js b/src/components/ui/form/FormError.tsx similarity index 71% rename from src/components/ui/form/FormError.js rename to src/components/ui/form/FormError.tsx index b3d175e..472c553 100644 --- a/src/components/ui/form/FormError.js +++ b/src/components/ui/form/FormError.tsx @@ -1,11 +1,20 @@ -import PropTypes from 'prop-types'; import React from 'react'; - +import PropTypes from 'prop-types'; import errorsDict from 'services/errorsDict'; import styles from './form.scss'; -export default function FormError({ error }) { +export default function FormError({ + error, +}: { + error?: + | string + | React.ReactNode + | { + type: string; + payload: { [key: string]: any }; + }; +}) { return error ? (
{errorsDict.resolve(error)}
) : null; diff --git a/src/components/ui/form/FormInputComponent.js b/src/components/ui/form/FormInputComponent.tsx similarity index 75% rename from src/components/ui/form/FormInputComponent.js rename to src/components/ui/form/FormInputComponent.tsx index 9ac7bc0..47c2779 100644 --- a/src/components/ui/form/FormInputComponent.js +++ b/src/components/ui/form/FormInputComponent.tsx @@ -1,19 +1,18 @@ -// @flow -import type { MessageDescriptor } from 'react-intl'; import React from 'react'; +import { MessageDescriptor } from 'react-intl'; import FormComponent from './FormComponent'; import FormError from './FormError'; type Error = string | MessageDescriptor; -export default class FormInputComponent extends FormComponent< +export default class FormInputComponent extends FormComponent< P & { - error?: Error, + error?: Error; }, S & { - error?: Error, - }, + error?: Error; + } > { componentWillReceiveProps() { if (this.state && this.state.error) { @@ -30,6 +29,7 @@ export default class FormInputComponent extends FormComponent< } setError(error: Error) { + // @ts-ignore this.setState({ error }); } } diff --git a/src/components/ui/form/FormModel.js b/src/components/ui/form/FormModel.ts similarity index 89% rename from src/components/ui/form/FormModel.js rename to src/components/ui/form/FormModel.ts index 5bdf751..aabfc84 100644 --- a/src/components/ui/form/FormModel.js +++ b/src/components/ui/form/FormModel.ts @@ -1,11 +1,19 @@ -// @flow import FormInputComponent from './FormInputComponent'; type LoadingListener = (isLoading: boolean) => void; +type ValidationError = + | string + | { + type: string; + payload?: { [key: string]: any }; + }; + export default class FormModel { fields = {}; - errors = {}; + errors: { + [fieldId: string]: ValidationError; + } = {}; handlers: LoadingListener[] = []; renderErrors: boolean; _isLoading: boolean; @@ -32,9 +40,9 @@ export default class FormModel { bindField(name: string) { this.fields[name] = {}; - const props: Object = { + const props: { [key: string]: any } = { name, - ref: (el: ?FormInputComponent) => { + ref: (el: FormInputComponent | null) => { if (el) { if (!(el instanceof FormInputComponent)) { throw new Error('Expected FormInputComponent component'); @@ -100,7 +108,7 @@ export default class FormModel { * * @param {object} errors - object maping {fieldId: errorType} */ - setErrors(errors: { [key: string]: string }) { + setErrors(errors: { [key: string]: ValidationError }) { if (typeof errors !== 'object' || errors === null) { throw new Error('Errors must be an object'); } @@ -121,11 +129,10 @@ export default class FormModel { }); } - /** - * @returns {object|string|null} - */ - getFirstError() { - return this.errors ? Object.values(this.errors).shift() : null; + getFirstError(): ValidationError | null { + const [error] = Object.values(this.errors); + + return error || null; } /** @@ -200,10 +207,7 @@ export default class FormModel { this.notifyHandlers(); } - /** - * @api private - */ - notifyHandlers() { + private notifyHandlers() { this.handlers.forEach(fn => fn(this._isLoading)); } } diff --git a/src/components/ui/form/Input.test.js b/src/components/ui/form/Input.test.tsx similarity index 96% rename from src/components/ui/form/Input.test.js rename to src/components/ui/form/Input.test.tsx index b0bc814..c977b1f 100644 --- a/src/components/ui/form/Input.test.js +++ b/src/components/ui/form/Input.test.tsx @@ -7,7 +7,8 @@ import Input from './Input'; describe('Input', () => { it('should return input value', () => { - let component; + let component: any; + const wrapper = mount( , 'placeholder'> & { + skin: Skin; + color: Color; + center: boolean; + disabled: boolean; + label?: string | MessageDescriptor; + placeholder?: string | MessageDescriptor; + error?: string | { type: string; payload: string }; + icon?: string; + copy?: boolean; }, { - wasCopied: boolean, - }, + wasCopied: boolean; + } > { static defaultProps = { color: COLOR_GREEN, @@ -43,43 +41,68 @@ export default class Input extends FormInputComponent< elRef = React.createRef(); render() { - const { color, skin, center } = this.props; - let { icon, label, copy } = this.props; + const { + color, + skin, + center, + icon: iconType, + copy: showCopyIcon, + placeholder: placeholderText, + } = this.props; + let { label: labelContent } = this.props; const { wasCopied } = this.state; + let placeholder: string | undefined; const props = omit( { type: 'text', ...this.props, }, - ['label', 'error', 'skin', 'color', 'center', 'icon', 'copy'], + [ + 'label', + 'placeholder', + 'error', + 'skin', + 'color', + 'center', + 'icon', + 'copy', + ], ); - if (label) { + let label: React.ReactElement | null = null; + let copyIcon: React.ReactElement | null = null; + let icon: React.ReactElement | null = null; + + if (labelContent) { if (!props.id) { props.id = uniqueId('input'); } - label = this.formatMessage(label); + labelContent = this.formatMessage(labelContent); label = ( ); } - props.placeholder = this.formatMessage(props.placeholder); + if (placeholderText) { + placeholder = this.formatMessage(placeholderText); + } let baseClass = styles.formRow; - if (icon) { + if (iconType) { baseClass = styles.formIconRow; - icon = ; + icon = ( + + ); } - if (copy) { - copy = ( + if (showCopyIcon) { + copyIcon = (
{icon} - {copy} + {copyIcon}
{this.renderError()}
diff --git a/src/components/ui/form/LinkButton.js b/src/components/ui/form/LinkButton.js deleted file mode 100644 index dffcb5f..0000000 --- a/src/components/ui/form/LinkButton.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow -import type { ElementConfig } from 'react'; -import React from 'react'; -import { Link } from 'react-router-dom'; - -import Button from './Button'; - -export default function LinkButton(props: { - ...$Exact>, - ...$Exact>, -}) { - const { to, ...restProps } = props; - - return