Migrated domainRedirects reducer to redux/toolkit

This commit is contained in:
Alejandro Celaya 2022-11-04 18:56:34 +01:00
parent b6d08e2203
commit 34f4411aa1
5 changed files with 61 additions and 68 deletions

View file

@ -1,33 +1,23 @@
import { Action, Dispatch } from 'redux'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkDomainRedirects } from '../../api/types'; import { ShlinkDomainRedirects } from '../../api/types';
import { GetState } from '../../container/types'; import { ShlinkState } from '../../container/types';
import { ApiErrorAction } from '../../api/types/actions';
import { parseApiError } from '../../api/utils';
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START'; const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
export interface EditDomainRedirects { export interface EditDomainRedirects {
domain: string; domain: string;
redirects: ShlinkDomainRedirects; redirects: ShlinkDomainRedirects;
} }
export interface EditDomainRedirectsAction extends Action<string>, EditDomainRedirects {} export const editDomainRedirects = (
buildShlinkApiClient: ShlinkApiClientBuilder,
export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => ({ ) => createAsyncThunk<EditDomainRedirects, EditDomainRedirects, { state: ShlinkState }>(
domain, EDIT_DOMAIN_REDIRECTS,
redirects: domainRedirects, async ({ domain, redirects: domainRedirects }, { getState }) => {
}: EditDomainRedirects) => async (dispatch: Dispatch, getState: GetState) => { const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
try {
const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects }); const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects });
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects }); return { domain, redirects };
} catch (e: any) { },
dispatch<ApiErrorAction>({ type: EDIT_DOMAIN_REDIRECTS_ERROR, errorData: parseApiError(e) }); );
}
};

View file

@ -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 { ShlinkDomainRedirects } from '../../api/types';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkState } from '../../container/types'; import { ShlinkState } from '../../container/types';
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 } from './domainRedirects';
import { ProblemDetailsError } from '../../api/types/errors'; import { ProblemDetailsError } from '../../api/types/errors';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { EditDomainRedirects } from './domainRedirects';
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS'; const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN'; const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
export interface DomainsList { export interface DomainsList {
domains: Domain[]; domains: Domain[];
@ -39,13 +39,16 @@ const initialState: DomainsList = {
error: false, error: false,
}; };
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) => export const replaceRedirectsOnDomain = ({ domain, redirects }: EditDomainRedirects) =>
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects }); (d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects });
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 const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { export const domainsListReducerCreator = (
buildShlinkApiClient: ShlinkApiClientBuilder,
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
) => {
const listDomains = createAsyncThunk<ListDomains, void, { state: ShlinkState }>( const listDomains = createAsyncThunk<ListDomains, void, { state: ShlinkState }>(
LIST_DOMAINS, LIST_DOMAINS,
async (_, { getState }) => { async (_, { getState }) => {
@ -110,10 +113,10 @@ export const domainsListReducerCreator = (buildShlinkApiClient: ShlinkApiClientB
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())), 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, ...state,
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)), domains: state.domains.map(replaceRedirectsOnDomain(payload)),
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(payload)),
})); }));
}, },
}); });

View file

@ -18,14 +18,19 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
)); ));
// Reducer // Reducer
bottle.serviceFactory('domainsListReducerCreator', domainsListReducerCreator, 'buildShlinkApiClient'); bottle.serviceFactory(
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here 'domainsListReducerCreator',
domainsListReducerCreator,
'buildShlinkApiClient',
'editDomainRedirects',
);
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator');
// Actions // Actions
bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator');
bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator'); // TODO Improve type checks on the prop that gets picked here bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator');
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient'); 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; export default provideServices;

View file

