Migrated shortUrlsList reducer to RTK

This commit is contained in:
Alejandro Celaya 2022-11-09 19:13:44 +01:00
parent ae49090bad
commit 7bfccafca8
12 changed files with 184 additions and 160 deletions

View file

@ -2,7 +2,6 @@ import { IContainer } from 'bottlejs';
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { serversReducer } from '../servers/reducers/servers'; import { serversReducer } from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer'; import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits'; import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
import tagVisitsReducer from '../visits/reducers/tagVisits'; import tagVisitsReducer from '../visits/reducers/tagVisits';
import domainVisitsReducer from '../visits/reducers/domainVisits'; import domainVisitsReducer from '../visits/reducers/domainVisits';
@ -17,7 +16,7 @@ import { ShlinkState } from '../container/types';
export default (container: IContainer) => combineReducers<ShlinkState>({ export default (container: IContainer) => combineReducers<ShlinkState>({
servers: serversReducer, servers: serversReducer,
selectedServer: selectedServerReducer, selectedServer: selectedServerReducer,
shortUrlsList: shortUrlsListReducer, shortUrlsList: container.shortUrlsListReducer,
shortUrlCreation: container.shortUrlCreationReducer, shortUrlCreation: container.shortUrlCreationReducer,
shortUrlDeletion: container.shortUrlDeletionReducer, shortUrlDeletion: container.shortUrlDeletionReducer,
shortUrlEdition: container.shortUrlEditionReducer, shortUrlEdition: container.shortUrlEditionReducer,

View file

@ -6,7 +6,6 @@ import { parseApiError } from '../../api/utils';
import { ProblemDetailsError } from '../../api/types/errors'; import { ProblemDetailsError } from '../../api/types/errors';
const REDUCER_PREFIX = 'shlink/shortUrlCreation'; const REDUCER_PREFIX = 'shlink/shortUrlCreation';
export const CREATE_SHORT_URL = `${REDUCER_PREFIX}/createShortUrl`;
export type ShortUrlCreation = { export type ShortUrlCreation = {
saving: false; saving: false;
@ -37,7 +36,7 @@ const initialState: ShortUrlCreation = {
}; };
export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk( export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
CREATE_SHORT_URL, `${REDUCER_PREFIX}/createShortUrl`,
(data: ShortUrlData, { getState }): Promise<ShortUrl> => { (data: ShortUrlData, { getState }): Promise<ShortUrl> => {
const { createShortUrl: shlinkCreateShortUrl } = buildShlinkApiClient(getState); const { createShortUrl: shlinkCreateShortUrl } = buildShlinkApiClient(getState);
return shlinkCreateShortUrl(data); return shlinkCreateShortUrl(data);

View file

@ -1,4 +1,4 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { createAsyncThunk } from '../../utils/helpers/redux'; import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
@ -6,7 +6,6 @@ import { ProblemDetailsError } from '../../api/types/errors';
import { ShortUrlIdentifier } from '../data'; import { ShortUrlIdentifier } from '../data';
const REDUCER_PREFIX = 'shlink/shortUrlDeletion'; const REDUCER_PREFIX = 'shlink/shortUrlDeletion';
export const SHORT_URL_DELETED = `${REDUCER_PREFIX}/deleteShortUrl`;
export interface ShortUrlDeletion { export interface ShortUrlDeletion {
shortCode: string; shortCode: string;
@ -16,8 +15,6 @@ export interface ShortUrlDeletion {
errorData?: ProblemDetailsError; errorData?: ProblemDetailsError;
} }
export type DeleteShortUrlAction = PayloadAction<ShortUrlIdentifier>;
const initialState: ShortUrlDeletion = { const initialState: ShortUrlDeletion = {
shortCode: '', shortCode: '',
loading: false, loading: false,
@ -25,16 +22,16 @@ const initialState: ShortUrlDeletion = {
error: false, error: false,
}; };
export const shortUrlDeletionReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
const deleteShortUrl = createAsyncThunk( `${REDUCER_PREFIX}/deleteShortUrl`,
SHORT_URL_DELETED, async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrlIdentifier> => {
async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrlIdentifier> => { const { deleteShortUrl: shlinkDeleteShortUrl } = buildShlinkApiClient(getState);
const { deleteShortUrl: shlinkDeleteShortUrl } = buildShlinkApiClient(getState); await shlinkDeleteShortUrl(shortCode, domain);
await shlinkDeleteShortUrl(shortCode, domain); return { shortCode, domain };
return { shortCode, domain }; },
}, );
);
export const shortUrlDeletionReducerCreator = (deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>) => {
const { actions, reducer } = createSlice({ const { actions, reducer } = createSlice({
name: REDUCER_PREFIX, name: REDUCER_PREFIX,
initialState, initialState,
@ -42,11 +39,14 @@ export const shortUrlDeletionReducerCreator = (buildShlinkApiClient: ShlinkApiCl
resetDeleteShortUrl: () => initialState, resetDeleteShortUrl: () => initialState,
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(deleteShortUrl.pending, (state) => ({ ...state, loading: true, error: false, deleted: false })); builder.addCase(
builder.addCase(deleteShortUrl.rejected, (state, { error }) => ( deleteShortUrlThunk.pending,
(state) => ({ ...state, loading: true, error: false, deleted: false }),
);
builder.addCase(deleteShortUrlThunk.rejected, (state, { error }) => (
{ ...state, errorData: parseApiError(error), loading: false, error: true, deleted: false } { ...state, errorData: parseApiError(error), loading: false, error: true, deleted: false }
)); ));
builder.addCase(deleteShortUrl.fulfilled, (state, { payload }) => ( builder.addCase(deleteShortUrlThunk.fulfilled, (state, { payload }) => (
{ ...state, shortCode: payload.shortCode, loading: false, error: false, deleted: true } { ...state, shortCode: payload.shortCode, loading: false, error: false, deleted: true }
)); ));
}, },
@ -54,5 +54,5 @@ export const shortUrlDeletionReducerCreator = (buildShlinkApiClient: ShlinkApiCl
const { resetDeleteShortUrl } = actions; const { resetDeleteShortUrl } = actions;
return { reducer, deleteShortUrl, resetDeleteShortUrl }; return { reducer, resetDeleteShortUrl };
}; };

View file

@ -6,7 +6,6 @@ import { parseApiError } from '../../api/utils';
import { ProblemDetailsError } from '../../api/types/errors'; import { ProblemDetailsError } from '../../api/types/errors';
const REDUCER_PREFIX = 'shlink/shortUrlEdition'; const REDUCER_PREFIX = 'shlink/shortUrlEdition';
export const SHORT_URL_EDITED = `${REDUCER_PREFIX}/editShortUrl`;
export interface ShortUrlEdition { export interface ShortUrlEdition {
shortUrl?: ShortUrl; shortUrl?: ShortUrl;
@ -29,7 +28,7 @@ const initialState: ShortUrlEdition = {
}; };
export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk( export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
SHORT_URL_EDITED, `${REDUCER_PREFIX}/editShortUrl`,
({ shortCode, domain, data }: EditShortUrl, { getState }): Promise<ShortUrl> => { ({ shortCode, domain, data }: EditShortUrl, { getState }): Promise<ShortUrl> => {
const { updateShortUrl } = buildShlinkApiClient(getState); const { updateShortUrl } = buildShlinkApiClient(getState);
return updateShortUrl(shortCode, domain, data as any); // FIXME parse dates return updateShortUrl(shortCode, domain, data as any); // FIXME parse dates
@ -51,4 +50,4 @@ export const shortUrlEditionReducerCreator = (editShortUrlThunk: ReturnType<type
(_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false, saved: true }), (_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false, saved: true }),
); );
}, },
}).reducer; });

View file

@ -1,21 +1,16 @@
import { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { assoc, assocPath, last, pipe, reject } from 'ramda'; import { assoc, assocPath, last, pipe, reject } from 'ramda';
import { Dispatch } from 'redux';
import { shortUrlMatches } from '../helpers'; import { shortUrlMatches } from '../helpers';
import { createNewVisits, CreateVisitsAction } from '../../visits/reducers/visitCreation'; import { createNewVisits } from '../../visits/reducers/visitCreation';
import { buildReducer } from '../../utils/helpers/redux'; import { createAsyncThunk } from '../../utils/helpers/redux';
import { GetState } from '../../container/types';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types'; import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion'; import { deleteShortUrl } from './shortUrlDeletion';
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation'; import { createShortUrl } from './shortUrlCreation';
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition'; import { editShortUrl } from './shortUrlEdition';
import { ShortUrl } from '../data'; import { ShortUrl } from '../data';
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; const REDUCER_PREFIX = 'shlink/shortUrlsList';
export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
export const ITEMS_IN_OVERVIEW_PAGE = 5; export const ITEMS_IN_OVERVIEW_PAGE = 5;
export interface ShortUrlsList { export interface ShortUrlsList {
@ -24,94 +19,104 @@ export interface ShortUrlsList {
error: boolean; error: boolean;
} }
export type ListShortUrlsAction = PayloadAction<ShlinkShortUrlsResponse>;
export type ListShortUrlsCombinedAction = (
ListShortUrlsAction
& CreateVisitsAction
& CreateShortUrlAction
& DeleteShortUrlAction
& ShortUrlEditedAction
);
const initialState: ShortUrlsList = { const initialState: ShortUrlsList = {
loading: true, loading: true,
error: false, error: false,
}; };
export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({ export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
[LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }), `${REDUCER_PREFIX}/listShortUrls`,
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }), (params: ShlinkShortUrlsListParams | void, { getState }): Promise<ShlinkShortUrlsResponse> => {
[LIST_SHORT_URLS]: (_, { payload: shortUrls }) => ({ loading: false, error: false, shortUrls }), const { listShortUrls: shlinkListShortUrls } = buildShlinkApiClient(getState);
[`${SHORT_URL_DELETED}/fulfilled`]: pipe( // TODO Do not hardcode action type here return shlinkListShortUrls(params ?? {});
(state: ShortUrlsList, { payload }: DeleteShortUrlAction) => (!state.shortUrls ? state : assocPath( },
['shortUrls', 'data'], );
reject<ShortUrl, ShortUrl[]>((shortUrl) =>
shortUrlMatches(shortUrl, payload.shortCode, payload.domain), state.shortUrls.data),
state,
)),
(state) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems - 1,
state,
)),
),
[createNewVisits.toString()]: (state, { payload }) => assocPath(
['shortUrls', 'data'],
state.shortUrls?.data?.map(
(currentShortUrl) => {
// Find the last of the new visit for this short URL, and pick the amount of visits from it
const lastVisit = last(
payload.createdVisits.filter(
({ shortUrl }) => shortUrl && shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain),
),
);
return lastVisit?.shortUrl export const shortUrlsListReducerCreator = (
? assoc('visitsCount', lastVisit.shortUrl.visitsCount, currentShortUrl) listShortUrlsThunk: ReturnType<typeof listShortUrls>,
: currentShortUrl; editShortUrlThunk: ReturnType<typeof editShortUrl>,
}, createShortUrlThunk: ReturnType<typeof createShortUrl>,
), deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>,
state, ) => createSlice({
), name: REDUCER_PREFIX,
[`${CREATE_SHORT_URL}/fulfilled`]: pipe( // TODO Do not hardcode action type here initialState,
// The only place where the list and the creation form coexist is the overview page. reducers: {},
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL. extraReducers: (builder) => {
// We can also remove the items above the amount that is displayed there. builder.addCase(listShortUrlsThunk.pending, (state) => ({ ...state, loading: true, error: false }));
(state: ShortUrlsList, { payload }: CreateShortUrlAction) => (!state.shortUrls ? state : assocPath( builder.addCase(listShortUrlsThunk.rejected, () => ({ loading: false, error: true }));
['shortUrls', 'data'], builder.addCase(
[payload, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)], listShortUrlsThunk.fulfilled,
state, (_, { payload: shortUrls }) => ({ loading: false, error: false, shortUrls }),
)), );
(state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems + 1,
state,
)),
),
// TODO Do not hardcode action type here
[`${SHORT_URL_EDITED}/fulfilled`]: (state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'],
state.shortUrls.data.map((shortUrl) => {
const { shortCode, domain } = editedShortUrl;
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl; builder.addCase(
}), createShortUrlThunk.fulfilled,
state, pipe(
)), // The only place where the list and the creation form coexist is the overview page.
}, initialState); // There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
// We can also remove the items above the amount that is displayed there.
(state, { payload }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'],
[payload, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)],
state,
)),
(state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems + 1,
state,
)),
),
);
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( builder.addCase(
params: ShlinkShortUrlsListParams = {}, editShortUrlThunk.fulfilled,
) => async (dispatch: Dispatch, getState: GetState) => { (state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
dispatch({ type: LIST_SHORT_URLS_START }); ['shortUrls', 'data'],
const { listShortUrls: shlinkListShortUrls } = buildShlinkApiClient(getState); state.shortUrls.data.map((shortUrl) => {
const { shortCode, domain } = editedShortUrl;
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
}),
state,
)),
);
try { builder.addCase(
const payload = await shlinkListShortUrls(params); deleteShortUrlThunk.fulfilled,
pipe(
(state, { payload }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'],
reject<ShortUrl, ShortUrl[]>((shortUrl) =>
shortUrlMatches(shortUrl, payload.shortCode, payload.domain), state.shortUrls.data),
state,
)),
(state) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems - 1,
state,
)),
),
);
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, payload }); builder.addCase(
} catch (e) { createNewVisits,
dispatch({ type: LIST_SHORT_URLS_ERROR }); (state, { payload }) => assocPath(
} ['shortUrls', 'data'],
}; state.shortUrls?.data?.map(
(currentShortUrl) => {
// Find the last of the new visit for this short URL, and pick the amount of visits from it
const lastVisit = last(
payload.createdVisits.filter(
({ shortUrl }) => shortUrl && shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain),
),
);
return lastVisit?.shortUrl
? assoc('visitsCount', lastVisit.shortUrl.visitsCount, currentShortUrl)
: currentShortUrl;
},
),
state,
),
);
},
});

