From 15a9fba0912ef4d4f6996e5f3549e49e529cf14a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 1 Nov 2022 12:52:27 +0100 Subject: [PATCH 01/12] Migrated redux store creation to redux toolkit --- package-lock.json | 49 +++++++++++++++++++++++++++++++++++++++--- package.json | 1 + src/container/store.ts | 19 ++++++++-------- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index f24f90ed..b0eafdcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@reduxjs/toolkit": "^1.8.6", "axios": "^1.1.2", "bootstrap": "^5.2.2", "bottlejs": "^2.0.1", @@ -4267,6 +4268,29 @@ "react-dom": "^18.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz", + "integrity": "sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==", + "dependencies": { + "immer": "^9.0.7", + "redux": "^4.1.2", + "redux-thunk": "^2.4.1", + "reselect": "^4.1.5" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.1.tgz", @@ -12146,7 +12170,6 @@ "version": "9.0.12", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -22895,6 +22918,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "node_modules/reselect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", + "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -29896,6 +29924,17 @@ "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", "requires": {} }, + "@reduxjs/toolkit": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz", + "integrity": "sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==", + "requires": { + "immer": "^9.0.7", + "redux": "^4.1.2", + "redux-thunk": "^2.4.1", + "reselect": "^4.1.5" + } + }, "@remix-run/router": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.1.tgz", @@ -35856,8 +35895,7 @@ "immer": { "version": "9.0.12", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", - "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==", - "dev": true + "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==" }, "immutable": { "version": "4.0.0", @@ -43789,6 +43827,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "reselect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", + "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" + }, "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/package.json b/package.json index cb026101..549b0d11 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@reduxjs/toolkit": "^1.8.6", "axios": "^1.1.2", "bootstrap": "^5.2.2", "bottlejs": "^2.0.1", diff --git a/src/container/store.ts b/src/container/store.ts index f4c80820..569db995 100644 --- a/src/container/store.ts +++ b/src/container/store.ts @@ -1,14 +1,10 @@ -import ReduxThunk from 'redux-thunk'; -import { applyMiddleware, compose, createStore } from 'redux'; import { save, load, RLSOptions } from 'redux-localstorage-simple'; -import reducers from '../reducers'; +import { configureStore } from '@reduxjs/toolkit'; +import reducer from '../reducers'; import { migrateDeprecatedSettings } from '../settings/helpers'; import { ShlinkState } from './types'; const isProduction = process.env.NODE_ENV === 'production'; -// eslint-disable-next-line no-mixed-operators -const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const localStorageConfig: RLSOptions = { states: ['settings', 'servers'], namespace: 'shlink', @@ -17,6 +13,11 @@ const localStorageConfig: RLSOptions = { }; const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); -export const store = createStore(reducers, preloadedState, composeEnhancers( - applyMiddleware(save(localStorageConfig), ReduxThunk), -)); +export const store = configureStore({ + devTools: !isProduction, + reducer, + preloadedState, + middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk( + { immutableCheck: false, serializableCheck: false }, // State is too big for these + ).concat(save(localStorageConfig)), +}); From 24483ec3301501a4cbd29ad58d7dae727f78c6a8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Nov 2022 20:40:14 +0100 Subject: [PATCH 02/12] Added first redux toolkit based reducer for domains --- src/container/store.ts | 5 +- src/domains/reducers/domainsList.ts | 153 ++++++++++++++++++++---- src/domains/services/provideServices.ts | 13 +- src/index.tsx | 3 +- src/reducers/index.ts | 6 +- 5 files changed, 144 insertions(+), 36 deletions(-) diff --git a/src/container/store.ts b/src/container/store.ts index 569db995..5f214f59 100644 --- a/src/container/store.ts +++ b/src/container/store.ts @@ -1,3 +1,4 @@ +import { IContainer } from 'bottlejs'; import { save, load, RLSOptions } from 'redux-localstorage-simple'; import { configureStore } from '@reduxjs/toolkit'; import reducer from '../reducers'; @@ -13,9 +14,9 @@ const localStorageConfig: RLSOptions = { }; const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState); -export const store = configureStore({ +export const setUpStore = (container: IContainer) => configureStore({ devTools: !isProduction, - reducer, + reducer: reducer(container), preloadedState, middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk( { immutableCheck: false, serializableCheck: false }, // State is too big for these diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index b1f5e061..40e0fcd6 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -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 { buildReducer } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; -import { GetState } from '../../container/types'; -import { parseApiError } from '../../api/utils'; +import { GetState, ShlinkState } from '../../container/types'; import { ApiErrorAction } from '../../api/types/actions'; import { Domain, DomainStatus } from '../data'; import { hasServerData } from '../../servers/data'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects'; 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_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR'; @@ -26,19 +28,17 @@ export interface DomainsList { errorData?: ProblemDetailsError; } -export interface ListDomainsAction extends Action { +type ListDomainsAction = PayloadAction<{ domains: Domain[]; defaultRedirects?: ShlinkDomainRedirects; -} +}>; -interface FilterDomainsAction extends Action { - searchTerm: string; -} +type FilterDomainsAction = PayloadAction; -interface ValidateDomain extends Action { +type ValidateDomain = PayloadAction<{ domain: string; status: DomainStatus; -} +}>; const initialState: DomainsList = { domains: [], @@ -59,27 +59,28 @@ export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomain export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); -export default buildReducer({ +const oldReducer = buildReducer({ [LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }), - [LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }), - [LIST_DOMAINS]: (_, { domains, defaultRedirects }) => - ({ ...initialState, domains, filteredDomains: domains, defaultRedirects }), - [FILTER_DOMAINS]: (state, { searchTerm }) => ({ + [LIST_DOMAINS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), + [LIST_DOMAINS]: (_, { payload }) => ({ ...initialState, searchTerm: payload, filteredDomains: payload.domains }), + [FILTER_DOMAINS]: (state, { payload }) => ({ ...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 }) => ({ ...state, domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), }), - [VALIDATE_DOMAIN]: (state, { domain, status }) => ({ + [VALIDATE_DOMAIN]: (state, { payload }) => ({ ...state, - domains: state.domains.map(replaceStatusOnDomain(domain, status)), - filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(domain, status)), + domains: state.domains.map(replaceStatusOnDomain(payload.domain, payload.status)), + filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status)), }), }, initialState); +export default oldReducer; + export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async ( dispatch: Dispatch, getState: GetState, @@ -88,18 +89,21 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState); try { - const resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({ + const payload = await shlinkListDomains().then(({ data, defaultRedirects }) => ({ domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })), defaultRedirects, })); - dispatch({ type: LIST_DOMAINS, ...resp }); + dispatch({ type: LIST_DOMAINS, payload }); } catch (e: any) { dispatch({ 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 ( dispatch: Dispatch, @@ -108,7 +112,10 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) const { selectedServer } = getState(); if (!hasServerData(selectedServer)) { - dispatch({ type: VALIDATE_DOMAIN, domain, status: 'invalid' }); + dispatch({ + type: VALIDATE_DOMAIN, + payload: { domain, status: 'invalid' }, + }); return; } @@ -122,8 +129,102 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) const { status } = await health(); - dispatch({ type: VALIDATE_DOMAIN, domain, status: status === 'pass' ? 'valid' : 'invalid' }); + dispatch({ + type: VALIDATE_DOMAIN, + payload: { domain, status: status === 'pass' ? 'valid' : 'invalid' }, + }); } catch (e) { - dispatch({ type: VALIDATE_DOMAIN, domain, status: 'invalid' }); + dispatch({ + 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>({ + 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) } // 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, + }; +}; diff --git a/src/domains/services/provideServices.ts b/src/domains/services/provideServices.ts index 9a4ba7fe..aaac9e2c 100644 --- a/src/domains/services/provideServices.ts +++ b/src/domains/services/provideServices.ts @@ -1,6 +1,7 @@ +import { prop } from 'ramda'; import Bottle from 'bottlejs'; import { ConnectDecorator } from '../../container/types'; -import { checkDomainHealth, filterDomains, listDomains } from '../reducers/domainsList'; +import { domainsReducerCreator } from '../reducers/domainsList'; import { DomainSelector } from '../DomainSelector'; import { ManageDomains } from '../ManageDomains'; import { editDomainRedirects } from '../reducers/domainRedirects'; @@ -16,11 +17,15 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { ['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 - bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient'); - bottle.serviceFactory('filterDomains', () => filterDomains); + bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here 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; diff --git a/src/index.tsx b/src/index.tsx index dc13305e..e8d471c2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import pack from '../package.json'; import { container } from './container'; -import { store } from './container/store'; +import { setUpStore } from './container/store'; import { fixLeafletIcons } from './utils/helpers/leaflet'; 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 @@ -14,6 +14,7 @@ import './index.scss'; // This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS fixLeafletIcons(); +const store = setUpStore(container); const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container; createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion diff --git a/src/reducers/index.ts b/src/reducers/index.ts index da523b81..625fadbf 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,3 +1,4 @@ +import Bottle from 'bottlejs'; import { combineReducers } from 'redux'; import serversReducer from '../servers/reducers/servers'; import selectedServerReducer from '../servers/reducers/selectedServer'; @@ -16,13 +17,12 @@ import tagDeleteReducer from '../tags/reducers/tagDelete'; import tagEditReducer from '../tags/reducers/tagEdit'; import mercureInfoReducer from '../mercure/reducers/mercureInfo'; import settingsReducer from '../settings/reducers/settings'; -import domainsListReducer from '../domains/reducers/domainsList'; import visitsOverviewReducer from '../visits/reducers/visitsOverview'; import appUpdatesReducer from '../app/reducers/appUpdates'; import sidebarReducer from '../common/reducers/sidebar'; import { ShlinkState } from '../container/types'; -export default combineReducers({ +export default (container: Bottle.IContainer) => combineReducers({ servers: serversReducer, selectedServer: selectedServerReducer, shortUrlsList: shortUrlsListReducer, @@ -40,7 +40,7 @@ export default combineReducers({ tagEdit: tagEditReducer, mercureInfo: mercureInfoReducer, settings: settingsReducer, - domainsList: domainsListReducer, + domainsList: container.domainsListReducer, visitsOverview: visitsOverviewReducer, appUpdated: appUpdatesReducer, sidebar: sidebarReducer, From 88e8f3363b07aee90b3c7431282409e6f8c3b8a1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 3 Nov 2022 19:52:57 +0100 Subject: [PATCH 03/12] Fixed domainsListReducer test so that it works with new payload prop in actions --- src/domains/reducers/domainsList.ts | 29 ++++++++++++----------- test/domains/reducers/domainsList.test.ts | 28 +++++++++++++++------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index 40e0fcd6..1733384d 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -28,17 +28,21 @@ export interface DomainsList { errorData?: ProblemDetailsError; } -type ListDomainsAction = PayloadAction<{ +interface ListDomains { domains: Domain[]; defaultRedirects?: ShlinkDomainRedirects; -}>; +} + +type ListDomainsAction = PayloadAction; type FilterDomainsAction = PayloadAction; -type ValidateDomain = PayloadAction<{ +interface ValidateDomain { domain: string; status: DomainStatus; -}>; +} + +type ValidateDomainAction = PayloadAction; const initialState: DomainsList = { domains: [], @@ -51,7 +55,7 @@ export type DomainsCombinedAction = ListDomainsAction & ApiErrorAction & FilterDomainsAction & EditDomainRedirectsAction -& ValidateDomain; +& ValidateDomainAction; export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects }); @@ -62,7 +66,7 @@ export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => const oldReducer = buildReducer({ [LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }), [LIST_DOMAINS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), - [LIST_DOMAINS]: (_, { payload }) => ({ ...initialState, searchTerm: payload, filteredDomains: payload.domains }), + [LIST_DOMAINS]: (_, { payload }) => ({ ...initialState, domains: payload.domains, filteredDomains: payload.domains }), [FILTER_DOMAINS]: (state, { payload }) => ({ ...state, filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())), @@ -112,7 +116,7 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) const { selectedServer } = getState(); if (!hasServerData(selectedServer)) { - dispatch({ + dispatch({ type: VALIDATE_DOMAIN, payload: { domain, status: 'invalid' }, }); @@ -129,12 +133,12 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) const { status } = await health(); - dispatch({ + dispatch({ type: VALIDATE_DOMAIN, payload: { domain, status: status === 'pass' ? 'valid' : 'invalid' }, }); } catch (e) { - dispatch({ + dispatch({ type: VALIDATE_DOMAIN, payload: { domain, status: 'invalid' }, }); @@ -143,10 +147,7 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) export const domainsReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { // eslint-disable-next-line @typescript-eslint/no-shadow - const listDomains = createAsyncThunk<{ - domains: Domain[]; - defaultRedirects?: ShlinkDomainRedirects; - }, void, { state: ShlinkState }>( + const listDomains = createAsyncThunk( LIST_DOMAINS, async (_, { getState }) => { const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState); @@ -160,7 +161,7 @@ export const domainsReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuild ); // eslint-disable-next-line @typescript-eslint/no-shadow - const checkDomainHealth = createAsyncThunk<{ domain: string; status: DomainStatus }, string, { state: ShlinkState }>( + const checkDomainHealth = createAsyncThunk( VALIDATE_DOMAIN, async (domain: string, { getState }) => { const { selectedServer } = getState(); diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index b00704bd..6d406e5e 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -52,13 +52,13 @@ describe('domainsListReducer', () => { }); it('returns domains on LIST_DOMAINS', () => { - expect(reducer(undefined, action(LIST_DOMAINS, { domains }))).toEqual( + expect(reducer(undefined, action(LIST_DOMAINS, { payload: { domains } } as any))).toEqual( { domains, filteredDomains: domains, loading: false, error: false }, ); }); it('filters domains on FILTER_DOMAINS', () => { - expect(reducer(Mock.of({ domains }), action(FILTER_DOMAINS, { searchTerm: 'oO' }))).toEqual( + expect(reducer(Mock.of({ domains }), action(FILTER_DOMAINS, { payload: 'oO' as any }))).toEqual( { domains, filteredDomains }, ); }); @@ -90,7 +90,7 @@ describe('domainsListReducer', () => { ])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => { expect(reducer( Mock.of({ domains, filteredDomains }), - action(VALIDATE_DOMAIN, { domain, status: 'valid' }), + action(VALIDATE_DOMAIN, { payload: { domain, status: 'valid' } } as any), )).toEqual({ domains: domains.map(replaceStatusOnDomain(domain, 'valid')), filteredDomains: filteredDomains.map(replaceStatusOnDomain(domain, 'valid')), @@ -117,7 +117,10 @@ describe('domainsListReducer', () => { expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_DOMAINS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_DOMAINS, domains, defaultRedirects: undefined }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: LIST_DOMAINS, + payload: { domains }, + }); expect(listDomains).toHaveBeenCalledTimes(1); }); }); @@ -128,7 +131,7 @@ describe('domainsListReducer', () => { ['bar'], ['something'], ])('creates action as expected', (searchTerm) => { - expect(filterDomainsAction(searchTerm)).toEqual({ type: FILTER_DOMAINS, searchTerm }); + expect(filterDomainsAction(searchTerm)).toEqual({ type: FILTER_DOMAINS, payload: searchTerm }); }); }); @@ -145,7 +148,10 @@ describe('domainsListReducer', () => { expect(getState).toHaveBeenCalledTimes(1); expect(health).not.toHaveBeenCalled(); expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: 'invalid' }); + expect(dispatch).toHaveBeenCalledWith({ + type: VALIDATE_DOMAIN, + payload: { domain, status: 'invalid' }, + }); }); it('dispatches invalid status when health endpoint returns an error', async () => { @@ -162,7 +168,10 @@ describe('domainsListReducer', () => { expect(getState).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: 'invalid' }); + expect(dispatch).toHaveBeenCalledWith({ + type: VALIDATE_DOMAIN, + payload: { domain, status: 'invalid' }, + }); }); it.each([ @@ -185,7 +194,10 @@ describe('domainsListReducer', () => { expect(getState).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: expectedStatus }); + expect(dispatch).toHaveBeenCalledWith({ + type: VALIDATE_DOMAIN, + payload: { domain, status: expectedStatus }, + }); }); }); }); From d25dbd5ae6703dc72e2c2d9b04ffe24455ba95de Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 3 Nov 2022 20:15:28 +0100 Subject: [PATCH 04/12] Replaced domainsList old reducer with new reducer in test --- src/domains/reducers/domainsList.ts | 26 +------------- src/domains/services/provideServices.ts | 12 +++---- test/domains/reducers/domainsList.test.ts | 42 +++++++++++++++-------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index 1733384d..4e0a88c5 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -11,7 +11,6 @@ import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects'; 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_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR'; @@ -52,7 +51,6 @@ const initialState: DomainsList = { }; export type DomainsCombinedAction = ListDomainsAction -& ApiErrorAction & FilterDomainsAction & EditDomainRedirectsAction & ValidateDomainAction; @@ -63,28 +61,6 @@ export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomain export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); -const oldReducer = buildReducer({ - [LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }), - [LIST_DOMAINS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), - [LIST_DOMAINS]: (_, { payload }) => ({ ...initialState, domains: payload.domains, filteredDomains: payload.domains }), - [FILTER_DOMAINS]: (state, { payload }) => ({ - ...state, - filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())), - }), - [EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({ - ...state, - domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), - filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), - }), - [VALIDATE_DOMAIN]: (state, { payload }) => ({ - ...state, - domains: state.domains.map(replaceStatusOnDomain(payload.domain, payload.status)), - filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status)), - }), -}, initialState); - -export default oldReducer; - export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async ( dispatch: Dispatch, getState: GetState, @@ -145,7 +121,7 @@ export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) } }; -export const domainsReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { +export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { // eslint-disable-next-line @typescript-eslint/no-shadow const listDomains = createAsyncThunk( LIST_DOMAINS, diff --git a/src/domains/services/provideServices.ts b/src/domains/services/provideServices.ts index aaac9e2c..f03a9c30 100644 --- a/src/domains/services/provideServices.ts +++ b/src/domains/services/provideServices.ts @@ -1,7 +1,7 @@ import { prop } from 'ramda'; import Bottle from 'bottlejs'; import { ConnectDecorator } from '../../container/types'; -import { domainsReducerCreator } from '../reducers/domainsList'; +import { domainsListReducerCreator } from '../reducers/domainsList'; import { DomainSelector } from '../DomainSelector'; import { ManageDomains } from '../ManageDomains'; import { editDomainRedirects } from '../reducers/domainRedirects'; @@ -18,14 +18,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { )); // Reducer - bottle.serviceFactory('domainsReducerCreator', domainsReducerCreator, 'buildShlinkApiClient'); - bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('domainsListReducerCreator', domainsListReducerCreator, 'buildShlinkApiClient'); + bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here // Actions - bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here - bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient'); - bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here }; export default provideServices; diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index 6d406e5e..7bdf2034 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -1,5 +1,6 @@ import { Mock } from 'ts-mockery'; -import reducer, { +import { AxiosError } from 'axios'; +import { LIST_DOMAINS, LIST_DOMAINS_ERROR, LIST_DOMAINS_START, @@ -10,8 +11,9 @@ import reducer, { listDomains as listDomainsAction, filterDomains as filterDomainsAction, replaceRedirectsOnDomain, - checkDomainHealth, + checkDomainHealth as validateDomain, replaceStatusOnDomain, + domainsListReducerCreator, } from '../../../src/domains/reducers/domainsList'; import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedirects'; import { ShlinkDomainRedirects } from '../../../src/api/types'; @@ -19,6 +21,7 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { Domain } from '../../../src/domains/data'; import { ShlinkState } from '../../../src/container/types'; import { SelectedServer, ServerData } from '../../../src/servers/data'; +import { parseApiError } from '../../../src/api/utils'; describe('domainsListReducer', () => { const dispatch = jest.fn(); @@ -31,6 +34,15 @@ describe('domainsListReducer', () => { Mock.of({ domain: 'Boo', status: 'validating' }), ]; const domains = [...filteredDomains, Mock.of({ domain: 'bar', status: 'validating' })]; + const error = Mock.of({ + response: { + data: { type: 'NOT_FOUND', status: 404 }, + }, + }); + // @ts-expect-error gfreg + const { reducer, listDomains: listDomainsThunk, filterDomains, checkDomainHealth } = domainsListReducerCreator( + buildShlinkApiClient, + ); beforeEach(jest.clearAllMocks); @@ -40,27 +52,27 @@ describe('domainsListReducer', () => { ); it('returns loading on LIST_DOMAINS_START', () => { - expect(reducer(undefined, action(LIST_DOMAINS_START))).toEqual( + expect(reducer(undefined, action(listDomainsThunk.pending.toString()))).toEqual( { domains: [], filteredDomains: [], loading: true, error: false }, ); }); it('returns error on LIST_DOMAINS_ERROR', () => { - expect(reducer(undefined, action(LIST_DOMAINS_ERROR))).toEqual( - { domains: [], filteredDomains: [], loading: false, error: true }, + expect(reducer(undefined, action(listDomainsThunk.rejected.toString(), { error } as any))).toEqual( + { domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error as any) }, ); }); it('returns domains on LIST_DOMAINS', () => { - expect(reducer(undefined, action(LIST_DOMAINS, { payload: { domains } } as any))).toEqual( - { domains, filteredDomains: domains, loading: false, error: false }, - ); + expect( + reducer(undefined, action(listDomainsThunk.fulfilled.toString(), { payload: { domains } } as any)), + ).toEqual({ domains, filteredDomains: domains, loading: false, error: false }); }); it('filters domains on FILTER_DOMAINS', () => { - expect(reducer(Mock.of({ domains }), action(FILTER_DOMAINS, { payload: 'oO' as any }))).toEqual( - { domains, filteredDomains }, - ); + expect( + reducer(Mock.of({ domains }), action(filterDomains.toString(), { payload: 'oO' as any })), + ).toEqual({ domains, filteredDomains }); }); it.each([ @@ -90,7 +102,7 @@ describe('domainsListReducer', () => { ])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => { expect(reducer( Mock.of({ domains, filteredDomains }), - action(VALIDATE_DOMAIN, { payload: { domain, status: 'valid' } } as any), + action(checkDomainHealth.fulfilled.toString(), { payload: { domain, status: 'valid' } } as any), )).toEqual({ domains: domains.map(replaceStatusOnDomain(domain, 'valid')), filteredDomains: filteredDomains.map(replaceStatusOnDomain(domain, 'valid')), @@ -143,7 +155,7 @@ describe('domainsListReducer', () => { selectedServer: Mock.all(), })); - await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState); + await validateDomain(buildShlinkApiClient)(domain)(dispatch, getState); expect(getState).toHaveBeenCalledTimes(1); expect(health).not.toHaveBeenCalled(); @@ -163,7 +175,7 @@ describe('domainsListReducer', () => { })); health.mockRejectedValue({}); - await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState); + await validateDomain(buildShlinkApiClient)(domain)(dispatch, getState); expect(getState).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1); @@ -189,7 +201,7 @@ describe('domainsListReducer', () => { })); health.mockResolvedValue({ status: healthStatus }); - await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState); + await validateDomain(buildShlinkApiClient)(domain)(dispatch, getState); expect(getState).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1); From da97b7656346c9cf7776329cefac5098f6b79dbc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 3 Nov 2022 20:29:10 +0100 Subject: [PATCH 05/12] Migrated rest of domainslistreducer-related elements on test to the new ones --- src/domains/reducers/domainsList.ts | 67 +-------------------- test/domains/reducers/domainsList.test.ts | 71 +++++++++++------------ 2 files changed, 35 insertions(+), 103 deletions(-) diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index 4e0a88c5..b6da573c 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -1,10 +1,8 @@ import { createSlice, PayloadAction, createAsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit'; -import { Dispatch } from 'redux'; import { AxiosError } from 'axios'; import { ShlinkDomainRedirects } from '../../api/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; -import { GetState, ShlinkState } from '../../container/types'; -import { ApiErrorAction } from '../../api/types/actions'; +import { ShlinkState } from '../../container/types'; import { Domain, DomainStatus } from '../data'; import { hasServerData } from '../../servers/data'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; @@ -12,10 +10,7 @@ import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedire import { ProblemDetailsError } from '../../api/types/errors'; import { parseApiError } from '../../api/utils'; -export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START'; -export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR'; export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS'; -export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN'; export interface DomainsList { @@ -61,66 +56,6 @@ export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomain export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); -export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async ( - dispatch: Dispatch, - getState: GetState, -) => { - dispatch({ type: LIST_DOMAINS_START }); - const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState); - - try { - const payload = await shlinkListDomains().then(({ data, defaultRedirects }) => ({ - domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })), - defaultRedirects, - })); - - dispatch({ type: LIST_DOMAINS, payload }); - } catch (e: any) { - dispatch({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) }); - } -}; - -export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ - type: FILTER_DOMAINS, - payload: searchTerm, -}); - -export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) => (domain: string) => async ( - dispatch: Dispatch, - getState: GetState, -) => { - const { selectedServer } = getState(); - - if (!hasServerData(selectedServer)) { - dispatch({ - type: VALIDATE_DOMAIN, - payload: { domain, status: 'invalid' }, - }); - - return; - } - - try { - const { url, ...rest } = selectedServer; - const { health } = buildShlinkApiClient({ - ...rest, - url: replaceAuthorityFromUri(url, domain), - }); - - const { status } = await health(); - - dispatch({ - type: VALIDATE_DOMAIN, - payload: { domain, status: status === 'pass' ? 'valid' : 'invalid' }, - }); - } catch (e) { - dispatch({ - type: VALIDATE_DOMAIN, - payload: { domain, status: 'invalid' }, - }); - } -}; - export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { // eslint-disable-next-line @typescript-eslint/no-shadow const listDomains = createAsyncThunk( diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index 7bdf2034..f6640dd8 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -1,17 +1,9 @@ import { Mock } from 'ts-mockery'; import { AxiosError } from 'axios'; import { - LIST_DOMAINS, - LIST_DOMAINS_ERROR, - LIST_DOMAINS_START, - FILTER_DOMAINS, - VALIDATE_DOMAIN, DomainsCombinedAction, DomainsList, - listDomains as listDomainsAction, - filterDomains as filterDomainsAction, replaceRedirectsOnDomain, - checkDomainHealth as validateDomain, replaceStatusOnDomain, domainsListReducerCreator, } from '../../../src/domains/reducers/domainsList'; @@ -39,8 +31,8 @@ describe('domainsListReducer', () => { data: { type: 'NOT_FOUND', status: 404 }, }, }); - // @ts-expect-error gfreg - const { reducer, listDomains: listDomainsThunk, filterDomains, checkDomainHealth } = domainsListReducerCreator( + // @ts-expect-error filterDomains is actually part of the result + const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator( buildShlinkApiClient, ); @@ -52,20 +44,20 @@ describe('domainsListReducer', () => { ); it('returns loading on LIST_DOMAINS_START', () => { - expect(reducer(undefined, action(listDomainsThunk.pending.toString()))).toEqual( + expect(reducer(undefined, action(listDomainsAction.pending.toString()))).toEqual( { domains: [], filteredDomains: [], loading: true, error: false }, ); }); it('returns error on LIST_DOMAINS_ERROR', () => { - expect(reducer(undefined, action(listDomainsThunk.rejected.toString(), { error } as any))).toEqual( + expect(reducer(undefined, action(listDomainsAction.rejected.toString(), { error } as any))).toEqual( { domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error as any) }, ); }); it('returns domains on LIST_DOMAINS', () => { expect( - reducer(undefined, action(listDomainsThunk.fulfilled.toString(), { payload: { domains } } as any)), + reducer(undefined, action(listDomainsAction.fulfilled.toString(), { payload: { domains } } as any)), ).toEqual({ domains, filteredDomains: domains, loading: false, error: false }); }); @@ -114,25 +106,31 @@ describe('domainsListReducer', () => { it('dispatches error when loading domains fails', async () => { listDomains.mockRejectedValue(new Error('error')); - await listDomainsAction(buildShlinkApiClient)()(dispatch, getState); + await listDomainsAction()(dispatch, getState, {}); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_DOMAINS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_DOMAINS_ERROR }); + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ + type: listDomainsAction.pending.toString(), + })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ + type: listDomainsAction.rejected.toString(), + })); expect(listDomains).toHaveBeenCalledTimes(1); }); it('dispatches domains once loaded', async () => { listDomains.mockResolvedValue({ data: domains }); - await listDomainsAction(buildShlinkApiClient)()(dispatch, getState); + await listDomainsAction()(dispatch, getState, {}); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_DOMAINS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { - type: LIST_DOMAINS, + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ + type: listDomainsAction.pending.toString(), + })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ + type: listDomainsAction.fulfilled.toString(), payload: { domains }, - }); + })); expect(listDomains).toHaveBeenCalledTimes(1); }); }); @@ -143,7 +141,9 @@ describe('domainsListReducer', () => { ['bar'], ['something'], ])('creates action as expected', (searchTerm) => { - expect(filterDomainsAction(searchTerm)).toEqual({ type: FILTER_DOMAINS, payload: searchTerm }); + expect(filterDomains(searchTerm)).toEqual( + expect.objectContaining({ type: filterDomains.toString(), payload: searchTerm }), + ); }); }); @@ -155,15 +155,14 @@ describe('domainsListReducer', () => { selectedServer: Mock.all(), })); - await validateDomain(buildShlinkApiClient)(domain)(dispatch, getState); + await checkDomainHealth(domain)(dispatch, getState, {}); expect(getState).toHaveBeenCalledTimes(1); expect(health).not.toHaveBeenCalled(); - expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith({ - type: VALIDATE_DOMAIN, + expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ + type: checkDomainHealth.fulfilled.toString(), payload: { domain, status: 'invalid' }, - }); + })); }); it('dispatches invalid status when health endpoint returns an error', async () => { @@ -175,15 +174,14 @@ describe('domainsListReducer', () => { })); health.mockRejectedValue({}); - await validateDomain(buildShlinkApiClient)(domain)(dispatch, getState); + await checkDomainHealth(domain)(dispatch, getState, {}); expect(getState).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith({ - type: VALIDATE_DOMAIN, + expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ + type: checkDomainHealth.fulfilled.toString(), payload: { domain, status: 'invalid' }, - }); + })); }); it.each([ @@ -201,15 +199,14 @@ describe('domainsListReducer', () => { })); health.mockResolvedValue({ status: healthStatus }); - await validateDomain(buildShlinkApiClient)(domain)(dispatch, getState); + await checkDomainHealth(domain)(dispatch, getState, {}); expect(getState).toHaveBeenCalledTimes(1); expect(health).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith({ - type: VALIDATE_DOMAIN, + expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ + type: checkDomainHealth.fulfilled.toString(), payload: { domain, status: expectedStatus }, - }); + })); }); }); }); From 18d478e16eba2745e88b5e05cc6c104e7951db55 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 3 Nov 2022 20:51:20 +0100 Subject: [PATCH 06/12] Removed unneeded type castings and eslint suppressions in domainsList reducer --- src/api/utils/index.ts | 6 +++- src/domains/reducers/domainsList.ts | 53 ++++++++++++----------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/api/utils/index.ts b/src/api/utils/index.ts index 2156a410..64b7c0a1 100644 --- a/src/api/utils/index.ts +++ b/src/api/utils/index.ts @@ -8,7 +8,11 @@ import { RegularNotFound, } from '../types/errors'; -export const parseApiError = (e: AxiosError) => e.response?.data; +const isAxiosError = (e: unknown): e is AxiosError => !!e && typeof e === 'object' && 'response' in e; + +export const parseApiError = (e: unknown): ProblemDetailsError | undefined => ( + isAxiosError(e) ? e.response?.data : undefined +); export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError => error?.type === ErrorTypeV2.INVALID_ARGUMENT || error?.type === ErrorTypeV3.INVALID_ARGUMENT; diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index b6da573c..75717ddf 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -1,5 +1,4 @@ import { createSlice, PayloadAction, createAsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit'; -import { AxiosError } from 'axios'; import { ShlinkDomainRedirects } from '../../api/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkState } from '../../container/types'; @@ -27,17 +26,20 @@ interface ListDomains { defaultRedirects?: ShlinkDomainRedirects; } -type ListDomainsAction = PayloadAction; - -type FilterDomainsAction = PayloadAction; - interface ValidateDomain { domain: string; status: DomainStatus; } +type ListDomainsAction = PayloadAction; +type FilterDomainsAction = PayloadAction; type ValidateDomainAction = PayloadAction; +export type DomainsCombinedAction = ListDomainsAction +& FilterDomainsAction +& EditDomainRedirectsAction +& ValidateDomainAction; + const initialState: DomainsList = { domains: [], filteredDomains: [], @@ -45,11 +47,6 @@ const initialState: DomainsList = { error: false, }; -export type DomainsCombinedAction = ListDomainsAction -& FilterDomainsAction -& EditDomainRedirectsAction -& ValidateDomainAction; - export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects }); @@ -57,7 +54,6 @@ export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { - // eslint-disable-next-line @typescript-eslint/no-shadow const listDomains = createAsyncThunk( LIST_DOMAINS, async (_, { getState }) => { @@ -71,7 +67,6 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB }, ); - // eslint-disable-next-line @typescript-eslint/no-shadow const checkDomainHealth = createAsyncThunk( VALIDATE_DOMAIN, async (domain: string, { getState }) => { @@ -101,35 +96,31 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB 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()), - ); - }, + filterDomains: (state, { payload }) => ({ + ...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) } // TODO Fix this casting + { ...initialState, error: true, errorData: parseApiError(error) } )); 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(checkDomainHealth.fulfilled, ({ domains, filteredDomains, ...rest }, { payload }) => ({ + ...rest, + domains: domains.map(replaceStatusOnDomain(payload.domain, payload.status)), + filteredDomains: 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)); - }); + builder.addCase(EDIT_DOMAIN_REDIRECTS, (state, { domain, redirects }: any) => ({ // TODO Fix this "any" + ...state, + domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), + filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), + })); }, }); From 79645099ba157fca6efb61ca0da47ed08e711f34 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 3 Nov 2022 20:53:59 +0100 Subject: [PATCH 07/12] Added explicit import --- src/reducers/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 625fadbf..2d3e41ee 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,4 +1,4 @@ -import Bottle from 'bottlejs'; +import { IContainer } from 'bottlejs'; import { combineReducers } from 'redux'; import serversReducer from '../servers/reducers/servers'; import selectedServerReducer from '../servers/reducers/selectedServer'; @@ -22,7 +22,7 @@ import appUpdatesReducer from '../app/reducers/appUpdates'; import sidebarReducer from '../common/reducers/sidebar'; import { ShlinkState } from '../container/types'; -export default (container: Bottle.IContainer) => combineReducers({ +export default (container: IContainer) => combineReducers({ servers: serversReducer, selectedServer: selectedServerReducer, shortUrlsList: shortUrlsListReducer, From 4fa6ae493d15b3b818335869e791ae62292b3d1f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 4 Nov 2022 16:50:03 +0100 Subject: [PATCH 08/12] Removed unnecesary type castings and improved type inference for actions in demainsListReducer --- src/domains/reducers/domainsList.ts | 32 +++++++++-------------- test/domains/reducers/domainsList.test.ts | 23 +++++++--------- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index 75717ddf..de1df166 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -1,15 +1,16 @@ -import { createSlice, PayloadAction, createAsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit'; +import { createSlice, createAsyncThunk, createAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { ShlinkDomainRedirects } from '../../api/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkState } from '../../container/types'; import { Domain, DomainStatus } from '../data'; import { hasServerData } from '../../servers/data'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; -import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects'; +import { EDIT_DOMAIN_REDIRECTS } from './domainRedirects'; import { ProblemDetailsError } from '../../api/types/errors'; import { parseApiError } from '../../api/utils'; export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS'; +export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN'; export interface DomainsList { @@ -31,15 +32,6 @@ interface ValidateDomain { status: DomainStatus; } -type ListDomainsAction = PayloadAction; -type FilterDomainsAction = PayloadAction; -type ValidateDomainAction = PayloadAction; - -export type DomainsCombinedAction = ListDomainsAction -& FilterDomainsAction -& EditDomainRedirectsAction -& ValidateDomainAction; - const initialState: DomainsList = { domains: [], filteredDomains: [], @@ -92,15 +84,12 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB }, ); - const { actions, reducer } = createSlice>({ + const filterDomains = createAction(FILTER_DOMAINS); + + const { reducer } = createSlice>({ name: 'domainsList', initialState, - reducers: { - filterDomains: (state, { payload }) => ({ - ...state, - filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())), - }), - }, + reducers: {}, extraReducers: (builder) => { builder.addCase(listDomains.pending, () => ({ ...initialState, loading: true })); builder.addCase(listDomains.rejected, (_, { error }) => ( @@ -116,6 +105,11 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB filteredDomains: filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status)), })); + builder.addCase(filterDomains, (state, { payload }) => ({ + ...state, + filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())), + })); + builder.addCase(EDIT_DOMAIN_REDIRECTS, (state, { domain, redirects }: any) => ({ // TODO Fix this "any" ...state, domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), @@ -128,6 +122,6 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB reducer, listDomains, checkDomainHealth, - ...actions, + filterDomains, }; }; diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index f6640dd8..dac4a87d 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -1,7 +1,6 @@ import { Mock } from 'ts-mockery'; import { AxiosError } from 'axios'; import { - DomainsCombinedAction, DomainsList, replaceRedirectsOnDomain, replaceStatusOnDomain, @@ -31,7 +30,6 @@ describe('domainsListReducer', () => { data: { type: 'NOT_FOUND', status: 404 }, }, }); - // @ts-expect-error filterDomains is actually part of the result const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator( buildShlinkApiClient, ); @@ -39,31 +37,27 @@ describe('domainsListReducer', () => { beforeEach(jest.clearAllMocks); describe('reducer', () => { - const action = (type: string, args: Partial = {}) => Mock.of( - { type, ...args }, - ); - it('returns loading on LIST_DOMAINS_START', () => { - expect(reducer(undefined, action(listDomainsAction.pending.toString()))).toEqual( + expect(reducer(undefined, { type: listDomainsAction.pending.toString() })).toEqual( { domains: [], filteredDomains: [], loading: true, error: false }, ); }); it('returns error on LIST_DOMAINS_ERROR', () => { - expect(reducer(undefined, action(listDomainsAction.rejected.toString(), { error } as any))).toEqual( - { domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error as any) }, + expect(reducer(undefined, { type: listDomainsAction.rejected.toString(), error })).toEqual( + { domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error) }, ); }); it('returns domains on LIST_DOMAINS', () => { expect( - reducer(undefined, action(listDomainsAction.fulfilled.toString(), { payload: { domains } } as any)), + reducer(undefined, { type: listDomainsAction.fulfilled.toString(), payload: { domains } }), ).toEqual({ domains, filteredDomains: domains, loading: false, error: false }); }); it('filters domains on FILTER_DOMAINS', () => { expect( - reducer(Mock.of({ domains }), action(filterDomains.toString(), { payload: 'oO' as any })), + reducer(Mock.of({ domains }), { type: filterDomains.toString(), payload: 'oO' }), ).toEqual({ domains, filteredDomains }); }); @@ -80,7 +74,7 @@ describe('domainsListReducer', () => { expect(reducer( Mock.of({ domains, filteredDomains }), - action(EDIT_DOMAIN_REDIRECTS, { domain, redirects }), + { type: EDIT_DOMAIN_REDIRECTS, domain, redirects }, )).toEqual({ domains: domains.map(replaceRedirectsOnDomain(domain, redirects)), filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), @@ -94,7 +88,10 @@ describe('domainsListReducer', () => { ])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => { expect(reducer( Mock.of({ domains, filteredDomains }), - action(checkDomainHealth.fulfilled.toString(), { payload: { domain, status: 'valid' } } as any), + { + type: checkDomainHealth.fulfilled.toString(), + payload: { domain, status: 'valid' }, + }, )).toEqual({ domains: domains.map(replaceStatusOnDomain(domain, 'valid')), filteredDomains: filteredDomains.map(replaceStatusOnDomain(domain, 'valid')), From b6d08e2203377f443776ae249d18a3c2dd13452c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 4 Nov 2022 17:10:02 +0100 Subject: [PATCH 09/12] Updated editDomainRedirects action, to expect a payload DTO instead of multiple args --- src/domains/DomainRow.tsx | 3 +- src/domains/ManageDomains.tsx | 4 +- src/domains/helpers/DomainDropdown.tsx | 4 +- .../helpers/EditDomainRedirectsModal.tsx | 16 ++++--- src/domains/reducers/domainRedirects.ts | 12 ++--- test/domains/helpers/DomainDropdown.test.tsx | 2 +- .../helpers/EditDomainRedirectsModal.test.tsx | 44 ++++++++++++------- test/domains/reducers/domainRedirects.test.ts | 12 +++-- 8 files changed, 61 insertions(+), 36 deletions(-) diff --git a/src/domains/DomainRow.tsx b/src/domains/DomainRow.tsx index f3ca7426..e57b0f36 100644 --- a/src/domains/DomainRow.tsx +++ b/src/domains/DomainRow.tsx @@ -8,11 +8,12 @@ import { SelectedServer } from '../servers/data'; import { Domain } from './data'; import { DomainStatusIcon } from './helpers/DomainStatusIcon'; import { DomainDropdown } from './helpers/DomainDropdown'; +import { EditDomainRedirects } from './reducers/domainRedirects'; interface DomainRowProps { domain: Domain; defaultRedirects?: ShlinkDomainRedirects; - editDomainRedirects: (domain: string, redirects: Partial) => Promise; + editDomainRedirects: (redirects: EditDomainRedirects) => Promise; checkDomainHealth: (domain: string) => void; selectedServer: SelectedServer; } diff --git a/src/domains/ManageDomains.tsx b/src/domains/ManageDomains.tsx index a6170595..f7c52992 100644 --- a/src/domains/ManageDomains.tsx +++ b/src/domains/ManageDomains.tsx @@ -4,7 +4,7 @@ import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; import { SimpleCard } from '../utils/SimpleCard'; import { SearchField } from '../utils/SearchField'; -import { ShlinkDomainRedirects } from '../api/types'; +import { EditDomainRedirects } from './reducers/domainRedirects'; import { SelectedServer } from '../servers/data'; import { DomainsList } from './reducers/domainsList'; import { DomainRow } from './DomainRow'; @@ -12,7 +12,7 @@ import { DomainRow } from './DomainRow'; interface ManageDomainsProps { listDomains: Function; filterDomains: (searchTerm: string) => void; - editDomainRedirects: (domain: string, redirects: Partial) => Promise; + editDomainRedirects: (redirects: EditDomainRedirects) => Promise; checkDomainHealth: (domain: string) => void; domainsList: DomainsList; selectedServer: SelectedServer; diff --git a/src/domains/helpers/DomainDropdown.tsx b/src/domains/helpers/DomainDropdown.tsx index 423c4db5..a91b40a8 100644 --- a/src/domains/helpers/DomainDropdown.tsx +++ b/src/domains/helpers/DomainDropdown.tsx @@ -7,14 +7,14 @@ import { useToggle } from '../../utils/helpers/hooks'; import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu'; import { EditDomainRedirectsModal } from './EditDomainRedirectsModal'; import { Domain } from '../data'; -import { ShlinkDomainRedirects } from '../../api/types'; +import { EditDomainRedirects } from '../reducers/domainRedirects'; import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features'; import { getServerId, SelectedServer } from '../../servers/data'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; interface DomainDropdownProps { domain: Domain; - editDomainRedirects: (domain: string, redirects: Partial) => Promise; + editDomainRedirects: (redirects: EditDomainRedirects) => Promise; selectedServer: SelectedServer; } diff --git a/src/domains/helpers/EditDomainRedirectsModal.tsx b/src/domains/helpers/EditDomainRedirectsModal.tsx index 0bc86bf8..26d11be5 100644 --- a/src/domains/helpers/EditDomainRedirectsModal.tsx +++ b/src/domains/helpers/EditDomainRedirectsModal.tsx @@ -1,15 +1,16 @@ import { FC, useState } from 'react'; import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; -import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types'; +import { ShlinkDomain } from '../../api/types'; import { InputFormGroup, InputFormGroupProps } from '../../utils/forms/InputFormGroup'; import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils'; import { InfoTooltip } from '../../utils/InfoTooltip'; +import { EditDomainRedirects } from '../reducers/domainRedirects'; interface EditDomainRedirectsModalProps { domain: ShlinkDomain; isOpen: boolean; toggle: () => void; - editDomainRedirects: (domain: string, redirects: Partial) => Promise; + editDomainRedirects: (redirects: EditDomainRedirects) => Promise; } const FormGroup: FC = ({ isLast, ...rest }) => ( @@ -30,10 +31,13 @@ export const EditDomainRedirectsModal: FC = ( const [invalidShortUrlRedirect, setInvalidShortUrlRedirect] = useState( domain.redirects?.invalidShortUrlRedirect ?? '', ); - const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, { - baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect), - regular404Redirect: nonEmptyValueOrNull(regular404Redirect), - invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect), + const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects({ + domain: domain.domain, + redirects: { + baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect), + regular404Redirect: nonEmptyValueOrNull(regular404Redirect), + invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect), + }, }).then(toggle)); return ( diff --git a/src/domains/reducers/domainRedirects.ts b/src/domains/reducers/domainRedirects.ts index 8cb65d93..4a5472b9 100644 --- a/src/domains/reducers/domainRedirects.ts +++ b/src/domains/reducers/domainRedirects.ts @@ -9,15 +9,17 @@ export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_R export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR'; export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS'; -export interface EditDomainRedirectsAction extends Action { +export interface EditDomainRedirects { domain: string; redirects: ShlinkDomainRedirects; } -export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( - domain: string, - domainRedirects: Partial, -) => async (dispatch: Dispatch, getState: GetState) => { +export interface EditDomainRedirectsAction extends Action, EditDomainRedirects {} + +export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => ({ + domain, + redirects: domainRedirects, +}: EditDomainRedirects) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: EDIT_DOMAIN_REDIRECTS_START }); const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState); diff --git a/test/domains/helpers/DomainDropdown.test.tsx b/test/domains/helpers/DomainDropdown.test.tsx index 90b5658b..1efd1a5e 100644 --- a/test/domains/helpers/DomainDropdown.test.tsx +++ b/test/domains/helpers/DomainDropdown.test.tsx @@ -71,7 +71,7 @@ describe('', () => { expect(editDomainRedirects).not.toHaveBeenCalled(); await user.click(screen.getByText('Save')); - expect(editDomainRedirects).toHaveBeenCalledWith(domain, expect.anything()); + expect(editDomainRedirects).toHaveBeenCalledWith(expect.objectContaining({ domain })); await waitForElementToBeRemoved(() => screen.queryByRole('dialog')); }); diff --git a/test/domains/helpers/EditDomainRedirectsModal.test.tsx b/test/domains/helpers/EditDomainRedirectsModal.test.tsx index 6f402350..a83ddcbb 100644 --- a/test/domains/helpers/EditDomainRedirectsModal.test.tsx +++ b/test/domains/helpers/EditDomainRedirectsModal.test.tsx @@ -40,37 +40,49 @@ describe('', () => { expect(editDomainRedirects).not.toHaveBeenCalled(); submitForm(); - await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', { - baseUrlRedirect: 'baz', - regular404Redirect: null, - invalidShortUrlRedirect: null, + await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({ + domain: 'foo.com', + redirects: { + baseUrlRedirect: 'baz', + regular404Redirect: null, + invalidShortUrlRedirect: null, + }, })); await user.clear(screen.getByDisplayValue('baz')); await user.type(screen.getAllByPlaceholderText('No redirect')[0], 'new_base_url'); await user.type(screen.getAllByPlaceholderText('No redirect')[2], 'new_invalid_short_url'); submitForm(); - await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', { - baseUrlRedirect: 'new_base_url', - regular404Redirect: null, - invalidShortUrlRedirect: 'new_invalid_short_url', + await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({ + domain: 'foo.com', + redirects: { + baseUrlRedirect: 'new_base_url', + regular404Redirect: null, + invalidShortUrlRedirect: 'new_invalid_short_url', + }, })); await user.type(screen.getAllByPlaceholderText('No redirect')[1], 'new_regular_404'); await user.clear(screen.getByDisplayValue('new_invalid_short_url')); submitForm(); - await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', { - baseUrlRedirect: 'new_base_url', - regular404Redirect: 'new_regular_404', - invalidShortUrlRedirect: null, + await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({ + domain: 'foo.com', + redirects: { + baseUrlRedirect: 'new_base_url', + regular404Redirect: 'new_regular_404', + invalidShortUrlRedirect: null, + }, })); await Promise.all(screen.getAllByPlaceholderText('No redirect').map((element) => user.clear(element))); submitForm(); - await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', { - baseUrlRedirect: null, - regular404Redirect: null, - invalidShortUrlRedirect: null, + await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({ + domain: 'foo.com', + redirects: { + baseUrlRedirect: null, + regular404Redirect: null, + invalidShortUrlRedirect: null, + }, })); }); }); diff --git a/test/domains/reducers/domainRedirects.test.ts b/test/domains/reducers/domainRedirects.test.ts index f77f8377..c7ed433d 100644 --- a/test/domains/reducers/domainRedirects.test.ts +++ b/test/domains/reducers/domainRedirects.test.ts @@ -3,7 +3,7 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { EDIT_DOMAIN_REDIRECTS, EDIT_DOMAIN_REDIRECTS_ERROR, - EDIT_DOMAIN_REDIRECTS_START, + EDIT_DOMAIN_REDIRECTS_START, EditDomainRedirects, editDomainRedirects as editDomainRedirectsAction, } from '../../../src/domains/reducers/domainRedirects'; import { ShlinkDomainRedirects } from '../../../src/api/types'; @@ -22,7 +22,10 @@ describe('domainRedirectsReducer', () => { it('dispatches error when loading domains fails', async () => { editDomainRedirects.mockRejectedValue(new Error('error')); - await editDomainRedirectsAction(buildShlinkApiClient)(domain, {})(dispatch, getState); + await editDomainRedirectsAction(buildShlinkApiClient)(Mock.of({ domain }))( + dispatch, + getState, + ); expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START }); @@ -33,7 +36,10 @@ describe('domainRedirectsReducer', () => { it('dispatches domain and redirects once loaded', async () => { editDomainRedirects.mockResolvedValue(redirects); - await editDomainRedirectsAction(buildShlinkApiClient)(domain, {})(dispatch, getState); + await editDomainRedirectsAction(buildShlinkApiClient)(Mock.of({ domain }))( + dispatch, + getState, + ); expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START }); From 34f4411aa195754eee9face2df7f871738a262bb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 4 Nov 2022 18:56:34 +0100 Subject: [PATCH 10/12] Migrated domainRedirects reducer to redux/toolkit --- src/domains/reducers/domainRedirects.ts | 34 ++++++--------- src/domains/reducers/domainsList.ts | 23 ++++++----- src/domains/services/provideServices.ts | 15 ++++--- test/domains/reducers/domainRedirects.test.ts | 41 ++++++++----------- test/domains/reducers/domainsList.test.ts | 16 ++++---- 5 files changed, 61 insertions(+), 68 deletions(-) diff --git a/src/domains/reducers/domainRedirects.ts b/src/domains/reducers/domainRedirects.ts index 4a5472b9..aa97871b 100644 --- a/src/domains/reducers/domainRedirects.ts +++ b/src/domains/reducers/domainRedirects.ts @@ -1,33 +1,23 @@ -import { Action, Dispatch } from 'redux'; +import { createAsyncThunk } from '@reduxjs/toolkit'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkDomainRedirects } from '../../api/types'; -import { GetState } from '../../container/types'; -import { ApiErrorAction } from '../../api/types/actions'; -import { parseApiError } from '../../api/utils'; +import { ShlinkState } from '../../container/types'; -export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START'; -export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR'; -export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS'; +const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS'; export interface EditDomainRedirects { domain: string; redirects: ShlinkDomainRedirects; } -export interface EditDomainRedirectsAction extends Action, EditDomainRedirects {} - -export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => ({ - domain, - redirects: domainRedirects, -}: EditDomainRedirects) => async (dispatch: Dispatch, getState: GetState) => { - dispatch({ type: EDIT_DOMAIN_REDIRECTS_START }); - const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState); - - try { +export const editDomainRedirects = ( + buildShlinkApiClient: ShlinkApiClientBuilder, +) => createAsyncThunk( + EDIT_DOMAIN_REDIRECTS, + async ({ domain, redirects: domainRedirects }, { getState }) => { + const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState); const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects }); - dispatch({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects }); - } catch (e: any) { - dispatch({ type: EDIT_DOMAIN_REDIRECTS_ERROR, errorData: parseApiError(e) }); - } -}; + return { domain, redirects }; + }, +); diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index de1df166..bf7f8758 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -1,17 +1,17 @@ -import { createSlice, createAsyncThunk, createAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { createSlice, createAsyncThunk, createAction, SliceCaseReducers, AsyncThunk } from '@reduxjs/toolkit'; import { ShlinkDomainRedirects } from '../../api/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkState } from '../../container/types'; import { Domain, DomainStatus } from '../data'; import { hasServerData } from '../../servers/data'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; -import { EDIT_DOMAIN_REDIRECTS } from './domainRedirects'; import { ProblemDetailsError } from '../../api/types/errors'; import { parseApiError } from '../../api/utils'; +import { EditDomainRedirects } from './domainRedirects'; -export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS'; -export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; -export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN'; +const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS'; +const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; +const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN'; export interface DomainsList { domains: Domain[]; @@ -39,13 +39,16 @@ const initialState: DomainsList = { error: false, }; -export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) => +export const replaceRedirectsOnDomain = ({ domain, redirects }: EditDomainRedirects) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects }); export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); -export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { +export const domainsListReducerCreator = ( + buildShlinkApiClient: ShlinkApiClientBuilder, + editDomainRedirects: AsyncThunk, +) => { const listDomains = createAsyncThunk( LIST_DOMAINS, async (_, { getState }) => { @@ -110,10 +113,10 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())), })); - builder.addCase(EDIT_DOMAIN_REDIRECTS, (state, { domain, redirects }: any) => ({ // TODO Fix this "any" + builder.addCase(editDomainRedirects.fulfilled, (state, { payload }) => ({ ...state, - domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), - filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), + domains: state.domains.map(replaceRedirectsOnDomain(payload)), + filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(payload)), })); }, }); diff --git a/src/domains/services/provideServices.ts b/src/domains/services/provideServices.ts index f03a9c30..827302a3 100644 --- a/src/domains/services/provideServices.ts +++ b/src/domains/services/provideServices.ts @@ -18,14 +18,19 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { )); // Reducer - bottle.serviceFactory('domainsListReducerCreator', domainsListReducerCreator, 'buildShlinkApiClient'); - bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory( + 'domainsListReducerCreator', + domainsListReducerCreator, + 'buildShlinkApiClient', + 'editDomainRedirects', + ); + bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator'); // Actions - bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here - bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator'); + bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator'); bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient'); - bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here + bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator'); }; export default provideServices; diff --git a/test/domains/reducers/domainRedirects.test.ts b/test/domains/reducers/domainRedirects.test.ts index c7ed433d..567e0c1f 100644 --- a/test/domains/reducers/domainRedirects.test.ts +++ b/test/domains/reducers/domainRedirects.test.ts @@ -1,11 +1,6 @@ import { Mock } from 'ts-mockery'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; -import { - EDIT_DOMAIN_REDIRECTS, - EDIT_DOMAIN_REDIRECTS_ERROR, - EDIT_DOMAIN_REDIRECTS_START, EditDomainRedirects, - editDomainRedirects as editDomainRedirectsAction, -} from '../../../src/domains/reducers/domainRedirects'; +import { EditDomainRedirects, editDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import { ShlinkDomainRedirects } from '../../../src/api/types'; describe('domainRedirectsReducer', () => { @@ -16,35 +11,33 @@ describe('domainRedirectsReducer', () => { const redirects = Mock.all(); const dispatch = jest.fn(); const getState = jest.fn(); - const editDomainRedirects = jest.fn(); - const buildShlinkApiClient = () => Mock.of({ editDomainRedirects }); + const editDomainRedirectsCall = jest.fn(); + const buildShlinkApiClient = () => Mock.of({ editDomainRedirects: editDomainRedirectsCall }); + const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient); it('dispatches error when loading domains fails', async () => { - editDomainRedirects.mockRejectedValue(new Error('error')); + editDomainRedirectsCall.mockRejectedValue(new Error('error')); - await editDomainRedirectsAction(buildShlinkApiClient)(Mock.of({ domain }))( - dispatch, - getState, - ); + await editDomainRedirectsAction(Mock.of({ domain }))(dispatch, getState, {}); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_DOMAIN_REDIRECTS_ERROR }); - expect(editDomainRedirects).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ + type: editDomainRedirectsAction.rejected.toString(), + })); + expect(editDomainRedirectsCall).toHaveBeenCalledTimes(1); }); it('dispatches domain and redirects once loaded', async () => { - editDomainRedirects.mockResolvedValue(redirects); + editDomainRedirectsCall.mockResolvedValue(redirects); - await editDomainRedirectsAction(buildShlinkApiClient)(Mock.of({ domain }))( - dispatch, - getState, - ); + await editDomainRedirectsAction(Mock.of({ domain }))(dispatch, getState, {}); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_DOMAIN_REDIRECTS, domain, redirects }); - expect(editDomainRedirects).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ + type: editDomainRedirectsAction.fulfilled.toString(), + payload: { domain, redirects }, + })); + expect(editDomainRedirectsCall).toHaveBeenCalledTimes(1); }); }); }); diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index dac4a87d..7efb6b00 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -6,7 +6,7 @@ import { replaceStatusOnDomain, domainsListReducerCreator, } from '../../../src/domains/reducers/domainsList'; -import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedirects'; +import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import { ShlinkDomainRedirects } from '../../../src/api/types'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { Domain } from '../../../src/domains/data'; @@ -30,8 +30,10 @@ describe('domainsListReducer', () => { data: { type: 'NOT_FOUND', status: 404 }, }, }); + const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient); const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator( buildShlinkApiClient, + editDomainRedirectsThunk, ); beforeEach(jest.clearAllMocks); @@ -72,12 +74,12 @@ describe('domainsListReducer', () => { invalidShortUrlRedirect: null, }; - expect(reducer( - Mock.of({ domains, filteredDomains }), - { type: EDIT_DOMAIN_REDIRECTS, domain, redirects }, - )).toEqual({ - domains: domains.map(replaceRedirectsOnDomain(domain, redirects)), - filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), + expect(reducer(Mock.of({ domains, filteredDomains }), { + type: editDomainRedirectsThunk.fulfilled.toString(), + payload: { domain, redirects }, + })).toEqual({ + domains: domains.map(replaceRedirectsOnDomain({ domain, redirects })), + filteredDomains: filteredDomains.map(replaceRedirectsOnDomain({ domain, redirects })), }); }); From 6363822ffd0941222a76f40a87589acd40a844d8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 4 Nov 2022 18:58:21 +0100 Subject: [PATCH 11/12] Updated to redux/toolkit 1.9 --- package-lock.json | 66 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0eafdcd..411c80d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", - "@reduxjs/toolkit": "^1.8.6", + "@reduxjs/toolkit": "^1.9.0", "axios": "^1.1.2", "bootstrap": "^5.2.2", "bottlejs": "^2.0.1", @@ -4269,14 +4269,14 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz", - "integrity": "sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.0.tgz", + "integrity": "sha512-ak11IrjYcUXRqlhNPwnz6AcvA2ynJTu8PzDbbqQw4a3xR4KZtgiqbNblQD+10CRbfK4+5C79SOyxnT9dhBqFnA==", "dependencies": { - "immer": "^9.0.7", - "redux": "^4.1.2", - "redux-thunk": "^2.4.1", - "reselect": "^4.1.5" + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", @@ -12167,9 +12167,9 @@ } }, "node_modules/immer": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", - "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==", + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", + "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -22746,9 +22746,9 @@ } }, "node_modules/redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "peerDependencies": { "redux": "^4" } @@ -22919,9 +22919,9 @@ "dev": true }, "node_modules/reselect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", - "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", @@ -29925,14 +29925,14 @@ "requires": {} }, "@reduxjs/toolkit": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz", - "integrity": "sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.0.tgz", + "integrity": "sha512-ak11IrjYcUXRqlhNPwnz6AcvA2ynJTu8PzDbbqQw4a3xR4KZtgiqbNblQD+10CRbfK4+5C79SOyxnT9dhBqFnA==", "requires": { - "immer": "^9.0.7", - "redux": "^4.1.2", - "redux-thunk": "^2.4.1", - "reselect": "^4.1.5" + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" } }, "@remix-run/router": { @@ -35893,9 +35893,9 @@ "dev": true }, "immer": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", - "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==" + "version": "9.0.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", + "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==" }, "immutable": { "version": "4.0.0", @@ -43691,9 +43691,9 @@ } }, "redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "requires": {} }, "regenerate": { @@ -43828,9 +43828,9 @@ "dev": true }, "reselect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", - "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" }, "resize-observer-polyfill": { "version": "1.5.1", diff --git a/package.json b/package.json index 549b0d11..db375266 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@fortawesome/free-regular-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", - "@reduxjs/toolkit": "^1.8.6", + "@reduxjs/toolkit": "^1.9.0", "axios": "^1.1.2", "bootstrap": "^5.2.2", "bottlejs": "^2.0.1", From a1e2cd72749c74013a043745e43d26141a893803 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 4 Nov 2022 18:59:08 +0100 Subject: [PATCH 12/12] Fixed changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3afe33..399364b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed * [#713](https://github.com/shlinkio/shlink-web-client/issues/713) Updated dependencies. +* [#620](https://github.com/shlinkio/shlink-web-client/issues/620) Migrated domains-related reducers to redux toolkit as a POC. ### Deprecated * *Nothing*