Added first redux toolkit based reducer for domains

This commit is contained in:
Alejandro Celaya
2022-11-02 20:40:14 +01:00
parent 15a9fba091
commit 24483ec330
5 changed files with 144 additions and 36 deletions

View File

@@ -1,3 +1,4 @@
import { IContainer } from 'bottlejs';
import { save, load, RLSOptions } from 'redux-localstorage-simple'; import { save, load, RLSOptions } from 'redux-localstorage-simple';
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import reducer from '../reducers'; import reducer from '../reducers';
@@ -13,9 +14,9 @@ const localStorageConfig: RLSOptions = {
}; };
const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState);
export const store = configureStore({ export const setUpStore = (container: IContainer) => configureStore({
devTools: !isProduction, devTools: !isProduction,
reducer, reducer: reducer(container),
preloadedState, preloadedState,
middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk( middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk(
{ immutableCheck: false, serializableCheck: false }, // State is too big for these { immutableCheck: false, serializableCheck: false }, // State is too big for these

View File

@@ -1,15 +1,17 @@
import { Action, Dispatch } from 'redux'; import { createSlice, PayloadAction, createAsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
import { Dispatch } from 'redux';
import { AxiosError } from 'axios';
import { ShlinkDomainRedirects } from '../../api/types'; import { ShlinkDomainRedirects } from '../../api/types';
import { buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { GetState } from '../../container/types'; import { GetState, ShlinkState } from '../../container/types';
import { parseApiError } from '../../api/utils';
import { ApiErrorAction } from '../../api/types/actions'; import { ApiErrorAction } from '../../api/types/actions';
import { Domain, DomainStatus } from '../data'; import { Domain, DomainStatus } from '../data';
import { hasServerData } from '../../servers/data'; import { hasServerData } from '../../servers/data';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects'; import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
import { ProblemDetailsError } from '../../api/types/errors'; import { ProblemDetailsError } from '../../api/types/errors';
import { parseApiError } from '../../api/utils';
import { buildReducer } from '../../utils/helpers/redux';
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START'; export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR'; export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
@@ -26,19 +28,17 @@ export interface DomainsList {
errorData?: ProblemDetailsError; errorData?: ProblemDetailsError;
} }
export interface ListDomainsAction extends Action<string> { type ListDomainsAction = PayloadAction<{
domains: Domain[]; domains: Domain[];
defaultRedirects?: ShlinkDomainRedirects; defaultRedirects?: ShlinkDomainRedirects;
} }>;
interface FilterDomainsAction extends Action<string> { type FilterDomainsAction = PayloadAction<string>;
searchTerm: string;
}
interface ValidateDomain extends Action<string> { type ValidateDomain = PayloadAction<{
domain: string; domain: string;
status: DomainStatus; status: DomainStatus;
} }>;
const initialState: DomainsList = { const initialState: DomainsList = {
domains: [], domains: [],
@@ -59,27 +59,28 @@ export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomain
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status });
export default buildReducer<DomainsList, DomainsCombinedAction>({ const oldReducer = buildReducer<DomainsList, DomainsCombinedAction>({
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }), [LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
[LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }), [LIST_DOMAINS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
[LIST_DOMAINS]: (_, { domains, defaultRedirects }) => [LIST_DOMAINS]: (_, { payload }) => ({ ...initialState, searchTerm: payload, filteredDomains: payload.domains }),
({ ...initialState, domains, filteredDomains: domains, defaultRedirects }), [FILTER_DOMAINS]: (state, { payload }) => ({
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
...state, ...state,
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm.toLowerCase())), filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())),
}), }),
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({ [EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
...state, ...state,
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
}), }),
[VALIDATE_DOMAIN]: (state, { domain, status }) => ({ [VALIDATE_DOMAIN]: (state, { payload }) => ({
...state, ...state,
domains: state.domains.map(replaceStatusOnDomain(domain, status)), domains: state.domains.map(replaceStatusOnDomain(payload.domain, payload.status)),
filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(domain, status)), filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status)),
}), }),
}, initialState); }, initialState);
export default oldReducer;
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async ( export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
dispatch: Dispatch, dispatch: Dispatch,
getState: GetState, getState: GetState,
@@ -88,18 +89,21 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState); const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
try { try {
const resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({ const payload = await shlinkListDomains().then(({ data, defaultRedirects }) => ({
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })), domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
defaultRedirects, defaultRedirects,
})); }));
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, ...resp }); dispatch<ListDomainsAction>({ type: LIST_DOMAINS, payload });
} catch (e: any) { } catch (e: any) {
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) }); dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
} }
}; };
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm }); export const filterDomains = (searchTerm: string): FilterDomainsAction => ({
type: FILTER_DOMAINS,
payload: searchTerm,
});
export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) => (domain: string) => async ( export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) => (domain: string) => async (
dispatch: Dispatch, dispatch: Dispatch,
@@ -108,7 +112,10 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder)
const { selectedServer } = getState(); const { selectedServer } = getState();
if (!hasServerData(selectedServer)) { if (!hasServerData(selectedServer)) {
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' }); dispatch<ValidateDomain>({
type: VALIDATE_DOMAIN,
payload: { domain, status: 'invalid' },
});
return; return;
} }
@@ -122,8 +129,102 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder)
const { status } = await health(); const { status } = await health();
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: status === 'pass' ? 'valid' : 'invalid' }); dispatch<ValidateDomain>({
type: VALIDATE_DOMAIN,
payload: { domain, status: status === 'pass' ? 'valid' : 'invalid' },
});
} catch (e) { } catch (e) {
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' }); dispatch<ValidateDomain>({
type: VALIDATE_DOMAIN,
payload: { domain, status: 'invalid' },
});
} }
}; };
export const domainsReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const listDomains = createAsyncThunk<{
domains: Domain[];
defaultRedirects?: ShlinkDomainRedirects;
}, void, { state: ShlinkState }>(
LIST_DOMAINS,
async (_, { getState }) => {
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
const { data, defaultRedirects } = await shlinkListDomains();
return {
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
defaultRedirects,
};
},
);
// eslint-disable-next-line @typescript-eslint/no-shadow
const checkDomainHealth = createAsyncThunk<{ domain: string; status: DomainStatus }, string, { state: ShlinkState }>(
VALIDATE_DOMAIN,
async (domain: string, { getState }) => {
const { selectedServer } = getState();
if (!hasServerData(selectedServer)) {
return { domain, status: 'invalid' };
}
try {
const { url, ...rest } = selectedServer;
const { health } = buildShlinkApiClient({
...rest,
url: replaceAuthorityFromUri(url, domain),
});
const { status } = await health();
return { domain, status: status === 'pass' ? 'valid' : 'invalid' };
} catch (e) {
return { domain, status: 'invalid' };
}
},
);
const { actions, reducer } = createSlice<DomainsList, SliceCaseReducers<DomainsList>>({
name: 'domainsList',
initialState,
reducers: {
filterDomains: (state, { payload }) => {
// eslint-disable-next-line no-param-reassign
state.filteredDomains = state.domains.filter(
({ domain }) => domain.toLowerCase().match(payload.toLowerCase()),
);
},
},
extraReducers: (builder) => {
builder.addCase(listDomains.pending, () => ({ ...initialState, loading: true }));
builder.addCase(listDomains.rejected, (_, { error }) => (
{ ...initialState, error: true, errorData: parseApiError(error as AxiosError<ProblemDetailsError>) } // TODO Fix this casting
));
builder.addCase(listDomains.fulfilled, (_, { payload }) => (
{ ...initialState, ...payload, filteredDomains: payload.domains }
));
builder.addCase(checkDomainHealth.fulfilled, (state, { payload }) => {
// eslint-disable-next-line no-param-reassign
state.domains = state.domains.map(replaceStatusOnDomain(payload.domain, payload.status));
// eslint-disable-next-line no-param-reassign
state.filteredDomains = state.filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status));
});
builder.addCase(EDIT_DOMAIN_REDIRECTS, (state, { domain, redirects }: any) => { // TODO Fix this "any"
// eslint-disable-next-line no-param-reassign
state.domains = state.domains.map(replaceRedirectsOnDomain(domain, redirects));
// eslint-disable-next-line no-param-reassign
state.filteredDomains = state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects));
});
},
});
return {
reducer,
listDomains,
checkDomainHealth,
...actions,
};
};

