Move App into shell dir. Decouple ContextProviders. Improved type coverage for reducers

This commit is contained in:
SleepWalker 2019-12-09 09:07:07 +02:00
parent 5c70021456
commit 128f327ec4
10 changed files with 107 additions and 73 deletions

View File

@ -1,34 +0,0 @@
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 'app/components/i18n';
import { Store } from 'redux';
import AuthFlowRoute from 'app/containers/AuthFlowRoute';
import RootPage from 'app/pages/root/RootPage';
import SuccessOauthPage from 'app/pages/auth/SuccessOauthPage';
const App = ({
store,
browserHistory,
}: {
store: Store;
browserHistory: any;
}) => (
<ReduxProvider store={store}>
<IntlProvider>
<Router history={browserHistory}>
<Switch>
<Route path="/oauth2/code/success" component={SuccessOauthPage} />
<AuthFlowRoute
path="/oauth2/:version(v\d+)/:clientId?"
component={() => null}
/>
<Route path="/" component={RootPage} />
</Switch>
</Router>
</IntlProvider>
</ReduxProvider>
);
export default hot(App);

View File

@ -1,4 +1,5 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { RootState } from 'app/reducers';
import { import {
ERROR, ERROR,
@ -27,23 +28,11 @@ export interface Client {
description: string; description: string;
} }
export interface State { interface OAuthState {
credentials: Credentials;
error:
| string
| {
type: string;
payload: { [key: string]: any };
};
isLoading: boolean;
isSwitcherEnabled: boolean;
client: Client | null;
login: string;
oauth: {
clientId: string; clientId: string;
redirectUrl: string; redirectUrl: string;
responseType: string; responseType: string;
description: string; description?: string;
scope: string; scope: string;
prompt: string; prompt: string;
loginHint: string; loginHint: string;
@ -52,11 +41,25 @@ export interface State {
code?: string; code?: string;
displayCode?: string; displayCode?: string;
acceptRequired?: boolean; acceptRequired?: boolean;
} | null; }
export interface State {
credentials: Credentials;
error:
| null
| string
| {
type: string;
payload: { [key: string]: any };
};
isLoading: boolean;
isSwitcherEnabled: boolean;
client: Client | null;
oauth: OAuthState | null;
scopes: string[]; scopes: string[];
} }
export default combineReducers({ export default combineReducers<State>({
credentials, credentials,
error, error,
isLoading, isLoading,
@ -66,7 +69,10 @@ export default combineReducers({
scopes, scopes,
}); });
function error(state = null, { type, payload = null, error = false }) { function error(
state = null,
{ type, payload = null, error = false },
): State['error'] {
switch (type) { switch (type) {
case ERROR: case ERROR:
if (!error) { if (!error) {
@ -89,7 +95,7 @@ function credentials(
type: string; type: string;
payload: Credentials | null; payload: Credentials | null;
}, },
) { ): State['credentials'] {
if (type === SET_CREDENTIALS) { if (type === SET_CREDENTIALS) {
if (payload && typeof payload === 'object') { if (payload && typeof payload === 'object') {
return { return {
@ -103,7 +109,10 @@ function credentials(
return state; return state;
} }
function isSwitcherEnabled(state = true, { type, payload = false }) { function isSwitcherEnabled(
state = true,
{ type, payload = false },
): State['isSwitcherEnabled'] {
switch (type) { switch (type) {
case SET_SWITCHER: case SET_SWITCHER:
if (typeof payload !== 'boolean') { if (typeof payload !== 'boolean') {
@ -117,7 +126,10 @@ function isSwitcherEnabled(state = true, { type, payload = false }) {
} }
} }
function isLoading(state = false, { type, payload = null }) { function isLoading(
state = false,
{ type, payload = null },
): State['isLoading'] {
switch (type) { switch (type) {
case SET_LOADING_STATE: case SET_LOADING_STATE:
return !!payload; return !!payload;
@ -127,7 +139,7 @@ function isLoading(state = false, { type, payload = null }) {
} }
} }
function client(state = null, { type, payload }) { function client(state = null, { type, payload }): State['client'] {
switch (type) { switch (type) {
case SET_CLIENT: case SET_CLIENT:
return { return {
@ -141,7 +153,10 @@ function client(state = null, { type, payload }) {
} }
} }
function oauth(state: State | null = null, { type, payload }) { function oauth(
state: State['oauth'] = null,
{ type, payload },
): State['oauth'] {
switch (type) { switch (type) {
case SET_OAUTH: case SET_OAUTH:
return { return {
@ -156,7 +171,7 @@ function oauth(state: State | null = null, { type, payload }) {
case SET_OAUTH_RESULT: case SET_OAUTH_RESULT:
return { return {
...state, ...(state as OAuthState),
success: payload.success, success: payload.success,
code: payload.code, code: payload.code,
displayCode: payload.displayCode, displayCode: payload.displayCode,
@ -164,7 +179,7 @@ function oauth(state: State | null = null, { type, payload }) {
case REQUIRE_PERMISSIONS_ACCEPT: case REQUIRE_PERMISSIONS_ACCEPT:
return { return {
...state, ...(state as OAuthState),
acceptRequired: true, acceptRequired: true,
}; };
@ -173,7 +188,7 @@ function oauth(state: State | null = null, { type, payload }) {
} }
} }
function scopes(state = [], { type, payload = [] }) { function scopes(state = [], { type, payload = [] }): State['scopes'] {
switch (type) { switch (type) {
case SET_SCOPES: case SET_SCOPES:
return payload; return payload;
@ -183,10 +198,10 @@ function scopes(state = [], { type, payload = [] }) {
} }
} }
export function getLogin(state: { [key: string]: any }): string | null { export function getLogin(state: RootState): string | null {
return state.auth.credentials.login || null; return state.auth.credentials.login || null;
} }
export function getCredentials(state: { [key: string]: any }): Credentials { export function getCredentials(state: RootState): Credentials {
return state.auth.credentials; return state.auth.credentials;
} }

View File

@ -1,6 +1,8 @@
import { BSOD } from './actions'; import { BSOD } from './actions';
export default function(state = false, { type }) { export type State = boolean;
export default function(state: State = false, { type }): State {
if (type === BSOD) { if (type === BSOD) {
return true; return true;
} }

View File

@ -14,7 +14,7 @@ import history, { browserHistory } from 'app/services/history';
import i18n from 'app/services/i18n'; import i18n from 'app/services/i18n';
import { loadScript, debounce } from 'app/functions'; import { loadScript, debounce } from 'app/functions';
import App from './App'; import App from './shell';
const win: { [key: string]: any } = window as any; const win: { [key: string]: any } = window as any;
@ -35,7 +35,7 @@ Promise.all([
i18n.ensureIntl(), // ensure, that intl is polyfilled before any rendering i18n.ensureIntl(), // ensure, that intl is polyfilled before any rendering
]).then(() => { ]).then(() => {
ReactDOM.render( ReactDOM.render(
<App store={store} browserHistory={browserHistory} />, <App store={store} history={browserHistory} />,
document.getElementById('app'), document.getElementById('app'),
); );

View File

@ -7,13 +7,14 @@ import accounts, {
} from 'app/components/accounts/reducer'; } from 'app/components/accounts/reducer';
import i18n, { State as I18nState } from 'app/components/i18n/reducer'; import i18n, { State as I18nState } from 'app/components/i18n/reducer';
import popup, { State as PopupState } from 'app/components/ui/popup/reducer'; import popup, { State as PopupState } from 'app/components/ui/popup/reducer';
import bsod from 'app/components/ui/bsod/reducer'; import bsod, { State as BsodState } from 'app/components/ui/bsod/reducer';
import apps, { Apps } from 'app/components/dev/apps/reducer'; import apps, { Apps } from 'app/components/dev/apps/reducer';
import { ThunkDispatch, ThunkAction as ReduxThunkAction } from 'redux-thunk'; import { ThunkDispatch, ThunkAction as ReduxThunkAction } from 'redux-thunk';
import { Store as ReduxStore } from 'redux'; import { Store as ReduxStore } from 'redux';
export interface RootState { export interface RootState {
auth: AuthState; auth: AuthState;
bsod: BsodState;
accounts: AccountsState; accounts: AccountsState;
user: User; user: User;
popup: PopupState; popup: PopupState;
@ -41,7 +42,7 @@ export type Store = ReduxStore<RootState> & {
dispatch: Dispatch; dispatch: Dispatch;
}; };
export default combineReducers({ export default combineReducers<RootState>({
bsod, bsod,
auth, auth,
user, user,

View File

@ -33,7 +33,7 @@ type OauthRequestData = {
client_id: string; client_id: string;
redirect_uri: string; redirect_uri: string;
response_type: string; response_type: string;
description: string; description?: string;
scope: string; scope: string;
prompt: string; prompt: string;
login_hint?: string; login_hint?: string;
@ -44,7 +44,7 @@ export type OauthData = {
clientId: string; clientId: string;
redirectUrl: string; redirectUrl: string;
responseType: string; responseType: string;
description: string; description?: string;
scope: string; scope: string;
prompt: string; // comma separated list of 'none' | 'consent' | 'select_account'; prompt: string; // comma separated list of 'none' | 'consent' | 'select_account';
loginHint?: string; loginHint?: string;

View File

@ -0,0 +1,24 @@
import React from 'react';
import { hot } from 'react-hot-loader/root';
import { Route, Switch } from 'react-router-dom';
import { Store } from 'app/reducers';
import AuthFlowRoute from 'app/containers/AuthFlowRoute';
import RootPage from 'app/pages/root/RootPage';
import SuccessOauthPage from 'app/pages/auth/SuccessOauthPage';
import ContextProvider from './ContextProvider';
const App = ({ store, history }: { store: Store; history: any }) => (
<ContextProvider store={store} history={history}>
<Switch>
<Route path="/oauth2/code/success" component={SuccessOauthPage} />
<AuthFlowRoute
path="/oauth2/:version(v\d+)/:clientId?"
component={() => null}
/>
<Route path="/" component={RootPage} />
</Switch>
</ContextProvider>
);
export default hot(App);

View File

@ -0,0 +1,25 @@
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { Router } from 'react-router-dom';
import { IntlProvider } from 'app/components/i18n';
import { Store } from 'app/reducers';
function ContextProvider({
children,
store,
history,
}: {
children: React.ReactNode;
store: Store;
history: any;
}) {
return (
<ReduxProvider store={store}>
<IntlProvider>
<Router history={history}>{children}</Router>
</IntlProvider>
</ReduxProvider>
);
}
export default ContextProvider;

View File

@ -0,0 +1,2 @@
export { default } from './App';
export { default as ContextProvider } from './ContextProvider';

View File

@ -6,26 +6,25 @@ import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import persistState from 'redux-localstorage'; import persistState from 'redux-localstorage';
import reducers from 'app/reducers'; import reducers, { Store } from 'app/reducers';
export default function storeFactory() { export default function storeFactory(): Store {
const middlewares = applyMiddleware(thunk); const middlewares = applyMiddleware(thunk);
const persistStateEnhancer = persistState(['accounts', 'user'], { const persistStateEnhancer = persistState(['accounts', 'user'], {
key: 'redux-storage', key: 'redux-storage',
}); });
/* global process: false */
let enhancer; let enhancer;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
enhancer = compose(middlewares, persistStateEnhancer); enhancer = compose(middlewares, persistStateEnhancer);
} else { } else {
const composeEnhancers = const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
enhancer = composeEnhancers(middlewares, persistStateEnhancer); enhancer = composeEnhancers(middlewares, persistStateEnhancer);
} }
const store = createStore(reducers, {}, enhancer); const store = createStore(reducers, {}, enhancer) as Store;
// Hot reload reducers // Hot reload reducers
if (module.hot && typeof module.hot.accept === 'function') { if (module.hot && typeof module.hot.accept === 'function') {