View file

@ -7,9 +7,9 @@ import { ShortUrlsRowMenu } from '../helpers/ShortUrlsRowMenu';
import { CreateShortUrl } from '../CreateShortUrl'; import { CreateShortUrl } from '../CreateShortUrl';
import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal'; import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal';
import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult'; import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult';
import { listShortUrls } from '../reducers/shortUrlsList'; import { listShortUrls, shortUrlsListReducerCreator } from '../reducers/shortUrlsList';
import { shortUrlCreationReducerCreator, createShortUrl } from '../reducers/shortUrlCreation'; import { shortUrlCreationReducerCreator, createShortUrl } from '../reducers/shortUrlCreation';
import { shortUrlDeletionReducerCreator } from '../reducers/shortUrlDeletion'; import { shortUrlDeletionReducerCreator, deleteShortUrl } from '../reducers/shortUrlDeletion';
import { editShortUrl, shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition'; import { editShortUrl, shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition';
import { shortUrlDetailReducerCreator } from '../reducers/shortUrlDetail'; import { shortUrlDetailReducerCreator } from '../reducers/shortUrlDetail';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
@ -57,12 +57,23 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer'])); bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
// Reducers // Reducers
bottle.serviceFactory(
'shortUrlsListReducerCreator',
shortUrlsListReducerCreator,
'listShortUrls',
'editShortUrl',
'createShortUrl',
'deleteShortUrl',
);
bottle.serviceFactory('shortUrlsListReducer', prop('reducer'), 'shortUrlsListReducerCreator');
bottle.serviceFactory('shortUrlCreationReducerCreator', shortUrlCreationReducerCreator, 'createShortUrl'); bottle.serviceFactory('shortUrlCreationReducerCreator', shortUrlCreationReducerCreator, 'createShortUrl');
bottle.serviceFactory('shortUrlCreationReducer', prop('reducer'), 'shortUrlCreationReducerCreator'); bottle.serviceFactory('shortUrlCreationReducer', prop('reducer'), 'shortUrlCreationReducerCreator');
bottle.serviceFactory('shortUrlEditionReducer', shortUrlEditionReducerCreator, 'editShortUrl'); bottle.serviceFactory('shortUrlEditionReducerCreator', shortUrlEditionReducerCreator, 'editShortUrl');
bottle.serviceFactory('shortUrlEditionReducer', prop('reducer'), 'shortUrlEditionReducerCreator');
bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'buildShlinkApiClient'); bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'deleteShortUrl');
bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator'); bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator');
bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'buildShlinkApiClient'); bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'buildShlinkApiClient');
@ -74,7 +85,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient'); bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient');
bottle.serviceFactory('resetCreateShortUrl', prop('resetCreateShortUrl'), 'shortUrlCreationReducerCreator'); bottle.serviceFactory('resetCreateShortUrl', prop('resetCreateShortUrl'), 'shortUrlCreationReducerCreator');
bottle.serviceFactory('deleteShortUrl', prop('deleteShortUrl'), 'shortUrlDeletionReducerCreator'); bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient');
bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator'); bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator');
bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator'); bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator');