View File

@@ -1,6 +1,7 @@
import { prop } from 'ramda';
import Bottle from 'bottlejs'; import Bottle from 'bottlejs';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
import { checkDomainHealth, filterDomains, listDomains } from '../reducers/domainsList'; import { domainsReducerCreator } from '../reducers/domainsList';
import { DomainSelector } from '../DomainSelector'; import { DomainSelector } from '../DomainSelector';
import { ManageDomains } from '../ManageDomains'; import { ManageDomains } from '../ManageDomains';
import { editDomainRedirects } from '../reducers/domainRedirects'; import { editDomainRedirects } from '../reducers/domainRedirects';
@@ -16,11 +17,15 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'], ['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
)); ));
// Reducer
bottle.serviceFactory('domainsReducerCreator', domainsReducerCreator, 'buildShlinkApiClient');
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here
// Actions // Actions
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient'); bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here
bottle.serviceFactory('filterDomains', () => filterDomains); bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient'); bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
bottle.serviceFactory('checkDomainHealth', checkDomainHealth, 'buildShlinkApiClient'); bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here
}; };
export default provideServices; export default provideServices;

View File

@@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import pack from '../package.json'; import pack from '../package.json';
import { container } from './container'; import { container } from './container';
import { store } from './container/store'; import { setUpStore } from './container/store';
import { fixLeafletIcons } from './utils/helpers/leaflet'; import { fixLeafletIcons } from './utils/helpers/leaflet';
import { register as registerServiceWorker } from './serviceWorkerRegistration'; import { register as registerServiceWorker } from './serviceWorkerRegistration';
import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
@@ -14,6 +14,7 @@ import './index.scss';
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS // This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
fixLeafletIcons(); fixLeafletIcons();
const store = setUpStore(container);
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container; const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;
createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion

