From 5ecc791b38ecbf77a5d7c388e0c3e8f5e62d5eb6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 21:32:19 +0100 Subject: [PATCH 1/8] Ensured tags list is not updated until the edit modal is closed --- src/tags/helpers/EditTagModal.tsx | 7 ++++--- src/tags/reducers/tagEdit.ts | 16 ++++++++-------- test/tags/helpers/EditTagModal.test.tsx | 8 +++----- test/tags/reducers/tagEdit.test.ts | 9 ++++----- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/tags/helpers/EditTagModal.tsx b/src/tags/helpers/EditTagModal.tsx index 4b6af3d7..55b5ccb6 100644 --- a/src/tags/helpers/EditTagModal.tsx +++ b/src/tags/helpers/EditTagModal.tsx @@ -1,3 +1,4 @@ +import { pipe } from 'ramda'; import { useState } from 'react'; import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader, Popover, InputGroup } from 'reactstrap'; import { HexColorPicker } from 'react-colorful'; @@ -24,16 +25,16 @@ export const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( const [newTagName, setNewTagName] = useState(tag); const [color, setColor] = useState(getColorForKey(tag)); const [showColorPicker, toggleColorPicker, , hideColorPicker] = useToggle(); - const { editing, error, errorData } = tagEdit; + const { editing, error, edited, errorData } = tagEdit; const saveTag = handleEventPreventingDefault( async () => editTag(tag, newTagName, color) - .then(() => tagEdited(tag, newTagName, color)) .then(toggle) .catch(() => {}), ); + const onClosed = pipe(hideColorPicker, () => edited && tagEdited(tag, newTagName, color)); return ( - +
Edit tag diff --git a/src/tags/reducers/tagEdit.ts b/src/tags/reducers/tagEdit.ts index b3582f62..19a8599f 100644 --- a/src/tags/reducers/tagEdit.ts +++ b/src/tags/reducers/tagEdit.ts @@ -11,13 +11,13 @@ import { ProblemDetailsError } from '../../api/types/errors'; export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START'; export const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR'; export const EDIT_TAG = 'shlink/editTag/EDIT_TAG'; - export const TAG_EDITED = 'shlink/editTag/TAG_EDITED'; export interface TagEdition { - oldName: string; - newName: string; + oldName?: string; + newName?: string; editing: boolean; + edited: boolean; error: boolean; errorData?: ProblemDetailsError; } @@ -29,18 +29,18 @@ export interface EditTagAction extends Action { } const initialState: TagEdition = { - oldName: '', - newName: '', editing: false, + edited: false, error: false, }; export default buildReducer({ - [EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }), - [EDIT_TAG_ERROR]: (state, { errorData }) => ({ ...state, editing: false, error: true, errorData }), + [EDIT_TAG_START]: () => ({ editing: true, edited: false, error: false }), + [EDIT_TAG_ERROR]: (_, { errorData }) => ({ editing: false, edited: false, error: true, errorData }), [EDIT_TAG]: (_, action) => ({ ...pick(['oldName', 'newName'], action), editing: false, + edited: true, error: false, }), }, initialState); @@ -56,7 +56,7 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener try { await shlinkEditTag(oldName, newName); colorGenerator.setColorForKey(newName, color); - dispatch({ type: EDIT_TAG, oldName, newName }); + dispatch({ type: EDIT_TAG, oldName, newName, color }); } catch (e: any) { dispatch({ type: EDIT_TAG_ERROR, errorData: parseApiError(e) }); diff --git a/test/tags/helpers/EditTagModal.test.tsx b/test/tags/helpers/EditTagModal.test.tsx index 46820437..b4bc1dbf 100644 --- a/test/tags/helpers/EditTagModal.test.tsx +++ b/test/tags/helpers/EditTagModal.test.tsx @@ -9,12 +9,11 @@ import { ProblemDetailsError } from '../../../src/api/types/errors'; describe('', () => { const EditTagModal = createEditTagModal(Mock.of({ getColorForKey: jest.fn(() => 'green') })); const editTag = jest.fn().mockReturnValue(Promise.resolve()); - const tagEdited = jest.fn().mockReturnValue(Promise.resolve()); const toggle = jest.fn(); const setUp = (tagEdit: Partial = {}) => { const edition = Mock.of(tagEdit); return renderWithEvents( - , + , ); }; @@ -30,7 +29,6 @@ describe('', () => { expect(toggle).toHaveBeenCalledTimes(2); expect(editTag).not.toHaveBeenCalled(); - expect(tagEdited).not.toHaveBeenCalled(); }); it.each([ @@ -63,12 +61,12 @@ describe('', () => { const { user } = setUp(); expect(editTag).not.toHaveBeenCalled(); - expect(tagEdited).not.toHaveBeenCalled(); + expect(toggle).not.toHaveBeenCalled(); await user.click(screen.getByRole('button', { name: 'Save' })); expect(editTag).toHaveBeenCalled(); - expect(tagEdited).toHaveBeenCalled(); + expect(toggle).toHaveBeenCalled(); }); it('changes color when changing on color picker', async () => { diff --git a/test/tags/reducers/tagEdit.test.ts b/test/tags/reducers/tagEdit.test.ts index d000b343..994b5b3a 100644 --- a/test/tags/reducers/tagEdit.test.ts +++ b/test/tags/reducers/tagEdit.test.ts @@ -21,24 +21,23 @@ describe('tagEditReducer', () => { it('returns loading on EDIT_TAG_START', () => { expect(reducer(undefined, Mock.of({ type: EDIT_TAG_START }))).toEqual({ editing: true, + edited: false, error: false, - oldName: '', - newName: '', }); }); it('returns error on EDIT_TAG_ERROR', () => { expect(reducer(undefined, Mock.of({ type: EDIT_TAG_ERROR }))).toEqual({ editing: false, + edited: false, error: true, - oldName: '', - newName: '', }); }); it('returns tag names on EDIT_TAG', () => { expect(reducer(undefined, { type: EDIT_TAG, oldName, newName, color })).toEqual({ editing: false, + edited: true, error: false, oldName: 'foo', newName: 'bar', @@ -82,7 +81,7 @@ describe('tagEditReducer', () => { expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_TAG_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_TAG, oldName, newName }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_TAG, oldName, newName, color }); }); it('throws on error', async () => { From f8fc1245ca74f4d5226854f0dccb977ca2f35317 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 21:45:33 +0100 Subject: [PATCH 2/8] Migrated editTag and tagEdited actions to use payload --- src/tags/reducers/tagEdit.ts | 24 ++++++++++++++++-------- src/tags/reducers/tagsList.ts | 6 +++--- test/tags/reducers/tagEdit.test.ts | 18 +++++++++++++----- test/tags/reducers/tagsList.test.ts | 8 +++++++- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/tags/reducers/tagEdit.ts b/src/tags/reducers/tagEdit.ts index 19a8599f..177387e7 100644 --- a/src/tags/reducers/tagEdit.ts +++ b/src/tags/reducers/tagEdit.ts @@ -1,5 +1,6 @@ import { pick } from 'ramda'; -import { Action, Dispatch } from 'redux'; +import { PayloadAction } from '@reduxjs/toolkit'; +import { Dispatch } from 'redux'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; import { ColorGenerator } from '../../utils/services/ColorGenerator'; @@ -22,12 +23,14 @@ export interface TagEdition { errorData?: ProblemDetailsError; } -export interface EditTagAction extends Action { +interface EditTag { oldName: string; newName: string; color: string; } +export type EditTagAction = PayloadAction; + const initialState: TagEdition = { editing: false, edited: false, @@ -37,8 +40,8 @@ const initialState: TagEdition = { export default buildReducer({ [EDIT_TAG_START]: () => ({ editing: true, edited: false, error: false }), [EDIT_TAG_ERROR]: (_, { errorData }) => ({ editing: false, edited: false, error: true, errorData }), - [EDIT_TAG]: (_, action) => ({ - ...pick(['oldName', 'newName'], action), + [EDIT_TAG]: (_, { payload }) => ({ + ...pick(['oldName', 'newName'], payload), editing: false, edited: true, error: false, @@ -56,7 +59,10 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener try { await shlinkEditTag(oldName, newName); colorGenerator.setColorForKey(newName, color); - dispatch({ type: EDIT_TAG, oldName, newName, color }); + dispatch({ + type: EDIT_TAG, + payload: { oldName, newName, color }, + }); } catch (e: any) { dispatch({ type: EDIT_TAG_ERROR, errorData: parseApiError(e) }); @@ -66,7 +72,9 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener export const tagEdited = (oldName: string, newName: string, color: string): EditTagAction => ({ type: TAG_EDITED, - oldName, - newName, - color, + payload: { + oldName, + newName, + color, + }, }); diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index 485b55f4..89e56f78 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -90,10 +90,10 @@ export default buildReducer({ tags: rejectTag(state.tags, tag), filteredTags: rejectTag(state.filteredTags, tag), }), - [TAG_EDITED]: (state, { oldName, newName }) => ({ + [TAG_EDITED]: (state, { payload }) => ({ ...state, - tags: state.tags.map(renameTag(oldName, newName)).sort(), - filteredTags: state.filteredTags.map(renameTag(oldName, newName)).sort(), + tags: state.tags.map(renameTag(payload.oldName, payload.newName)).sort(), + filteredTags: state.filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(), }), [FILTER_TAGS]: (state, { searchTerm }) => ({ ...state, diff --git a/test/tags/reducers/tagEdit.test.ts b/test/tags/reducers/tagEdit.test.ts index 994b5b3a..5f168611 100644 --- a/test/tags/reducers/tagEdit.test.ts +++ b/test/tags/reducers/tagEdit.test.ts @@ -35,7 +35,10 @@ describe('tagEditReducer', () => { }); it('returns tag names on EDIT_TAG', () => { - expect(reducer(undefined, { type: EDIT_TAG, oldName, newName, color })).toEqual({ + expect(reducer(undefined, { + type: EDIT_TAG, + payload: { oldName, newName, color }, + })).toEqual({ editing: false, edited: true, error: false, @@ -49,9 +52,11 @@ describe('tagEditReducer', () => { it('returns action based on provided params', () => expect(tagEdited('foo', 'bar', '#ff0000')).toEqual({ type: TAG_EDITED, - oldName: 'foo', - newName: 'bar', - color: '#ff0000', + payload: { + oldName: 'foo', + newName: 'bar', + color: '#ff0000', + }, })); }); @@ -81,7 +86,10 @@ describe('tagEditReducer', () => { expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_TAG_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_TAG, oldName, newName, color }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: EDIT_TAG, + payload: { oldName, newName, color }, + }); }); it('throws on error', async () => { diff --git a/test/tags/reducers/tagsList.test.ts b/test/tags/reducers/tagsList.test.ts index 277d3c28..5a40dd54 100644 --- a/test/tags/reducers/tagsList.test.ts +++ b/test/tags/reducers/tagsList.test.ts @@ -60,7 +60,13 @@ describe('tagsListReducer', () => { const newName = 'renamed'; const expectedTags = ['foo', 'renamed', 'baz'].sort(); - expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_EDITED, oldName, newName } as any)).toEqual({ + expect(reducer( + state({ tags, filteredTags: tags }), + { + type: TAG_EDITED, + payload: { oldName, newName }, + } as any, + )).toEqual({ tags: expectedTags, filteredTags: expectedTags, }); From 648744f440ba868adb3636aa76b8bc8edfe9366a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 21:57:01 +0100 Subject: [PATCH 3/8] Migrated tag actions to have a single DTO param --- src/tags/helpers/EditTagModal.tsx | 10 +++++----- src/tags/reducers/tagEdit.ts | 17 ++++------------- src/tags/reducers/tagsList.ts | 4 ++-- test/tags/reducers/tagEdit.test.ts | 6 +++--- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/tags/helpers/EditTagModal.tsx b/src/tags/helpers/EditTagModal.tsx index 55b5ccb6..59297b6c 100644 --- a/src/tags/helpers/EditTagModal.tsx +++ b/src/tags/helpers/EditTagModal.tsx @@ -8,15 +8,15 @@ import { useToggle } from '../../utils/helpers/hooks'; import { handleEventPreventingDefault } from '../../utils/utils'; import { ColorGenerator } from '../../utils/services/ColorGenerator'; import { TagModalProps } from '../data'; -import { TagEdition } from '../reducers/tagEdit'; +import { EditTag, TagEdition } from '../reducers/tagEdit'; import { Result } from '../../utils/Result'; import { ShlinkApiError } from '../../api/ShlinkApiError'; import './EditTagModal.scss'; interface EditTagModalProps extends TagModalProps { tagEdit: TagEdition; - editTag: (oldName: string, newName: string, color: string) => Promise; - tagEdited: (oldName: string, newName: string, color: string) => void; + editTag: (editTag: EditTag) => Promise; + tagEdited: (tagEdited: EditTag) => void; } export const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( @@ -27,11 +27,11 @@ export const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( const [showColorPicker, toggleColorPicker, , hideColorPicker] = useToggle(); const { editing, error, edited, errorData } = tagEdit; const saveTag = handleEventPreventingDefault( - async () => editTag(tag, newTagName, color) + async () => editTag({ oldName: tag, newName: newTagName, color }) .then(toggle) .catch(() => {}), ); - const onClosed = pipe(hideColorPicker, () => edited && tagEdited(tag, newTagName, color)); + const onClosed = pipe(hideColorPicker, () => edited && tagEdited({ oldName: tag, newName: newTagName, color })); return ( diff --git a/src/tags/reducers/tagEdit.ts b/src/tags/reducers/tagEdit.ts index 177387e7..b8e9e6ea 100644 --- a/src/tags/reducers/tagEdit.ts +++ b/src/tags/reducers/tagEdit.ts @@ -1,5 +1,5 @@ import { pick } from 'ramda'; -import { PayloadAction } from '@reduxjs/toolkit'; +import { createAction, PayloadAction } from '@reduxjs/toolkit'; import { Dispatch } from 'redux'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; @@ -23,7 +23,7 @@ export interface TagEdition { errorData?: ProblemDetailsError; } -interface EditTag { +export interface EditTag { oldName: string; newName: string; color: string; @@ -49,9 +49,7 @@ export default buildReducer({ }, initialState); export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => ( - oldName: string, - newName: string, - color: string, + { oldName, newName, color }: EditTag, ) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: EDIT_TAG_START }); const { editTag: shlinkEditTag } = buildShlinkApiClient(getState); @@ -70,11 +68,4 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener } }; -export const tagEdited = (oldName: string, newName: string, color: string): EditTagAction => ({ - type: TAG_EDITED, - payload: { - oldName, - newName, - color, - }, -}); +export const tagEdited = createAction(TAG_EDITED); diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index 89e56f78..ae52954e 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -11,7 +11,7 @@ import { TagStats } from '../data'; import { ApiErrorAction } from '../../api/types/actions'; import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducers/shortUrlCreation'; import { DeleteTagAction, TAG_DELETED } from './tagDelete'; -import { EditTagAction, TAG_EDITED } from './tagEdit'; +import { EditTagAction, tagEdited } from './tagEdit'; import { ProblemDetailsError } from '../../api/types/errors'; export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; @@ -90,7 +90,7 @@ export default buildReducer({ tags: rejectTag(state.tags, tag), filteredTags: rejectTag(state.filteredTags, tag), }), - [TAG_EDITED]: (state, { payload }) => ({ + [tagEdited.toString()]: (state, { payload }) => ({ ...state, tags: state.tags.map(renameTag(payload.oldName, payload.newName)).sort(), filteredTags: state.filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(), diff --git a/test/tags/reducers/tagEdit.test.ts b/test/tags/reducers/tagEdit.test.ts index 5f168611..c7707480 100644 --- a/test/tags/reducers/tagEdit.test.ts +++ b/test/tags/reducers/tagEdit.test.ts @@ -50,7 +50,7 @@ describe('tagEditReducer', () => { describe('tagEdited', () => { it('returns action based on provided params', () => - expect(tagEdited('foo', 'bar', '#ff0000')).toEqual({ + expect(tagEdited({ oldName: 'foo', newName: 'bar', color: '#ff0000' })).toEqual({ type: TAG_EDITED, payload: { oldName: 'foo', @@ -74,7 +74,7 @@ describe('tagEditReducer', () => { it('calls API on success', async () => { const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color); + const dispatchable = editTag(() => apiClientMock, colorGenerator)({ oldName, newName, color }); await dispatchable(dispatch, getState); @@ -95,7 +95,7 @@ describe('tagEditReducer', () => { it('throws on error', async () => { const error = 'Error'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color); + const dispatchable = editTag(() => apiClientMock, colorGenerator)({ oldName, newName, color }); try { await dispatchable(dispatch, getState); From 0571a4a88fc5840f1bfa85c7602bff5c82655a30 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 22:12:48 +0100 Subject: [PATCH 4/8] Migrated tagEdit reducer to RTK --- src/reducers/index.ts | 3 +- src/tags/reducers/tagEdit.ts | 77 +++++++++++++--------------- src/tags/services/provideServices.ts | 14 +++-- test/tags/reducers/tagEdit.test.ts | 58 +++++++++------------ test/tags/reducers/tagsList.test.ts | 4 +- 5 files changed, 73 insertions(+), 83 deletions(-) diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 9c412690..dbbeef32 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -10,7 +10,6 @@ import orphanVisitsReducer from '../visits/reducers/orphanVisits'; import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits'; import tagsListReducer from '../tags/reducers/tagsList'; import tagDeleteReducer from '../tags/reducers/tagDelete'; -import tagEditReducer from '../tags/reducers/tagEdit'; import { settingsReducer } from '../settings/reducers/settings'; import visitsOverviewReducer from '../visits/reducers/visitsOverview'; import { appUpdatesReducer } from '../app/reducers/appUpdates'; @@ -32,7 +31,7 @@ export default (container: IContainer) => combineReducers({ nonOrphanVisits: nonOrphanVisitsReducer, tagsList: tagsListReducer, tagDelete: tagDeleteReducer, - tagEdit: tagEditReducer, + tagEdit: container.tagEditReducer, mercureInfo: container.mercureInfoReducer, settings: settingsReducer, domainsList: container.domainsListReducer, diff --git a/src/tags/reducers/tagEdit.ts b/src/tags/reducers/tagEdit.ts index b8e9e6ea..087a8497 100644 --- a/src/tags/reducers/tagEdit.ts +++ b/src/tags/reducers/tagEdit.ts @@ -1,18 +1,13 @@ import { pick } from 'ramda'; -import { createAction, PayloadAction } from '@reduxjs/toolkit'; -import { Dispatch } from 'redux'; -import { buildReducer } from '../../utils/helpers/redux'; -import { GetState } from '../../container/types'; +import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk } from '../../utils/helpers/redux'; import { ColorGenerator } from '../../utils/services/ColorGenerator'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { parseApiError } from '../../api/utils'; -import { ApiErrorAction } from '../../api/types/actions'; import { ProblemDetailsError } from '../../api/types/errors'; -export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START'; -export const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR'; -export const EDIT_TAG = 'shlink/editTag/EDIT_TAG'; -export const TAG_EDITED = 'shlink/editTag/TAG_EDITED'; +const EDIT_TAG = 'shlink/editTag/EDIT_TAG'; +const TAG_EDITED = 'shlink/editTag/TAG_EDITED'; export interface TagEdition { oldName?: string; @@ -37,35 +32,37 @@ const initialState: TagEdition = { error: false, }; -export default buildReducer({ - [EDIT_TAG_START]: () => ({ editing: true, edited: false, error: false }), - [EDIT_TAG_ERROR]: (_, { errorData }) => ({ editing: false, edited: false, error: true, errorData }), - [EDIT_TAG]: (_, { payload }) => ({ - ...pick(['oldName', 'newName'], payload), - editing: false, - edited: true, - error: false, - }), -}, initialState); - -export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => ( - { oldName, newName, color }: EditTag, -) => async (dispatch: Dispatch, getState: GetState) => { - dispatch({ type: EDIT_TAG_START }); - const { editTag: shlinkEditTag } = buildShlinkApiClient(getState); - - try { - await shlinkEditTag(oldName, newName); - colorGenerator.setColorForKey(newName, color); - dispatch({ - type: EDIT_TAG, - payload: { oldName, newName, color }, - }); - } catch (e: any) { - dispatch({ type: EDIT_TAG_ERROR, errorData: parseApiError(e) }); - - throw e; - } -}; - export const tagEdited = createAction(TAG_EDITED); + +export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => { + const editTag = createAsyncThunk( + EDIT_TAG, + async ({ oldName, newName, color }: EditTag, { getState }): Promise => { + await buildShlinkApiClient(getState).editTag(oldName, newName); + colorGenerator.setColorForKey(newName, color); + + return { oldName, newName, color }; + }, + ); + + const { reducer } = createSlice({ + name: 'tagEditReducer', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(editTag.pending, () => ({ editing: true, edited: false, error: false })); + builder.addCase( + editTag.rejected, + (_, { error }) => ({ editing: false, edited: false, error: true, errorData: parseApiError(error) }), + ); + builder.addCase(editTag.fulfilled, (_, { payload }) => ({ + ...pick(['oldName', 'newName'], payload), + editing: false, + edited: true, + error: false, + })); + }, + }); + + return { reducer, editTag }; +}; diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts index e7d58453..04f95289 100644 --- a/src/tags/services/provideServices.ts +++ b/src/tags/services/provideServices.ts @@ -1,3 +1,4 @@ +import { prop } from 'ramda'; import Bottle, { IContainer } from 'bottlejs'; import { TagsSelector } from '../helpers/TagsSelector'; import { TagCard } from '../TagCard'; @@ -6,7 +7,7 @@ import { EditTagModal } from '../helpers/EditTagModal'; import { TagsList } from '../TagsList'; import { filterTags, listTags } from '../reducers/tagsList'; import { deleteTag, tagDeleted } from '../reducers/tagDelete'; -import { editTag, tagEdited } from '../reducers/tagEdit'; +import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit'; import { ConnectDecorator } from '../../container/types'; import { TagsCards } from '../TagsCards'; import { TagsTable } from '../TagsTable'; @@ -36,6 +37,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { ['forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo'], )); + // Reducers + bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'buildShlinkApiClient', 'ColorGenerator'); + bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator'); + // Actions const listTagsActionFactory = (force: boolean) => ({ buildShlinkApiClient }: IContainer) => listTags(buildShlinkApiClient, force); @@ -43,11 +48,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.factory('listTags', listTagsActionFactory(false)); bottle.factory('forceListTags', listTagsActionFactory(true)); bottle.serviceFactory('filterTags', () => filterTags); - bottle.serviceFactory('tagDeleted', () => tagDeleted); - bottle.serviceFactory('tagEdited', () => tagEdited); bottle.serviceFactory('deleteTag', deleteTag, 'buildShlinkApiClient'); - bottle.serviceFactory('editTag', editTag, 'buildShlinkApiClient', 'ColorGenerator'); + bottle.serviceFactory('tagDeleted', () => tagDeleted); + + bottle.serviceFactory('editTag', prop('editTag'), 'tagEditReducerCreator'); + bottle.serviceFactory('tagEdited', () => tagEdited); }; export default provideServices; diff --git a/test/tags/reducers/tagEdit.test.ts b/test/tags/reducers/tagEdit.test.ts index c7707480..7f496dd1 100644 --- a/test/tags/reducers/tagEdit.test.ts +++ b/test/tags/reducers/tagEdit.test.ts @@ -1,13 +1,5 @@ import { Mock } from 'ts-mockery'; -import reducer, { - EDIT_TAG_START, - EDIT_TAG_ERROR, - EDIT_TAG, - TAG_EDITED, - tagEdited, - editTag, - EditTagAction, -} from '../../../src/tags/reducers/tagEdit'; +import { tagEdited, EditTagAction, tagEditReducerCreator } from '../../../src/tags/reducers/tagEdit'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import { ShlinkState } from '../../../src/container/types'; @@ -16,10 +8,14 @@ describe('tagEditReducer', () => { const oldName = 'foo'; const newName = 'bar'; const color = '#ff0000'; + const editTagCall = jest.fn(); + const buildShlinkApiClient = () => Mock.of({ editTag: editTagCall }); + const colorGenerator = Mock.of({ setColorForKey: jest.fn() }); + const { reducer, editTag } = tagEditReducerCreator(buildShlinkApiClient, colorGenerator); describe('reducer', () => { it('returns loading on EDIT_TAG_START', () => { - expect(reducer(undefined, Mock.of({ type: EDIT_TAG_START }))).toEqual({ + expect(reducer(undefined, Mock.of({ type: editTag.pending.toString() }))).toEqual({ editing: true, edited: false, error: false, @@ -27,7 +23,7 @@ describe('tagEditReducer', () => { }); it('returns error on EDIT_TAG_ERROR', () => { - expect(reducer(undefined, Mock.of({ type: EDIT_TAG_ERROR }))).toEqual({ + expect(reducer(undefined, Mock.of({ type: editTag.rejected.toString() }))).toEqual({ editing: false, edited: false, error: true, @@ -36,7 +32,7 @@ describe('tagEditReducer', () => { it('returns tag names on EDIT_TAG', () => { expect(reducer(undefined, { - type: EDIT_TAG, + type: editTag.fulfilled.toString(), payload: { oldName, newName, color }, })).toEqual({ editing: false, @@ -51,7 +47,7 @@ describe('tagEditReducer', () => { describe('tagEdited', () => { it('returns action based on provided params', () => expect(tagEdited({ oldName: 'foo', newName: 'bar', color: '#ff0000' })).toEqual({ - type: TAG_EDITED, + type: tagEdited.toString(), payload: { oldName: 'foo', newName: 'bar', @@ -61,56 +57,48 @@ describe('tagEditReducer', () => { }); describe('editTag', () => { - const createApiClientMock = (result: Promise) => Mock.of({ - editTag: jest.fn(async () => result), - }); - const colorGenerator = Mock.of({ - setColorForKey: jest.fn(), - }); const dispatch = jest.fn(); const getState = () => Mock.of(); afterEach(jest.clearAllMocks); it('calls API on success', async () => { - const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = editTag(() => apiClientMock, colorGenerator)({ oldName, newName, color }); + editTagCall.mockResolvedValue(undefined); - await dispatchable(dispatch, getState); + await editTag({ oldName, newName, color })(dispatch, getState, {}); - expect(apiClientMock.editTag).toHaveBeenCalledTimes(1); - expect(apiClientMock.editTag).toHaveBeenCalledWith(oldName, newName); + expect(editTagCall).toHaveBeenCalledTimes(1); + expect(editTagCall).toHaveBeenCalledWith(oldName, newName); expect(colorGenerator.setColorForKey).toHaveBeenCalledTimes(1); expect(colorGenerator.setColorForKey).toHaveBeenCalledWith(newName, color); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_TAG_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { - type: EDIT_TAG, + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: editTag.pending.toString() })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ + type: editTag.fulfilled.toString(), payload: { oldName, newName, color }, - }); + })); }); it('throws on error', async () => { const error = 'Error'; - const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = editTag(() => apiClientMock, colorGenerator)({ oldName, newName, color }); + editTagCall.mockRejectedValue(error); try { - await dispatchable(dispatch, getState); + await editTag({ oldName, newName, color })(dispatch, getState, {}); } catch (e) { expect(e).toEqual(error); } - expect(apiClientMock.editTag).toHaveBeenCalledTimes(1); - expect(apiClientMock.editTag).toHaveBeenCalledWith(oldName, newName); + expect(editTagCall).toHaveBeenCalledTimes(1); + expect(editTagCall).toHaveBeenCalledWith(oldName, newName); expect(colorGenerator.setColorForKey).not.toHaveBeenCalled(); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_TAG_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_TAG_ERROR }); + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: editTag.pending.toString() })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: editTag.rejected.toString() })); }); }); }); diff --git a/test/tags/reducers/tagsList.test.ts b/test/tags/reducers/tagsList.test.ts index 5a40dd54..bce325b0 100644 --- a/test/tags/reducers/tagsList.test.ts +++ b/test/tags/reducers/tagsList.test.ts @@ -9,10 +9,10 @@ import reducer, { TagsList, } from '../../../src/tags/reducers/tagsList'; import { TAG_DELETED } from '../../../src/tags/reducers/tagDelete'; -import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit'; import { ShlinkState } from '../../../src/container/types'; import { ShortUrl } from '../../../src/short-urls/data'; import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation'; +import { tagEdited } from '../../../src/tags/reducers/tagEdit'; describe('tagsListReducer', () => { const state = (props: Partial) => Mock.of(props); @@ -63,7 +63,7 @@ describe('tagsListReducer', () => { expect(reducer( state({ tags, filteredTags: tags }), { - type: TAG_EDITED, + type: tagEdited.toString(), payload: { oldName, newName }, } as any, )).toEqual({ From dbb08a6ce0b6caeccabf56c3e27224e98ba238ec Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 22:19:44 +0100 Subject: [PATCH 5/8] Ensured tags deleted are not removed from list until modal has been hidden --- src/tags/helpers/DeleteTagConfirmModal.tsx | 5 ++--- src/tags/reducers/tagDelete.ts | 8 ++++--- .../helpers/DeleteTagConfirmModal.test.tsx | 21 +++++++++---------- test/tags/reducers/tagDelete.test.ts | 3 +++ 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/tags/helpers/DeleteTagConfirmModal.tsx b/src/tags/helpers/DeleteTagConfirmModal.tsx index 6828af13..fd2cbfdb 100644 --- a/src/tags/helpers/DeleteTagConfirmModal.tsx +++ b/src/tags/helpers/DeleteTagConfirmModal.tsx @@ -13,15 +13,14 @@ interface DeleteTagConfirmModalProps extends TagModalProps { export const DeleteTagConfirmModal = ( { tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps, ) => { - const { deleting, error, errorData } = tagDelete; + const { deleting, error, deleted, errorData } = tagDelete; const doDelete = async () => { await deleteTag(tag); - tagDeleted(tag); toggle(); }; return ( - + deleted && tagDeleted(tag)}> Delete tag Are you sure you want to delete tag {tag}? diff --git a/src/tags/reducers/tagDelete.ts b/src/tags/reducers/tagDelete.ts index f0111660..2c6683d6 100644 --- a/src/tags/reducers/tagDelete.ts +++ b/src/tags/reducers/tagDelete.ts @@ -13,6 +13,7 @@ export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED'; export interface TagDeletion { deleting: boolean; + deleted: boolean; error: boolean; errorData?: ProblemDetailsError; } @@ -23,13 +24,14 @@ export interface DeleteTagAction extends Action { const initialState: TagDeletion = { deleting: false, + deleted: false, error: false, }; export default buildReducer({ - [DELETE_TAG_START]: () => ({ deleting: true, error: false }), - [DELETE_TAG_ERROR]: (_, { errorData }) => ({ deleting: false, error: true, errorData }), - [DELETE_TAG]: () => ({ deleting: false, error: false }), + [DELETE_TAG_START]: () => ({ deleting: true, deleted: false, error: false }), + [DELETE_TAG_ERROR]: (_, { errorData }) => ({ deleting: false, deleted: false, error: true, errorData }), + [DELETE_TAG]: () => ({ deleting: false, deleted: true, error: false }), }, initialState); export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: string) => async ( diff --git a/test/tags/helpers/DeleteTagConfirmModal.test.tsx b/test/tags/helpers/DeleteTagConfirmModal.test.tsx index 8ca667ea..00a7cdf1 100644 --- a/test/tags/helpers/DeleteTagConfirmModal.test.tsx +++ b/test/tags/helpers/DeleteTagConfirmModal.test.tsx @@ -6,14 +6,14 @@ import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const tag = 'nodejs'; const deleteTag = jest.fn(); - const tagDeleted = jest.fn(); + const toggle = jest.fn(); const setUp = (tagDelete: TagDeletion) => renderWithEvents( ''} + toggle={toggle} isOpen deleteTag={deleteTag} - tagDeleted={tagDeleted} + tagDeleted={jest.fn()} tagDelete={tagDelete} />, ); @@ -21,7 +21,7 @@ describe('', () => { afterEach(jest.resetAllMocks); it('asks confirmation for provided tag to be deleted', () => { - setUp({ error: false, deleting: false }); + setUp({ error: false, deleted: false, deleting: false }); const delBtn = screen.getByRole('button', { name: 'Delete tag' }); @@ -33,12 +33,12 @@ describe('', () => { }); it('shows error message when deletion failed', () => { - setUp({ error: true, deleting: false }); + setUp({ error: true, deleted: false, deleting: false }); expect(screen.getByText('Something went wrong while deleting the tag :(')).toBeInTheDocument(); }); it('shows loading status while deleting', () => { - setUp({ error: false, deleting: true }); + setUp({ error: false, deleted: false, deleting: true }); const delBtn = screen.getByRole('button', { name: 'Deleting tag...' }); @@ -48,22 +48,21 @@ describe('', () => { }); it('hides tag modal when btn is clicked', async () => { - const { user } = setUp({ error: false, deleting: false }); + const { user } = setUp({ error: false, deleted: true, deleting: false }); await user.click(screen.getByRole('button', { name: 'Delete tag' })); expect(deleteTag).toHaveBeenCalledTimes(1); expect(deleteTag).toHaveBeenCalledWith(tag); - expect(tagDeleted).toHaveBeenCalledTimes(1); - expect(tagDeleted).toHaveBeenCalledWith(tag); + expect(toggle).toHaveBeenCalledTimes(1); }); it('does no further actions when modal is closed without deleting tag', async () => { - const { user } = setUp({ error: false, deleting: false }); + const { user } = setUp({ error: false, deleted: true, deleting: false }); await user.click(screen.getByLabelText('Close')); expect(deleteTag).not.toHaveBeenCalled(); - expect(tagDeleted).not.toHaveBeenCalled(); + expect(toggle).toHaveBeenCalled(); }); }); diff --git a/test/tags/reducers/tagDelete.test.ts b/test/tags/reducers/tagDelete.test.ts index a4726bf1..cb05952c 100644 --- a/test/tags/reducers/tagDelete.test.ts +++ b/test/tags/reducers/tagDelete.test.ts @@ -15,6 +15,7 @@ describe('tagDeleteReducer', () => { it('returns loading on DELETE_TAG_START', () => { expect(reducer(undefined, { type: DELETE_TAG_START })).toEqual({ deleting: true, + deleted: false, error: false, }); }); @@ -22,6 +23,7 @@ describe('tagDeleteReducer', () => { it('returns error on DELETE_TAG_ERROR', () => { expect(reducer(undefined, { type: DELETE_TAG_ERROR })).toEqual({ deleting: false, + deleted: false, error: true, }); }); @@ -29,6 +31,7 @@ describe('tagDeleteReducer', () => { it('returns tag names on DELETE_TAG', () => { expect(reducer(undefined, { type: DELETE_TAG })).toEqual({ deleting: false, + deleted: true, error: false, }); }); From 22b3794154d47d64d1a8668eaf5dd19c9f679ab6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 22:22:44 +0100 Subject: [PATCH 6/8] Migrated tagDeleted action to payload --- src/tags/reducers/tagDelete.ts | 9 ++++----- src/tags/reducers/tagsList.ts | 2 +- test/tags/reducers/tagDelete.test.ts | 2 +- test/tags/reducers/tagsList.test.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/tags/reducers/tagDelete.ts b/src/tags/reducers/tagDelete.ts index 2c6683d6..ec5e3ebe 100644 --- a/src/tags/reducers/tagDelete.ts +++ b/src/tags/reducers/tagDelete.ts @@ -1,4 +1,5 @@ -import { Action, Dispatch } from 'redux'; +import { createAction, PayloadAction } from '@reduxjs/toolkit'; +import { Dispatch } from 'redux'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; @@ -18,9 +19,7 @@ export interface TagDeletion { errorData?: ProblemDetailsError; } -export interface DeleteTagAction extends Action { - tag: string; -} +export type DeleteTagAction = PayloadAction; const initialState: TagDeletion = { deleting: false, @@ -51,4 +50,4 @@ export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: } }; -export const tagDeleted = (tag: string): DeleteTagAction => ({ type: TAG_DELETED, tag }); +export const tagDeleted = createAction(TAG_DELETED); diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index ae52954e..b954e04d 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -85,7 +85,7 @@ export default buildReducer({ [LIST_TAGS_START]: () => ({ ...initialState, loading: true }), [LIST_TAGS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), [LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }), - [TAG_DELETED]: (state, { tag }) => ({ + [TAG_DELETED]: (state, { payload: tag }) => ({ ...state, tags: rejectTag(state.tags, tag), filteredTags: rejectTag(state.filteredTags, tag), diff --git a/test/tags/reducers/tagDelete.test.ts b/test/tags/reducers/tagDelete.test.ts index cb05952c..c87e2bfd 100644 --- a/test/tags/reducers/tagDelete.test.ts +++ b/test/tags/reducers/tagDelete.test.ts @@ -41,7 +41,7 @@ describe('tagDeleteReducer', () => { it('returns action based on provided params', () => expect(tagDeleted('foo')).toEqual({ type: TAG_DELETED, - tag: 'foo', + payload: 'foo', })); }); diff --git a/test/tags/reducers/tagsList.test.ts b/test/tags/reducers/tagsList.test.ts index bce325b0..1fcb390e 100644 --- a/test/tags/reducers/tagsList.test.ts +++ b/test/tags/reducers/tagsList.test.ts @@ -48,7 +48,7 @@ describe('tagsListReducer', () => { const tag = 'foo'; const expectedTags = ['bar', 'baz']; - expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_DELETED, tag } as any)).toEqual({ + expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_DELETED, payload: tag } as any)).toEqual({ tags: expectedTags, filteredTags: expectedTags, }); From 692eaf7dc9f46d418a357a20349bb46e458ef7fe Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 22:29:15 +0100 Subject: [PATCH 7/8] Referenced createNewVisits action directly, instead of its action type --- src/short-urls/reducers/shortUrlsList.ts | 4 ++-- src/tags/reducers/tagsList.ts | 4 ++-- src/visits/reducers/domainVisits.ts | 4 ++-- src/visits/reducers/nonOrphanVisits.ts | 4 ++-- src/visits/reducers/orphanVisits.ts | 4 ++-- src/visits/reducers/shortUrlVisits.ts | 4 ++-- src/visits/reducers/tagVisits.ts | 4 ++-- src/visits/reducers/visitCreation.ts | 2 +- src/visits/reducers/visitsOverview.ts | 4 ++-- test/short-urls/reducers/shortUrlsList.test.ts | 4 ++-- test/visits/reducers/domainVisits.test.ts | 4 ++-- test/visits/reducers/nonOrphanVisits.test.ts | 4 ++-- test/visits/reducers/orphanVisits.test.ts | 4 ++-- test/visits/reducers/shortUrlVisits.test.ts | 4 ++-- test/visits/reducers/tagVisits.test.ts | 4 ++-- test/visits/reducers/visitCreation.test.ts | 4 ++-- test/visits/reducers/visitsOverview.test.ts | 4 ++-- 17 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts index 54b2b4cc..3e492bff 100644 --- a/src/short-urls/reducers/shortUrlsList.ts +++ b/src/short-urls/reducers/shortUrlsList.ts @@ -1,7 +1,7 @@ import { assoc, assocPath, last, pipe, reject } from 'ramda'; import { Action, Dispatch } from 'redux'; import { shortUrlMatches } from '../helpers'; -import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation'; +import { createNewVisits, CreateVisitsAction } from '../../visits/reducers/visitCreation'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; @@ -57,7 +57,7 @@ export default buildReducer({ state, )), ), - [CREATE_VISITS]: (state, { payload }) => assocPath( + [createNewVisits.toString()]: (state, { payload }) => assocPath( ['shortUrls', 'data'], state.shortUrls?.data?.map( (currentShortUrl) => { diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index b954e04d..c4b25da7 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -1,6 +1,6 @@ import { isEmpty, reject } from 'ramda'; import { Action, Dispatch } from 'redux'; -import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation'; +import { createNewVisits, CreateVisitsAction } from '../../visits/reducers/visitCreation'; import { buildReducer } from '../../utils/helpers/redux'; import { ShlinkTags } from '../../api/types'; import { GetState } from '../../container/types'; @@ -99,7 +99,7 @@ export default buildReducer({ ...state, filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm.toLowerCase())), }), - [CREATE_VISITS]: (state, { payload }) => ({ + [createNewVisits.toString()]: (state, { payload }) => ({ ...state, stats: increaseVisitsForTags(calculateVisitsPerTag(payload.createdVisits), state.stats), }), diff --git a/src/visits/reducers/domainVisits.ts b/src/visits/reducers/domainVisits.ts index 72f83300..eb2c80fe 100644 --- a/src/visits/reducers/domainVisits.ts +++ b/src/visits/reducers/domainVisits.ts @@ -7,7 +7,7 @@ import { ShlinkVisitsParams } from '../../api/types'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; -import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; +import { createNewVisits, CreateVisitsAction } from './visitCreation'; import { domainMatches } from '../../short-urls/helpers'; export const GET_DOMAIN_VISITS_START = 'shlink/domainVisits/GET_DOMAIN_VISITS_START'; @@ -56,7 +56,7 @@ export default buildReducer({ [GET_DOMAIN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_DOMAIN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), [GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), - [CREATE_VISITS]: (state, { payload }) => { + [createNewVisits.toString()]: (state, { payload }) => { const { domain, visits, query = {} } = state; const { startDate, endDate } = query; const newVisits = payload.createdVisits diff --git a/src/visits/reducers/nonOrphanVisits.ts b/src/visits/reducers/nonOrphanVisits.ts index 84e2555f..94c037fc 100644 --- a/src/visits/reducers/nonOrphanVisits.ts +++ b/src/visits/reducers/nonOrphanVisits.ts @@ -12,7 +12,7 @@ import { ShlinkVisitsParams } from '../../api/types'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; -import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; +import { createNewVisits, CreateVisitsAction } from './visitCreation'; export const GET_NON_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_START'; export const GET_NON_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_ERROR'; @@ -52,7 +52,7 @@ export default buildReducer({ [GET_NON_ORPHAN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), [GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), - [CREATE_VISITS]: (state, { payload }) => { + [createNewVisits.toString()]: (state, { payload }) => { const { visits, query = {} } = state; const { startDate, endDate } = query; const newVisits = payload.createdVisits diff --git a/src/visits/reducers/orphanVisits.ts b/src/visits/reducers/orphanVisits.ts index 27023458..ecbe832d 100644 --- a/src/visits/reducers/orphanVisits.ts +++ b/src/visits/reducers/orphanVisits.ts @@ -15,7 +15,7 @@ import { isOrphanVisit } from '../types/helpers'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; -import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; +import { createNewVisits, CreateVisitsAction } from './visitCreation'; export const GET_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_ORPHAN_VISITS_START'; export const GET_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_ORPHAN_VISITS_ERROR'; @@ -55,7 +55,7 @@ export default buildReducer({ [GET_ORPHAN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_ORPHAN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), [GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), - [CREATE_VISITS]: (state, { payload }) => { + [createNewVisits.toString()]: (state, { payload }) => { const { visits, query = {} } = state; const { startDate, endDate } = query; const newVisits = payload.createdVisits diff --git a/src/visits/reducers/shortUrlVisits.ts b/src/visits/reducers/shortUrlVisits.ts index 2bfe505e..c768d8b5 100644 --- a/src/visits/reducers/shortUrlVisits.ts +++ b/src/visits/reducers/shortUrlVisits.ts @@ -9,7 +9,7 @@ import { ShlinkVisitsParams } from '../../api/types'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; -import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; +import { createNewVisits, CreateVisitsAction } from './visitCreation'; export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR'; @@ -60,7 +60,7 @@ export default buildReducer({ [GET_SHORT_URL_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_SHORT_URL_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), [GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), - [CREATE_VISITS]: (state, { payload }) => { + [createNewVisits.toString()]: (state, { payload }) => { const { shortCode, domain, visits, query = {} } = state; const { startDate, endDate } = query; const newVisits = payload.createdVisits diff --git a/src/visits/reducers/tagVisits.ts b/src/visits/reducers/tagVisits.ts index 07372ea1..65b0bfc8 100644 --- a/src/visits/reducers/tagVisits.ts +++ b/src/visits/reducers/tagVisits.ts @@ -7,7 +7,7 @@ import { ShlinkVisitsParams } from '../../api/types'; import { ApiErrorAction } from '../../api/types/actions'; import { isBetween } from '../../utils/helpers/date'; import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; -import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; +import { createNewVisits, CreateVisitsAction } from './visitCreation'; export const GET_TAG_VISITS_START = 'shlink/tagVisits/GET_TAG_VISITS_START'; export const GET_TAG_VISITS_ERROR = 'shlink/tagVisits/GET_TAG_VISITS_ERROR'; @@ -53,7 +53,7 @@ export default buildReducer({ [GET_TAG_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_TAG_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), [GET_TAG_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), - [CREATE_VISITS]: (state, { payload }) => { + [createNewVisits.toString()]: (state, { payload }) => { const { tag, visits, query = {} } = state; const { startDate, endDate } = query; const newVisits = payload.createdVisits diff --git a/src/visits/reducers/visitCreation.ts b/src/visits/reducers/visitCreation.ts index a14a339a..3f7f9137 100644 --- a/src/visits/reducers/visitCreation.ts +++ b/src/visits/reducers/visitCreation.ts @@ -1,7 +1,7 @@ import { createAction, PayloadAction } from '@reduxjs/toolkit'; import { CreateVisit } from '../types'; -export const CREATE_VISITS = 'shlink/visitCreation/CREATE_VISITS'; +const CREATE_VISITS = 'shlink/visitCreation/CREATE_VISITS'; export type CreateVisitsAction = PayloadAction<{ createdVisits: CreateVisit[]; diff --git a/src/visits/reducers/visitsOverview.ts b/src/visits/reducers/visitsOverview.ts index a9a0ff9a..824d7f5f 100644 --- a/src/visits/reducers/visitsOverview.ts +++ b/src/visits/reducers/visitsOverview.ts @@ -4,7 +4,7 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde import { GetState } from '../../container/types'; import { buildReducer } from '../../utils/helpers/redux'; import { groupNewVisitsByType } from '../types/helpers'; -import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; +import { createNewVisits, CreateVisitsAction } from './visitCreation'; export const GET_OVERVIEW_START = 'shlink/visitsOverview/GET_OVERVIEW_START'; export const GET_OVERVIEW_ERROR = 'shlink/visitsOverview/GET_OVERVIEW_ERROR'; @@ -30,7 +30,7 @@ export default buildReducer ({ ...initialState, loading: true }), [GET_OVERVIEW_ERROR]: () => ({ ...initialState, error: true }), [GET_OVERVIEW]: (_, { visitsCount, orphanVisitsCount }) => ({ ...initialState, visitsCount, orphanVisitsCount }), - [CREATE_VISITS]: ({ visitsCount, orphanVisitsCount = 0, ...rest }, { payload }) => { + [createNewVisits.toString()]: ({ visitsCount, orphanVisitsCount = 0, ...rest }, { payload }) => { const { regularVisits, orphanVisits } = groupNewVisitsByType(payload.createdVisits); return { diff --git a/test/short-urls/reducers/shortUrlsList.test.ts b/test/short-urls/reducers/shortUrlsList.test.ts index 05b500ea..3eccb8d8 100644 --- a/test/short-urls/reducers/shortUrlsList.test.ts +++ b/test/short-urls/reducers/shortUrlsList.test.ts @@ -6,12 +6,12 @@ import reducer, { listShortUrls, } from '../../../src/short-urls/reducers/shortUrlsList'; import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion'; -import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { ShortUrl } from '../../../src/short-urls/data'; 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', () => { const shortCode = 'abc123'; @@ -85,7 +85,7 @@ describe('shortUrlsListReducer', () => { error: false, }; - expect(reducer(state, { type: CREATE_VISITS, payload: { createdVisits } } as any)).toEqual({ + expect(reducer(state, { type: createNewVisits.toString(), payload: { createdVisits } } as any)).toEqual({ shortUrls: { data: [ { shortCode, domain: 'example.com', visitsCount: 5 }, diff --git a/test/visits/reducers/domainVisits.test.ts b/test/visits/reducers/domainVisits.test.ts index f3d8d7d8..d7f92bbb 100644 --- a/test/visits/reducers/domainVisits.test.ts +++ b/test/visits/reducers/domainVisits.test.ts @@ -13,7 +13,6 @@ import reducer, { DomainVisits, DEFAULT_DOMAIN, } from '../../../src/visits/reducers/domainVisits'; -import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/api/types'; @@ -22,6 +21,7 @@ import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; import { DateInterval } from '../../../src/utils/dates/types'; import { ShortUrl } from '../../../src/short-urls/data'; +import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; describe('domainVisitsReducer', () => { const now = new Date(); @@ -134,7 +134,7 @@ describe('domainVisitsReducer', () => { }); const { visits } = reducer(prevState, { - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [{ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } }] }, } as any); diff --git a/test/visits/reducers/nonOrphanVisits.test.ts b/test/visits/reducers/nonOrphanVisits.test.ts index f4c2bc40..285b1e5e 100644 --- a/test/visits/reducers/nonOrphanVisits.test.ts +++ b/test/visits/reducers/nonOrphanVisits.test.ts @@ -11,7 +11,6 @@ import reducer, { GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED, GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, } from '../../../src/visits/reducers/nonOrphanVisits'; -import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit, VisitsInfo } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/api/types'; @@ -19,6 +18,7 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; import { DateInterval } from '../../../src/utils/dates/types'; +import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; describe('nonOrphanVisitsReducer', () => { const now = new Date(); @@ -106,7 +106,7 @@ describe('nonOrphanVisitsReducer', () => { const visit = Mock.of({ date: formatIsoDate(now) ?? undefined }); const { visits } = reducer(prevState, { - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [{ visit }, { visit }] }, } as any); diff --git a/test/visits/reducers/orphanVisits.test.ts b/test/visits/reducers/orphanVisits.test.ts index 5325ac77..5c259a6e 100644 --- a/test/visits/reducers/orphanVisits.test.ts +++ b/test/visits/reducers/orphanVisits.test.ts @@ -11,7 +11,6 @@ import reducer, { GET_ORPHAN_VISITS_PROGRESS_CHANGED, GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, } from '../../../src/visits/reducers/orphanVisits'; -import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit, VisitsInfo } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/api/types'; @@ -19,6 +18,7 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; import { DateInterval } from '../../../src/utils/dates/types'; +import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; describe('orphanVisitsReducer', () => { const now = new Date(); @@ -106,7 +106,7 @@ describe('orphanVisitsReducer', () => { const visit = Mock.of({ date: formatIsoDate(now) ?? undefined }); const { visits } = reducer(prevState, { - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [{ visit }, { visit }] }, } as any); diff --git a/test/visits/reducers/shortUrlVisits.test.ts b/test/visits/reducers/shortUrlVisits.test.ts index d396edb4..f1134557 100644 --- a/test/visits/reducers/shortUrlVisits.test.ts +++ b/test/visits/reducers/shortUrlVisits.test.ts @@ -12,7 +12,6 @@ import reducer, { GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL, ShortUrlVisits, } from '../../../src/visits/reducers/shortUrlVisits'; -import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/api/types'; @@ -20,6 +19,7 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; import { DateInterval } from '../../../src/utils/dates/types'; +import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; describe('shortUrlVisitsReducer', () => { const now = new Date(); @@ -127,7 +127,7 @@ describe('shortUrlVisitsReducer', () => { }); const { visits } = reducer(prevState, { - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [{ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } }] }, } as any); diff --git a/test/visits/reducers/tagVisits.test.ts b/test/visits/reducers/tagVisits.test.ts index d5fd0375..9e5c9e78 100644 --- a/test/visits/reducers/tagVisits.test.ts +++ b/test/visits/reducers/tagVisits.test.ts @@ -12,7 +12,6 @@ import reducer, { GET_TAG_VISITS_FALLBACK_TO_INTERVAL, TagVisits, } from '../../../src/visits/reducers/tagVisits'; -import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/api/types'; @@ -20,6 +19,7 @@ import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { formatIsoDate } from '../../../src/utils/helpers/date'; import { DateInterval } from '../../../src/utils/dates/types'; +import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; describe('tagVisitsReducer', () => { const now = new Date(); @@ -127,7 +127,7 @@ describe('tagVisitsReducer', () => { }); const { visits } = reducer(prevState, { - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [{ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } }] }, } as any); diff --git a/test/visits/reducers/visitCreation.test.ts b/test/visits/reducers/visitCreation.test.ts index eba1d4a7..19e5f899 100644 --- a/test/visits/reducers/visitCreation.test.ts +++ b/test/visits/reducers/visitCreation.test.ts @@ -1,5 +1,5 @@ import { Mock } from 'ts-mockery'; -import { CREATE_VISITS, createNewVisits } from '../../../src/visits/reducers/visitCreation'; +import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; import { ShortUrl } from '../../../src/short-urls/data'; import { Visit } from '../../../src/visits/types'; @@ -10,7 +10,7 @@ describe('visitCreationReducer', () => { it('just returns the action with proper type', () => { expect(createNewVisits([{ shortUrl, visit }])).toEqual({ - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [{ shortUrl, visit }] }, }); }); diff --git a/test/visits/reducers/visitsOverview.test.ts b/test/visits/reducers/visitsOverview.test.ts index 8405c43a..e93a6565 100644 --- a/test/visits/reducers/visitsOverview.test.ts +++ b/test/visits/reducers/visitsOverview.test.ts @@ -7,7 +7,7 @@ import reducer, { VisitsOverview, loadVisitsOverview, } from '../../../src/visits/reducers/visitsOverview'; -import { CREATE_VISITS, CreateVisitsAction } from '../../../src/visits/reducers/visitCreation'; +import { createNewVisits, CreateVisitsAction } from '../../../src/visits/reducers/visitCreation'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkVisitsOverview } from '../../../src/api/types'; import { ShlinkState } from '../../../src/container/types'; @@ -51,7 +51,7 @@ describe('visitsOverviewReducer', () => { const { visitsCount, orphanVisitsCount } = reducer( state({ visitsCount: 100, orphanVisitsCount: providedOrphanVisitsCount }), { - type: CREATE_VISITS, + type: createNewVisits.toString(), payload: { createdVisits: [ Mock.of({ visit: Mock.all() }), From 033df3c3d65e30c1d19ae881798a67f916717156 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Nov 2022 22:41:02 +0100 Subject: [PATCH 8/8] Migrated tagDelete reducer to RTK --- src/reducers/index.ts | 3 +- src/tags/reducers/tagDelete.ts | 59 +++++++++++++--------------- src/tags/reducers/tagsList.ts | 4 +- src/tags/services/provideServices.ts | 7 +++- test/tags/reducers/tagDelete.test.ts | 54 +++++++++++-------------- test/tags/reducers/tagsList.test.ts | 7 +++- 6 files changed, 63 insertions(+), 71 deletions(-) diff --git a/src/reducers/index.ts b/src/reducers/index.ts index dbbeef32..b91d1f9b 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -9,7 +9,6 @@ import domainVisitsReducer from '../visits/reducers/domainVisits'; import orphanVisitsReducer from '../visits/reducers/orphanVisits'; import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits'; import tagsListReducer from '../tags/reducers/tagsList'; -import tagDeleteReducer from '../tags/reducers/tagDelete'; import { settingsReducer } from '../settings/reducers/settings'; import visitsOverviewReducer from '../visits/reducers/visitsOverview'; import { appUpdatesReducer } from '../app/reducers/appUpdates'; @@ -30,7 +29,7 @@ export default (container: IContainer) => combineReducers({ orphanVisits: orphanVisitsReducer, nonOrphanVisits: nonOrphanVisitsReducer, tagsList: tagsListReducer, - tagDelete: tagDeleteReducer, + tagDelete: container.tagDeleteReducer, tagEdit: container.tagEditReducer, mercureInfo: container.mercureInfoReducer, settings: settingsReducer, diff --git a/src/tags/reducers/tagDelete.ts b/src/tags/reducers/tagDelete.ts index ec5e3ebe..bb7039bf 100644 --- a/src/tags/reducers/tagDelete.ts +++ b/src/tags/reducers/tagDelete.ts @@ -1,16 +1,11 @@ -import { createAction, PayloadAction } from '@reduxjs/toolkit'; -import { Dispatch } from 'redux'; -import { buildReducer } from '../../utils/helpers/redux'; -import { GetState } from '../../container/types'; +import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { parseApiError } from '../../api/utils'; -import { ApiErrorAction } from '../../api/types/actions'; import { ProblemDetailsError } from '../../api/types/errors'; -export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; -export const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR'; -export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG'; -export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED'; +const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG'; +const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED'; export interface TagDeletion { deleting: boolean; @@ -27,27 +22,27 @@ const initialState: TagDeletion = { error: false, }; -export default buildReducer({ - [DELETE_TAG_START]: () => ({ deleting: true, deleted: false, error: false }), - [DELETE_TAG_ERROR]: (_, { errorData }) => ({ deleting: false, deleted: false, error: true, errorData }), - [DELETE_TAG]: () => ({ deleting: false, deleted: true, error: false }), -}, initialState); - -export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: string) => async ( - dispatch: Dispatch, - getState: GetState, -) => { - dispatch({ type: DELETE_TAG_START }); - const { deleteTags } = buildShlinkApiClient(getState); - - try { - await deleteTags([tag]); - dispatch({ type: DELETE_TAG }); - } catch (e: any) { - dispatch({ type: DELETE_TAG_ERROR, errorData: parseApiError(e) }); - - throw e; - } -}; - export const tagDeleted = createAction(TAG_DELETED); + +export const tagDeleteReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { + const deleteTag = createAsyncThunk(DELETE_TAG, async (tag: string, { getState }): Promise => { + const { deleteTags } = buildShlinkApiClient(getState); + await deleteTags([tag]); + }); + + const { reducer } = createSlice({ + name: 'tagDeleteReducer', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(deleteTag.pending, () => ({ deleting: true, deleted: false, error: false })); + builder.addCase( + deleteTag.rejected, + (_, { error }) => ({ deleting: false, deleted: false, error: true, errorData: parseApiError(error) }), + ); + builder.addCase(deleteTag.fulfilled, () => ({ deleting: false, deleted: true, error: false })); + }, + }); + + return { reducer, deleteTag }; +}; diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index c4b25da7..5c60ab58 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -10,7 +10,7 @@ import { parseApiError } from '../../api/utils'; import { TagStats } from '../data'; import { ApiErrorAction } from '../../api/types/actions'; import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducers/shortUrlCreation'; -import { DeleteTagAction, TAG_DELETED } from './tagDelete'; +import { DeleteTagAction, tagDeleted } from './tagDelete'; import { EditTagAction, tagEdited } from './tagEdit'; import { ProblemDetailsError } from '../../api/types/errors'; @@ -85,7 +85,7 @@ export default buildReducer({ [LIST_TAGS_START]: () => ({ ...initialState, loading: true }), [LIST_TAGS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), [LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }), - [TAG_DELETED]: (state, { payload: tag }) => ({ + [tagDeleted.toString()]: (state, { payload: tag }) => ({ ...state, tags: rejectTag(state.tags, tag), filteredTags: rejectTag(state.filteredTags, tag), diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts index 04f95289..e73a8e2d 100644 --- a/src/tags/services/provideServices.ts +++ b/src/tags/services/provideServices.ts @@ -6,7 +6,7 @@ import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal'; import { EditTagModal } from '../helpers/EditTagModal'; import { TagsList } from '../TagsList'; import { filterTags, listTags } from '../reducers/tagsList'; -import { deleteTag, tagDeleted } from '../reducers/tagDelete'; +import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete'; import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit'; import { ConnectDecorator } from '../../container/types'; import { TagsCards } from '../TagsCards'; @@ -41,6 +41,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'buildShlinkApiClient', 'ColorGenerator'); bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator'); + bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient'); + bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator'); + // Actions const listTagsActionFactory = (force: boolean) => ({ buildShlinkApiClient }: IContainer) => listTags(buildShlinkApiClient, force); @@ -49,7 +52,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.factory('forceListTags', listTagsActionFactory(true)); bottle.serviceFactory('filterTags', () => filterTags); - bottle.serviceFactory('deleteTag', deleteTag, 'buildShlinkApiClient'); + bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator'); bottle.serviceFactory('tagDeleted', () => tagDeleted); bottle.serviceFactory('editTag', prop('editTag'), 'tagEditReducerCreator'); diff --git a/test/tags/reducers/tagDelete.test.ts b/test/tags/reducers/tagDelete.test.ts index c87e2bfd..3b4bf655 100644 --- a/test/tags/reducers/tagDelete.test.ts +++ b/test/tags/reducers/tagDelete.test.ts @@ -1,19 +1,18 @@ import { Mock } from 'ts-mockery'; -import reducer, { - DELETE_TAG_START, - DELETE_TAG_ERROR, - DELETE_TAG, - TAG_DELETED, - tagDeleted, - deleteTag, -} from '../../../src/tags/reducers/tagDelete'; +import { tagDeleted, tagDeleteReducerCreator } from '../../../src/tags/reducers/tagDelete'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; describe('tagDeleteReducer', () => { + const deleteTagsCall = jest.fn(); + const buildShlinkApiClient = () => Mock.of({ deleteTags: deleteTagsCall }); + const { reducer, deleteTag } = tagDeleteReducerCreator(buildShlinkApiClient); + + beforeEach(jest.clearAllMocks); + describe('reducer', () => { it('returns loading on DELETE_TAG_START', () => { - expect(reducer(undefined, { type: DELETE_TAG_START })).toEqual({ + expect(reducer(undefined, { type: deleteTag.pending.toString() })).toEqual({ deleting: true, deleted: false, error: false, @@ -21,7 +20,7 @@ describe('tagDeleteReducer', () => { }); it('returns error on DELETE_TAG_ERROR', () => { - expect(reducer(undefined, { type: DELETE_TAG_ERROR })).toEqual({ + expect(reducer(undefined, { type: deleteTag.rejected.toString() })).toEqual({ deleting: false, deleted: false, error: true, @@ -29,7 +28,7 @@ describe('tagDeleteReducer', () => { }); it('returns tag names on DELETE_TAG', () => { - expect(reducer(undefined, { type: DELETE_TAG })).toEqual({ + expect(reducer(undefined, { type: deleteTag.fulfilled.toString() })).toEqual({ deleting: false, deleted: true, error: false, @@ -40,53 +39,46 @@ describe('tagDeleteReducer', () => { describe('tagDeleted', () => { it('returns action based on provided params', () => expect(tagDeleted('foo')).toEqual({ - type: TAG_DELETED, + type: tagDeleted.toString(), payload: 'foo', })); }); describe('deleteTag', () => { - const createApiClientMock = (result: Promise) => Mock.of({ - deleteTags: jest.fn(async () => result), - }); const dispatch = jest.fn(); const getState = () => Mock.all(); - afterEach(() => dispatch.mockReset()); - it('calls API on success', async () => { const tag = 'foo'; - const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = deleteTag(() => apiClientMock)(tag); + deleteTagsCall.mockResolvedValue(undefined); - await dispatchable(dispatch, getState); + await deleteTag(tag)(dispatch, getState, {}); - expect(apiClientMock.deleteTags).toHaveBeenCalledTimes(1); - expect(apiClientMock.deleteTags).toHaveBeenNthCalledWith(1, [tag]); + expect(deleteTagsCall).toHaveBeenCalledTimes(1); + expect(deleteTagsCall).toHaveBeenNthCalledWith(1, [tag]); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: DELETE_TAG_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: DELETE_TAG }); + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: deleteTag.pending.toString() })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: deleteTag.fulfilled.toString() })); }); it('throws on error', async () => { const error = 'Error'; const tag = 'foo'; - const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = deleteTag(() => apiClientMock)(tag); + deleteTagsCall.mockRejectedValue(error); try { - await dispatchable(dispatch, getState); + await deleteTag(tag)(dispatch, getState, {}); } catch (e) { expect(e).toEqual(error); } - expect(apiClientMock.deleteTags).toHaveBeenCalledTimes(1); - expect(apiClientMock.deleteTags).toHaveBeenNthCalledWith(1, [tag]); + expect(deleteTagsCall).toHaveBeenCalledTimes(1); + expect(deleteTagsCall).toHaveBeenNthCalledWith(1, [tag]); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: DELETE_TAG_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: DELETE_TAG_ERROR }); + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: deleteTag.pending.toString() })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: deleteTag.rejected.toString() })); }); }); }); diff --git a/test/tags/reducers/tagsList.test.ts b/test/tags/reducers/tagsList.test.ts index 1fcb390e..12ed7cb5 100644 --- a/test/tags/reducers/tagsList.test.ts +++ b/test/tags/reducers/tagsList.test.ts @@ -8,11 +8,11 @@ import reducer, { listTags, TagsList, } from '../../../src/tags/reducers/tagsList'; -import { TAG_DELETED } from '../../../src/tags/reducers/tagDelete'; import { ShlinkState } from '../../../src/container/types'; import { ShortUrl } from '../../../src/short-urls/data'; import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation'; import { tagEdited } from '../../../src/tags/reducers/tagEdit'; +import { tagDeleted } from '../../../src/tags/reducers/tagDelete'; describe('tagsListReducer', () => { const state = (props: Partial) => Mock.of(props); @@ -48,7 +48,10 @@ describe('tagsListReducer', () => { const tag = 'foo'; const expectedTags = ['bar', 'baz']; - expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_DELETED, payload: tag } as any)).toEqual({ + expect(reducer( + state({ tags, filteredTags: tags }), + { type: tagDeleted.toString(), payload: tag } as any, + )).toEqual({ tags: expectedTags, filteredTags: expectedTags, });