Centralize all redux types into one place, add overrides for the connect, useSelector and useDispatch functions

This commit is contained in:
ErickSkrauch
2020-07-22 13:01:12 +03:00
parent 96e74cf9bb
commit 5a9c54002d
44 changed files with 199 additions and 141 deletions

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import clsx from 'clsx';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import { connect } from 'app/functions';
import * as loader from 'app/services/loader';
import { SKIN_DARK, COLOR_WHITE, Skin } from 'app/components/ui';
import { Button } from 'app/components/ui/form';
import { authenticate, revoke } from 'app/components/accounts/actions';
import { getActiveAccount, Account } from 'app/components/accounts/reducer';
import { RootState } from 'app/reducers';
import { State as AccountState } from 'app/components/accounts/reducer';
import styles from './accountSwitcher.scss';
@@ -19,7 +20,7 @@ interface Props {
onAfterAction: () => void;
// called after switching an account. The active account will be passed as arg
onSwitch: (account: Account) => void;
accounts: RootState['accounts'];
accounts: AccountState;
skin: Skin;
// whether active account should be expanded and shown on the top
highlightActiveAccount: boolean;
@@ -166,7 +167,7 @@ export class AccountSwitcher extends React.Component<Props> {
}
export default connect(
({ accounts }: RootState) => ({
({ accounts }) => ({
accounts,
}),
{

View File

@@ -8,7 +8,7 @@ import { authenticate, revoke, logoutAll, logoutStrangers } from 'app/components
import { add, activate, remove, reset } from 'app/components/accounts/actions/pure-actions';
import { updateUser, setUser } from 'app/components/user/actions';
import { setLogin, setAccountSwitcher } from 'app/components/auth/actions';
import { Dispatch, RootState } from 'app/reducers';
import { Dispatch, State as RootState } from 'app/types';
import { Account } from './reducer';

View File

@@ -5,7 +5,7 @@ import { relogin as navigateToLogin, setAccountSwitcher } from 'app/components/a
import { updateUser, setGuest } from 'app/components/user/actions';
import { setLocale } from 'app/components/i18n/actions';
import logger from 'app/services/logger';
import { ThunkAction } from 'app/reducers';
import { Action as AppAction } from 'app/types';
import { getActiveAccount, Account } from './reducer';
import { add, remove, activate, reset, updateToken } from './actions/pure-actions';
@@ -26,7 +26,7 @@ export function authenticate(
token: string;
refreshToken: string | null;
},
): ThunkAction<Promise<Account>> {
): AppAction<Promise<Account>> {
const { token, refreshToken } = account;
const email = 'email' in account ? account.email : null;
@@ -106,7 +106,7 @@ export function authenticate(
/**
* Re-fetch user data for currently active account
*/
export function refreshUserData(): ThunkAction<Promise<void>> {
export function refreshUserData(): AppAction<Promise<void>> {
return async (dispatch, getState) => {
const activeAccount = getActiveAccount(getState());
@@ -142,7 +142,7 @@ function findAccountIdFromToken(token: string): number {
*
* @returns {Function}
*/
export function ensureToken(): ThunkAction<Promise<void>> {
export function ensureToken(): AppAction<Promise<void>> {
return (dispatch, getState) => {
const { token } = getActiveAccount(getState()) || {};
@@ -182,7 +182,7 @@ export function recoverFromTokenError(
status: number;
message: string;
} | void,
): ThunkAction<Promise<void>> {
): AppAction<Promise<void>> {
return (dispatch, getState) => {
if (error && error.status === 401) {
const activeAccount = getActiveAccount(getState());
@@ -218,7 +218,7 @@ export function recoverFromTokenError(
*
* @returns {Function}
*/
export function requestNewToken(): ThunkAction<Promise<void>> {
export function requestNewToken(): AppAction<Promise<void>> {
return (dispatch, getState) => {
const { refreshToken } = getActiveAccount(getState()) || {};
@@ -250,7 +250,7 @@ export function requestNewToken(): ThunkAction<Promise<void>> {
*
* @returns {Function}
*/
export function revoke(account: Account): ThunkAction<Promise<void>> {
export function revoke(account: Account): AppAction<Promise<void>> {
return async (dispatch, getState) => {
const accountToReplace: Account | null =
getState().accounts.available.find(({ id }) => id !== account.id) || null;
@@ -276,7 +276,7 @@ export function revoke(account: Account): ThunkAction<Promise<void>> {
};
}
export function relogin(email?: string): ThunkAction<Promise<void>> {
export function relogin(email?: string): AppAction<Promise<void>> {
return async (dispatch, getState) => {
const activeAccount = getActiveAccount(getState());
@@ -288,7 +288,7 @@ export function relogin(email?: string): ThunkAction<Promise<void>> {
};
}
export function logoutAll(): ThunkAction<Promise<void>> {
export function logoutAll(): AppAction<Promise<void>> {
return (dispatch, getState) => {
dispatch(setGuest());
@@ -317,7 +317,7 @@ export function logoutAll(): ThunkAction<Promise<void>> {
*
* @returns {Function}
*/
export function logoutStrangers(): ThunkAction<Promise<void>> {
export function logoutStrangers(): AppAction<Promise<void>> {
return async (dispatch, getState) => {
const {
accounts: { available },

View File

@@ -1,7 +1,8 @@
import React, { CSSProperties, MouseEventHandler, ReactElement, ReactNode } from 'react';
import { AccountsState } from 'app/components/accounts';
import { User } from 'app/components/user';
import { connect } from 'react-redux';
import { connect } from 'app/functions';
import { TransitionMotion, spring, PlainStyle, Style, TransitionStyle, TransitionPlainStyle } from 'react-motion';
import { Panel, PanelBody, PanelFooter, PanelHeader } from 'app/components/ui/Panel';
import { Form } from 'app/components/ui/form';
@@ -9,7 +10,6 @@ import MeasureHeight from 'app/components/MeasureHeight';
import panelStyles from 'app/components/ui/panel.scss';
import icons from 'app/components/ui/icons.scss';
import authFlow from 'app/services/authFlow';
import { RootState } from 'app/reducers';
import { Provider as AuthContextProvider } from './Context';
import { getLogin, State as AuthState } from './reducer';
@@ -546,7 +546,7 @@ class PanelTransition extends React.PureComponent<Props, State> {
}
export default connect(
(state: RootState) => {
(state) => {
const login = getLogin(state);
let user = {
...state.user,

View File

@@ -23,7 +23,7 @@ import dispatchBsod from 'app/components/ui/bsod/dispatchBsod';
import { create as createPopup } from 'app/components/ui/popup/actions';
import ContactForm from 'app/components/contact';
import { Account } from 'app/components/accounts/reducer';
import { ThunkAction, Dispatch } from 'app/reducers';
import { Action as AppAction, Dispatch } from 'app/types';
import { Resp } from 'app/services/request';
import { Credentials, Client, OAuthState, getCredentials } from './reducer';
@@ -189,7 +189,7 @@ export function register({
);
}
export function activate({ key = '' }: { key: string }): ThunkAction<Promise<Account>> {
export function activate({ key = '' }: { key: string }): AppAction<Promise<Account>> {
return wrapInLoader((dispatch) =>
activateEndpoint(key)
.then(authHandler(dispatch))
@@ -239,7 +239,7 @@ export function setLogin(login: string | null): SetCredentialsAction {
return setCredentials(login ? { login } : null);
}
export function relogin(login: string | null): ThunkAction {
export function relogin(login: string | null): AppAction {
return (dispatch, getState) => {
const credentials = getCredentials(getState());
const returnUrl = credentials.returnUrl || location.pathname + location.search;
@@ -266,7 +266,7 @@ function requestTotp({
login: string;
password: string;
rememberMe: boolean;
}): ThunkAction {
}): AppAction {
return (dispatch, getState) => {
// merging with current credentials to propogate returnUrl
const credentials = getCredentials(getState());
@@ -521,7 +521,7 @@ export function setOAuthCode(payload: { success: boolean; code: string; displayC
};
}
export function resetOAuth(): ThunkAction {
export function resetOAuth(): AppAction {
return (dispatch): void => {
localStorage.removeItem('oauthData');
dispatch(setOAuthRequest({}));
@@ -531,7 +531,7 @@ export function resetOAuth(): ThunkAction {
/**
* Resets all temporary state related to auth
*/
export function resetAuth(): ThunkAction {
export function resetAuth(): AppAction {
return (dispatch, getSate): Promise<void> => {
dispatch(setLogin(null));
dispatch(resetOAuth());
@@ -588,7 +588,7 @@ export function setLoadingState(isLoading: boolean): SetLoadingAction {
};
}
function wrapInLoader<T>(fn: ThunkAction<Promise<T>>): ThunkAction<Promise<T>> {
function wrapInLoader<T>(fn: AppAction<Promise<T>>): AppAction<Promise<T>> {
return (dispatch, getState) => {
dispatch(setLoadingState(true));
const endLoading = () => dispatch(setLoadingState(false));
@@ -671,3 +671,12 @@ function validationErrorsHandler(
return Promise.reject(resp);
};
}
export type Action =
| ErrorAction
| CredentialsAction
| AccountSwitcherAction
| LoadingAction
| ClientAction
| OAuthAction
| ScopesAction;

View File

@@ -1,10 +1,10 @@
import React, { MouseEventHandler } from 'react';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { connect } from 'app/functions';
import { Button } from 'app/components/ui/form';
import copy from 'app/services/copy';
import { RootState } from 'app/reducers';
import styles from './finish.scss';
@@ -104,7 +104,7 @@ class Finish extends React.Component<Props> {
};
}
export default connect(({ auth }: RootState) => {
export default connect(({ auth }) => {
if (!auth || !auth.client || !auth.oauth) {
throw new Error('Can not connect Finish component. No auth data in state');
}

View File

@@ -1,6 +1,7 @@
import { combineReducers, Reducer } from 'redux';
import { RootState } from 'app/reducers';
import { Scope } from '../../services/api/oauth';
import { State as RootState } from 'app/types';
import { Scope } from 'app/services/api/oauth';
import {
ErrorAction,

View File

@@ -1,9 +1,8 @@
import React, { ComponentType, useCallback, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { send as sendFeedback } from 'app/services/api/feedback';
import { RootState } from 'app/reducers';
import logger from 'app/services/logger';
import { useReduxSelector } from 'app/functions';
import ContactFormPopup from './ContactFormPopup';
import SuccessContactFormPopup from './SuccessContactFormPopup';
@@ -13,7 +12,7 @@ interface Props {
}
const ContactForm: ComponentType<Props> = ({ onClose }) => {
const userEmail = useSelector((state: RootState) => state.user.email);
const userEmail = useReduxSelector((state) => state.user.email);
const usedEmailRef = useRef(userEmail); // Use ref to avoid unneeded redraw
const [isSent, setIsSent] = useState<boolean>(false);
const onSubmit = useCallback(

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { connect } from 'app/functions';
import { create as createPopup } from 'app/components/ui/popup/actions';
import ContactForm from 'app/components/contact';

View File

@@ -1,10 +1,11 @@
import { Dispatch, Action as ReduxAction } from 'redux';
import { OauthAppResponse } from 'app/services/api/oauth';
import oauth from 'app/services/api/oauth';
import { User } from 'app/components/user';
import { ThunkAction } from 'app/reducers';
import { Action as AppAction } from 'app/types';
import { Apps } from './reducer';
import { State } from './reducer';
interface SetAvailableAction extends ReduxAction {
type: 'apps:setAvailable';
@@ -18,11 +19,11 @@ export function setAppsList(apps: Array<OauthAppResponse>): SetAvailableAction {
};
}
export function getApp(state: { apps: Apps }, clientId: string): OauthAppResponse | null {
export function getApp(state: { apps: State }, clientId: string): OauthAppResponse | null {
return state.apps.available.find((app) => app.clientId === clientId) || null;
}
export function fetchApp(clientId: string): ThunkAction<Promise<void>> {
export function fetchApp(clientId: string): AppAction<Promise<void>> {
return async (dispatch) => {
const app = await oauth.getApp(clientId);
@@ -78,7 +79,7 @@ function createDeleteAppAction(clientId: string): DeleteAppAction {
};
}
export function resetApp(clientId: string, resetSecret: boolean): ThunkAction<Promise<void>> {
export function resetApp(clientId: string, resetSecret: boolean): AppAction<Promise<void>> {
return async (dispatch) => {
const { data: app } = await oauth.reset(clientId, resetSecret);

View File

@@ -2,15 +2,15 @@ import { OauthAppResponse } from 'app/services/api/oauth';
import { Action } from './actions';
export interface Apps {
export interface State {
available: Array<OauthAppResponse>;
}
const defaults: Apps = {
const defaults: State = {
available: [],
};
export default function apps(state: Apps = defaults, action: Action): Apps {
export default function apps(state: State = defaults, action: Action): State {
switch (action.type) {
case 'apps:setAvailable':
return {

View File

@@ -1,7 +1,8 @@
import React, { ComponentType, MouseEventHandler, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import { useReduxDispatch } from 'app/functions';
import LanguageSwitcher from 'app/components/languageSwitcher';
import SourceCode from 'app/components/sourceCode';
import { create as createPopup } from 'app/components/ui/popup/actions';
@@ -10,7 +11,7 @@ import { ContactLink } from 'app/components/contact';
import styles from './footerMenu.scss';
const FooterMenu: ComponentType = () => {
const dispatch = useDispatch();
const dispatch = useReduxDispatch();
const createPopupHandler = useCallback(
(popup: ComponentType): MouseEventHandler<HTMLAnchorElement> => (event) => {

View File

@@ -1,12 +1,12 @@
import React, { useState, useEffect, ComponentType } from 'react';
import { useSelector } from 'react-redux';
import { RawIntlProvider, IntlShape } from 'react-intl';
import i18n from 'app/services/i18n';
import { RootState } from 'app/reducers';
import { useReduxSelector } from 'app/functions';
const IntlProvider: ComponentType = ({ children }) => {
const [intl, setIntl] = useState<IntlShape>();
const locale = useSelector(({ i18n: i18nState }: RootState) => i18nState.locale);
const locale = useReduxSelector(({ i18n: i18nState }) => i18nState.locale);
useEffect(() => {
if (process.env.NODE_ENV === 'test') {

View File

@@ -1,8 +1,9 @@
import { Action as ReduxAction } from 'redux';
import i18n from 'app/services/i18n';
import { ThunkAction } from 'app/reducers';
export function setLocale(desiredLocale: string): ThunkAction<Promise<string>> {
import i18n from 'app/services/i18n';
import { Action as AppAction } from 'app/types';
export function setLocale(desiredLocale: string): AppAction<Promise<string>> {
return async (dispatch) => {
const locale = i18n.detectLanguage(desiredLocale);

View File

@@ -1,9 +1,8 @@
import React, { ComponentType, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import LOCALES from 'app/i18n';
import { changeLang } from 'app/components/user/actions';
import { RootState } from 'app/reducers';
import { useReduxDispatch, useReduxSelector } from 'app/functions';
import LanguageSwitcherPopup from './LanguageSwitcherPopup';
@@ -12,8 +11,8 @@ type Props = {
};
const LanguageSwitcher: ComponentType<Props> = ({ onClose = () => {} }) => {
const selectedLocale = useSelector((state: RootState) => state.i18n.locale);
const dispatch = useDispatch();
const selectedLocale = useReduxSelector((state) => state.i18n.locale);
const dispatch = useReduxDispatch();
const onChangeLang = useCallback(
(lang: string) => {

View File

@@ -1,22 +1,22 @@
import React, { ComponentType, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import clsx from 'clsx';
import { useReduxDispatch, useReduxSelector } from 'app/functions';
import { getLocaleIconUrl } from 'app/components/i18n';
import LANGS from 'app/i18n';
import { create as createPopup } from 'app/components/ui/popup/actions';
import LanguageSwitcher from 'app/components/languageSwitcher';
import { RootState } from 'app/reducers';
import styles from './link.scss';
const LanguageLink: ComponentType = () => {
const dispatch = useDispatch();
const dispatch = useReduxDispatch();
const showLanguageSwitcherPopup = useCallback(() => {
dispatch(createPopup({ Popup: LanguageSwitcher }));
}, [dispatch]);
const userLang = useSelector((state: RootState) => state.user.lang);
const interfaceLocale = useSelector((state: RootState) => state.i18n.locale);
const userLang = useReduxSelector((state) => state.user.lang);
const interfaceLocale = useReduxSelector((state) => state.i18n.locale);
const localeDefinition = LANGS[userLang] || LANGS[interfaceLocale];

View File

@@ -1,7 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ContextProvider } from 'app/shell';
import { Store } from 'app/reducers';
import { Store } from 'app/types';
import { History } from 'history';
import { bsod } from './actions';

View File

@@ -1,6 +1,6 @@
import request from 'app/services/request';
import logger from 'app/services/logger';
import { Store } from 'app/reducers';
import { Store } from 'app/types';
import { History } from 'history';
import dispatchBsod, { inject } from './dispatchBsod';

View File

@@ -1,9 +1,9 @@
import React, { ReactNode } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { browserHistory } from 'app/services/history';
import { connect } from 'react-redux';
import { Location } from 'history';
import { RootState } from 'app/reducers';
import { connect } from 'app/functions';
import { browserHistory } from 'app/services/history';
import { PopupConfig } from './reducer';
import { destroy } from './actions';
@@ -95,7 +95,7 @@ export class PopupStack extends React.Component<Props> {
}
export default connect(
(state: RootState) => ({
(state) => ({
...state.popup,
}),
{

View File

@@ -2,7 +2,7 @@ import { Action as ReduxAction } from 'redux';
import { changeLang as changeLangEndpoint, acceptRules as acceptRulesEndpoint } from 'app/services/api/accounts';
import { setLocale } from 'app/components/i18n/actions';
import { ThunkAction } from 'app/reducers';
import { Action as AppAction } from 'app/types';
import { User } from './reducer';
@@ -55,7 +55,7 @@ function changeLangPure(payload: string): ChangeLangAction {
};
}
export function changeLang(targetLang: string): ThunkAction<Promise<void>> {
export function changeLang(targetLang: string): AppAction<Promise<void>> {
return (dispatch, getState) =>
dispatch(setLocale(targetLang)).then((lang: string) => {
const { id, isGuest, lang: oldLang } = getState().user;
@@ -72,7 +72,7 @@ export function changeLang(targetLang: string): ThunkAction<Promise<void>> {
});
}
export function setGuest(): ThunkAction<Promise<void>> {
export function setGuest(): AppAction<Promise<void>> {
return async (dispatch, getState) => {
dispatch(
setUser({
@@ -83,7 +83,7 @@ export function setGuest(): ThunkAction<Promise<void>> {
};
}
export function acceptRules(): ThunkAction<Promise<{ success: boolean }>> {
export function acceptRules(): AppAction<Promise<{ success: boolean }>> {
return (dispatch, getState) => {
const { id } = getState().user;

View File

@@ -1,7 +1,7 @@
import { authenticate, logoutStrangers } from 'app/components/accounts/actions';
import { getActiveAccount } from 'app/components/accounts/reducer';
import request from 'app/services/request';
import { Store } from 'app/reducers';
import { Store } from 'app/types';
import { changeLang } from './actions';
import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware';

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import expect from 'app/test/unexpected';
import { RootState } from 'app/reducers';
import { State as RootState } from 'app/types';
import bearerHeaderMiddleware from 'app/components/user/middlewares/bearerHeaderMiddleware';
import { MiddlewareRequestOptions } from '../../../services/request/PromiseMiddlewareLayer';

View File

@@ -1,5 +1,5 @@
import { getActiveAccount } from 'app/components/accounts/reducer';
import { Store } from 'app/reducers';
import { Store } from 'app/types';
import { Middleware } from 'app/services/request';
/**

View File

@@ -1,6 +1,6 @@
import { ensureToken, recoverFromTokenError } from 'app/components/accounts/actions';
import { getActiveAccount } from 'app/components/accounts/reducer';
import { Store } from 'app/reducers';
import { Store } from 'app/types';
import { Middleware } from 'app/services/request';
/**