View file

@ -87,7 +87,7 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
export const filterTags = createAction<string>(`${REDUCER_PREFIX}/filterTags`); export const filterTags = createAction<string>(`${REDUCER_PREFIX}/filterTags`);
export const reducer = ( export const tagsListReducerCreator = (
listTagsThunk: ReturnType<typeof listTags>, listTagsThunk: ReturnType<typeof listTags>,
createShortUrlThunk: ReturnType<typeof createShortUrl>, createShortUrlThunk: ReturnType<typeof createShortUrl>,
) => createSlice({ ) => createSlice({
@ -128,4 +128,4 @@ export const reducer = (
tags: stateTags.concat(payload.tags.filter((tag: string) => !stateTags.includes(tag))), // More performant than [ ...new Set(...) ] tags: stateTags.concat(payload.tags.filter((tag: string) => !stateTags.includes(tag))), // More performant than [ ...new Set(...) ]
})); }));
}, },
}).reducer; });

View file

@ -5,7 +5,7 @@ import { TagCard } from '../TagCard';
import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal'; import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal';
import { EditTagModal } from '../helpers/EditTagModal'; import { EditTagModal } from '../helpers/EditTagModal';
import { TagsList } from '../TagsList'; import { TagsList } from '../TagsList';
import { filterTags, listTags, reducer } from '../reducers/tagsList'; import { filterTags, listTags, tagsListReducerCreator } from '../reducers/tagsList';
import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete'; import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete';
import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit'; import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
@ -44,7 +44,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient'); bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient');
bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator'); bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator');
bottle.serviceFactory('tagsListReducer', reducer, 'listTags', 'createShortUrl'); bottle.serviceFactory('tagsListReducerCreator', tagsListReducerCreator, 'listTags', 'createShortUrl');
bottle.serviceFactory('tagsListReducer', prop('reducer'), 'tagsListReducerCreator');
// Actions // Actions
const listTagsActionFactory = (force: boolean) => const listTagsActionFactory = (force: boolean) =>