View File

@@ -1,3 +1,4 @@
import Bottle from 'bottlejs';
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import serversReducer from '../servers/reducers/servers'; import serversReducer from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer'; import selectedServerReducer from '../servers/reducers/selectedServer';
@@ -16,13 +17,12 @@ import tagDeleteReducer from '../tags/reducers/tagDelete';
import tagEditReducer from '../tags/reducers/tagEdit'; import tagEditReducer from '../tags/reducers/tagEdit';
import mercureInfoReducer from '../mercure/reducers/mercureInfo'; import mercureInfoReducer from '../mercure/reducers/mercureInfo';
import settingsReducer from '../settings/reducers/settings'; import settingsReducer from '../settings/reducers/settings';
import domainsListReducer from '../domains/reducers/domainsList';
import visitsOverviewReducer from '../visits/reducers/visitsOverview'; import visitsOverviewReducer from '../visits/reducers/visitsOverview';
import appUpdatesReducer from '../app/reducers/appUpdates'; import appUpdatesReducer from '../app/reducers/appUpdates';
import sidebarReducer from '../common/reducers/sidebar'; import sidebarReducer from '../common/reducers/sidebar';
import { ShlinkState } from '../container/types'; import { ShlinkState } from '../container/types';
export default combineReducers<ShlinkState>({ export default (container: Bottle.IContainer) => combineReducers<ShlinkState>({
servers: serversReducer, servers: serversReducer,
selectedServer: selectedServerReducer, selectedServer: selectedServerReducer,
shortUrlsList: shortUrlsListReducer, shortUrlsList: shortUrlsListReducer,
@@ -40,7 +40,7 @@ export default combineReducers<ShlinkState>({
tagEdit: tagEditReducer, tagEdit: tagEditReducer,
mercureInfo: mercureInfoReducer, mercureInfo: mercureInfoReducer,
settings: settingsReducer, settings: settingsReducer,
domainsList: domainsListReducer, domainsList: container.domainsListReducer,
visitsOverview: visitsOverviewReducer, visitsOverview: visitsOverviewReducer,
appUpdated: appUpdatesReducer, appUpdated: appUpdatesReducer,
sidebar: sidebarReducer, sidebar: sidebarReducer,