@ -1,11 +1,6 @@
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import { import { EditDomainRedirects, editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
EDIT_DOMAIN_REDIRECTS,
EDIT_DOMAIN_REDIRECTS_ERROR,
EDIT_DOMAIN_REDIRECTS_START, EditDomainRedirects,
editDomainRedirects as editDomainRedirectsAction,
} from '../../../src/domains/reducers/domainRedirects';
import { ShlinkDomainRedirects } from '../../../src/api/types'; import { ShlinkDomainRedirects } from '../../../src/api/types';
describe('domainRedirectsReducer', () => { describe('domainRedirectsReducer', () => {
@ -16,35 +11,33 @@ describe('domainRedirectsReducer', () => {
const redirects = Mock.all<ShlinkDomainRedirects>(); const redirects = Mock.all<ShlinkDomainRedirects>();
const dispatch = jest.fn(); const dispatch = jest.fn();
const getState = jest.fn(); const getState = jest.fn();
const editDomainRedirects = jest.fn(); const editDomainRedirectsCall = jest.fn();
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ editDomainRedirects }); const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ editDomainRedirects: editDomainRedirectsCall });
const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient);
it('dispatches error when loading domains fails', async () => { it('dispatches error when loading domains fails', async () => {
editDomainRedirects.mockRejectedValue(new Error('error')); editDomainRedirectsCall.mockRejectedValue(new Error('error'));
await editDomainRedirectsAction(buildShlinkApiClient)(Mock.of<EditDomainRedirects>({ domain }))( await editDomainRedirectsAction(Mock.of<EditDomainRedirects>({ domain }))(dispatch, getState, {});
dispatch,
getState,
);
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START }); expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_DOMAIN_REDIRECTS_ERROR }); type: editDomainRedirectsAction.rejected.toString(),
expect(editDomainRedirects).toHaveBeenCalledTimes(1); }));
expect(editDomainRedirectsCall).toHaveBeenCalledTimes(1);
}); });
it('dispatches domain and redirects once loaded', async () => { it('dispatches domain and redirects once loaded', async () => {
editDomainRedirects.mockResolvedValue(redirects); editDomainRedirectsCall.mockResolvedValue(redirects);
await editDomainRedirectsAction(buildShlinkApiClient)(Mock.of<EditDomainRedirects>({ domain }))( await editDomainRedirectsAction(Mock.of<EditDomainRedirects>({ domain }))(dispatch, getState, {});
dispatch,
getState,
);
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START }); expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_DOMAIN_REDIRECTS, domain, redirects }); type: editDomainRedirectsAction.fulfilled.toString(),
expect(editDomainRedirects).toHaveBeenCalledTimes(1); payload: { domain, redirects },
}));
expect(editDomainRedirectsCall).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View file

@ -6,7 +6,7 @@ import {
replaceStatusOnDomain, replaceStatusOnDomain,
domainsListReducerCreator, domainsListReducerCreator,
} from '../../../src/domains/reducers/domainsList'; } 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 { ShlinkDomainRedirects } from '../../../src/api/types';
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import { Domain } from '../../../src/domains/data'; import { Domain } from '../../../src/domains/data';
@ -30,8 +30,10 @@ describe('domainsListReducer', () => {
data: { type: 'NOT_FOUND', status: 404 }, data: { type: 'NOT_FOUND', status: 404 },
}, },
}); });
const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient);
const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator( const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator(
buildShlinkApiClient, buildShlinkApiClient,
editDomainRedirectsThunk,
); );
beforeEach(jest.clearAllMocks); beforeEach(jest.clearAllMocks);
@ -72,12 +74,12 @@ describe('domainsListReducer', () => {
invalidShortUrlRedirect: null, invalidShortUrlRedirect: null,
}; };
expect(reducer( expect(reducer(Mock.of<DomainsList>({ domains, filteredDomains }), {
Mock.of<DomainsList>({ domains, filteredDomains }), type: editDomainRedirectsThunk.fulfilled.toString(),
{ type: EDIT_DOMAIN_REDIRECTS, domain, redirects }, payload: { domain, redirects },
)).toEqual({ })).toEqual({
domains: domains.map(replaceRedirectsOnDomain(domain, redirects)), domains: domains.map(replaceRedirectsOnDomain({ domain, redirects })),
filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)), filteredDomains: filteredDomains.map(replaceRedirectsOnDomain({ domain, redirects })),
}); });
}); });