View file

@ -1,12 +1,16 @@
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { shortUrlDeletionReducerCreator } from '../../../src/short-urls/reducers/shortUrlDeletion'; import {
shortUrlDeletionReducerCreator,
deleteShortUrl as deleteShortUrlCretor,
} from '../../../src/short-urls/reducers/shortUrlDeletion';
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import { ProblemDetailsError } from '../../../src/api/types/errors'; import { ProblemDetailsError } from '../../../src/api/types/errors';
describe('shortUrlDeletionReducer', () => { describe('shortUrlDeletionReducer', () => {
const deleteShortUrlCall = jest.fn(); const deleteShortUrlCall = jest.fn();
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ deleteShortUrl: deleteShortUrlCall }); const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ deleteShortUrl: deleteShortUrlCall });
const { reducer, resetDeleteShortUrl, deleteShortUrl } = shortUrlDeletionReducerCreator(buildShlinkApiClient); const deleteShortUrl = deleteShortUrlCretor(buildShlinkApiClient);
const { reducer, resetDeleteShortUrl } = shortUrlDeletionReducerCreator(deleteShortUrl);
beforeEach(jest.clearAllMocks); beforeEach(jest.clearAllMocks);

View file

@ -15,7 +15,7 @@ describe('shortUrlEditionReducer', () => {
const updateShortUrl = jest.fn().mockResolvedValue(shortUrl); const updateShortUrl = jest.fn().mockResolvedValue(shortUrl);
const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl }); const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl });
const editShortUrl = editShortUrlCreator(buildShlinkApiClient); const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
const reducer = shortUrlEditionReducerCreator(editShortUrl); const { reducer } = shortUrlEditionReducerCreator(editShortUrl);
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);

View file

@ -1,37 +1,44 @@
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import reducer, { import {
LIST_SHORT_URLS, listShortUrls as listShortUrlsCreator,
LIST_SHORT_URLS_ERROR, shortUrlsListReducerCreator,
LIST_SHORT_URLS_START,
listShortUrls,
} from '../../../src/short-urls/reducers/shortUrlsList'; } from '../../../src/short-urls/reducers/shortUrlsList';
import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { deleteShortUrl as deleteShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlDeletion';
import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types';
import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation';
import { editShortUrl as editShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlEdition';
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
import { ShortUrl } from '../../../src/short-urls/data'; import { ShortUrl } from '../../../src/short-urls/data';
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types';
import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation';
import { SHORT_URL_EDITED } from '../../../src/short-urls/reducers/shortUrlEdition';
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
describe('shortUrlsListReducer', () => { describe('shortUrlsListReducer', () => {
const shortCode = 'abc123'; const shortCode = 'abc123';
const listShortUrlsMock = jest.fn();
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
const listShortUrls = listShortUrlsCreator(buildShlinkApiClient);
const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
const deleteShortUrl = deleteShortUrlCreator(buildShlinkApiClient);
const { reducer } = shortUrlsListReducerCreator(listShortUrls, editShortUrl, createShortUrl, deleteShortUrl);
afterEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on LIST_SHORT_URLS_START', () => it('returns loading on LIST_SHORT_URLS_START', () =>
expect(reducer(undefined, { type: LIST_SHORT_URLS_START } as any)).toEqual({ expect(reducer(undefined, { type: listShortUrls.pending.toString() })).toEqual({
loading: true, loading: true,
error: false, error: false,
})); }));
it('returns short URLs on LIST_SHORT_URLS', () => it('returns short URLs on LIST_SHORT_URLS', () =>
expect(reducer(undefined, { type: LIST_SHORT_URLS, payload: { data: [] } } as any)).toEqual({ expect(reducer(undefined, { type: listShortUrls.fulfilled.toString(), payload: { data: [] } })).toEqual({
shortUrls: { data: [] }, shortUrls: { data: [] },
loading: false, loading: false,
error: false, error: false,
})); }));
it('returns error on LIST_SHORT_URLS_ERROR', () => it('returns error on LIST_SHORT_URLS_ERROR', () =>
expect(reducer(undefined, { type: LIST_SHORT_URLS_ERROR } as any)).toEqual({ expect(reducer(undefined, { type: listShortUrls.rejected.toString() })).toEqual({
loading: false, loading: false,
error: true, error: true,
})); }));
@ -52,7 +59,7 @@ describe('shortUrlsListReducer', () => {
error: false, error: false,
}; };
expect(reducer(state, { type: `${SHORT_URL_DELETED}/fulfilled`, payload: { shortCode } } as any)).toEqual({ expect(reducer(state, { type: deleteShortUrl.fulfilled.toString(), payload: { shortCode } })).toEqual({
shortUrls: { shortUrls: {
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }], data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
pagination: { totalItems: 9 }, pagination: { totalItems: 9 },
@ -85,7 +92,7 @@ describe('shortUrlsListReducer', () => {
error: false, error: false,
}; };
expect(reducer(state, { type: createNewVisits.toString(), payload: { createdVisits } } as any)).toEqual({ expect(reducer(state, { type: createNewVisits.toString(), payload: { createdVisits } })).toEqual({
shortUrls: { shortUrls: {
data: [ data: [
{ shortCode, domain: 'example.com', visitsCount: 5 }, { shortCode, domain: 'example.com', visitsCount: 5 },
@ -142,7 +149,7 @@ describe('shortUrlsListReducer', () => {
error: false, error: false,
}; };
expect(reducer(state, { type: `${CREATE_SHORT_URL}/fulfilled`, payload: newShortUrl } as any)).toEqual({ expect(reducer(state, { type: createShortUrl.fulfilled.toString(), payload: newShortUrl })).toEqual({
shortUrls: { shortUrls: {
data: expectedData, data: expectedData,
pagination: { totalItems: 16 }, pagination: { totalItems: 16 },
@ -181,7 +188,7 @@ describe('shortUrlsListReducer', () => {
error: false, error: false,
}; };
const result = reducer(state, { type: `${SHORT_URL_EDITED}/fulfilled`, payload: editedShortUrl } as any); const result = reducer(state, { type: editShortUrl.fulfilled.toString(), payload: editedShortUrl });
expect(result.shortUrls?.data).toEqual(expectedList); expect(result.shortUrls?.data).toEqual(expectedList);
}); });
@ -191,30 +198,29 @@ describe('shortUrlsListReducer', () => {
const dispatch = jest.fn(); const dispatch = jest.fn();
const getState = jest.fn().mockReturnValue({ selectedServer: {} }); const getState = jest.fn().mockReturnValue({ selectedServer: {} });
afterEach(jest.clearAllMocks);
it('dispatches proper actions if API client request succeeds', async () => { it('dispatches proper actions if API client request succeeds', async () => {
const listShortUrlsMock = jest.fn().mockResolvedValue({}); listShortUrlsMock.mockResolvedValue({});
const apiClientMock = Mock.of<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
await listShortUrls(() => apiClientMock)()(dispatch, getState); await listShortUrls()(dispatch, getState, {});
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START }); expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: listShortUrls.pending.toString() }));
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS, payload: {} }); expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({
type: listShortUrls.fulfilled.toString(),
payload: {},
}));
expect(listShortUrlsMock).toHaveBeenCalledTimes(1); expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
}); });
it('dispatches proper actions if API client request fails', async () => { it('dispatches proper actions if API client request fails', async () => {
const listShortUrlsMock = jest.fn().mockRejectedValue(undefined); listShortUrlsMock.mockRejectedValue(undefined);
const apiClientMock = Mock.of<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
await listShortUrls(() => apiClientMock)()(dispatch, getState); await listShortUrls()(dispatch, getState, {});
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START }); expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: listShortUrls.pending.toString() }));
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS_ERROR }); expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: listShortUrls.rejected.toString() }));
expect(listShortUrlsMock).toHaveBeenCalledTimes(1); expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
}); });

View file

@ -3,7 +3,7 @@ import {
TagsList, TagsList,
filterTags, filterTags,
listTags as listTagsCreator, listTags as listTagsCreator,
reducer as reducerCreator, tagsListReducerCreator,
} from '../../../src/tags/reducers/tagsList'; } from '../../../src/tags/reducers/tagsList';
import { ShlinkState } from '../../../src/container/types'; import { ShlinkState } from '../../../src/container/types';
import { ShortUrl } from '../../../src/short-urls/data'; import { ShortUrl } from '../../../src/short-urls/data';
@ -16,7 +16,7 @@ describe('tagsListReducer', () => {
const buildShlinkApiClient = jest.fn(); const buildShlinkApiClient = jest.fn();
const listTags = listTagsCreator(buildShlinkApiClient, true); const listTags = listTagsCreator(buildShlinkApiClient, true);
const createShortUrl = createShortUrlCreator(buildShlinkApiClient); const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
const reducer = reducerCreator(listTags, createShortUrl); const { reducer } = tagsListReducerCreator(listTags, createShortUrl);
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);