From 2a268de2cb37eaa134839020d3ffc5178c728f67 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 6 Nov 2022 12:32:55 +0100 Subject: [PATCH] Migrated editShortUrl reducer to RTK --- src/reducers/index.ts | 3 +- src/short-urls/EditShortUrl.tsx | 16 ++---- src/short-urls/reducers/shortUrlEdition.ts | 56 ++++++++++--------- src/short-urls/reducers/shortUrlsList.ts | 2 +- src/short-urls/services/provideServices.ts | 7 ++- test/short-urls/EditShortUrl.test.tsx | 9 ++- .../reducers/shortUrlEdition.test.ts | 45 +++++++-------- .../short-urls/reducers/shortUrlsList.test.ts | 2 +- 8 files changed, 74 insertions(+), 66 deletions(-) diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 885033ce..707fe5ca 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -4,7 +4,6 @@ import { serversReducer } from '../servers/reducers/servers'; import selectedServerReducer from '../servers/reducers/selectedServer'; import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList'; import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion'; -import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition'; import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits'; import tagVisitsReducer from '../visits/reducers/tagVisits'; import domainVisitsReducer from '../visits/reducers/domainVisits'; @@ -26,7 +25,7 @@ export default (container: IContainer) => combineReducers({ shortUrlsList: shortUrlsListReducer, shortUrlCreationResult: container.shortUrlCreationReducer, shortUrlDeletion: shortUrlDeletionReducer, - shortUrlEdition: shortUrlEditionReducer, + shortUrlEdition: container.shortUrlEditionReducer, shortUrlVisits: shortUrlVisitsReducer, tagVisits: tagVisitsReducer, domainVisits: domainVisitsReducer, diff --git a/src/short-urls/EditShortUrl.tsx b/src/short-urls/EditShortUrl.tsx index 8445bfc7..58687d7e 100644 --- a/src/short-urls/EditShortUrl.tsx +++ b/src/short-urls/EditShortUrl.tsx @@ -11,7 +11,7 @@ import { parseQuery } from '../utils/helpers/query'; import { Message } from '../utils/Message'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; -import { useGoBack, useToggle } from '../utils/helpers/hooks'; +import { useGoBack } from '../utils/helpers/hooks'; import { ShortUrlFormProps } from './ShortUrlForm'; import { ShortUrlDetail } from './reducers/shortUrlDetail'; import { EditShortUrl as EditShortUrlInfo, ShortUrlEdition } from './reducers/shortUrlEdition'; @@ -23,7 +23,7 @@ interface EditShortUrlConnectProps { shortUrlDetail: ShortUrlDetail; shortUrlEdition: ShortUrlEdition; getShortUrlDetail: (shortCode: string, domain: OptionalString) => void; - editShortUrl: (editShortUrl: EditShortUrlInfo) => Promise; + editShortUrl: (editShortUrl: EditShortUrlInfo) => void; } export const EditShortUrl = (ShortUrlForm: FC) => ({ @@ -38,13 +38,12 @@ export const EditShortUrl = (ShortUrlForm: FC) => ({ const params = useParams<{ shortCode: string }>(); const goBack = useGoBack(); const { loading, error, errorData, shortUrl } = shortUrlDetail; - const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition; + const { saving, saved, error: savingError, errorData: savingErrorData } = shortUrlEdition; const { domain } = parseQuery<{ domain?: string }>(search); const initialState = useMemo( () => shortUrlDataFromShortUrl(shortUrl, shortUrlCreationSettings), [shortUrl, shortUrlCreationSettings], ); - const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle(); useEffect(() => { params.shortCode && getShortUrlDetail(urlDecodeShortCode(params.shortCode), domain); @@ -87,18 +86,15 @@ export const EditShortUrl = (ShortUrlForm: FC) => ({ return; } - isNotSuccessful(); - editShortUrl({ ...shortUrl, data: shortUrlData }) - .then(isSuccessful) - .catch(isNotSuccessful); + editShortUrl({ ...shortUrl, data: shortUrlData }); }} /> - {savingError && ( + {saved && savingError && ( )} - {savingSucceeded && Short URL properly edited.} + {saved && !savingError && Short URL properly edited.} ); }; diff --git a/src/short-urls/reducers/shortUrlEdition.ts b/src/short-urls/reducers/shortUrlEdition.ts index e446c33e..462515ca 100644 --- a/src/short-urls/reducers/shortUrlEdition.ts +++ b/src/short-urls/reducers/shortUrlEdition.ts @@ -1,22 +1,18 @@ -import { PayloadAction } from '@reduxjs/toolkit'; -import { Dispatch } from 'redux'; -import { buildReducer } from '../../utils/helpers/redux'; -import { GetState } from '../../container/types'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk } from '../../utils/helpers/redux'; import { OptionalString } from '../../utils/utils'; import { EditShortUrlData, ShortUrl } from '../data'; 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_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START'; -export const EDIT_SHORT_URL_ERROR = 'shlink/shortUrlEdition/EDIT_SHORT_URL_ERROR'; export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED'; export interface ShortUrlEdition { shortUrl?: ShortUrl; saving: boolean; error: boolean; + saved: boolean; errorData?: ProblemDetailsError; } @@ -30,29 +26,35 @@ export type ShortUrlEditedAction = PayloadAction; const initialState: ShortUrlEdition = { saving: false, + saved: false, error: false, }; -export default buildReducer({ - [EDIT_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }), - [EDIT_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }), - [SHORT_URL_EDITED]: (_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false }), -}, initialState); +export const shortUrlEditionReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => { + const editShortUrl = createAsyncThunk( + SHORT_URL_EDITED, + ({ shortCode, domain, data }: EditShortUrl, { getState }): Promise => { + const { updateShortUrl } = buildShlinkApiClient(getState); + return updateShortUrl(shortCode, domain, data as any); // FIXME parse dates + }, + ); -export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( - { shortCode, domain, data }: EditShortUrl, -) => async (dispatch: Dispatch, getState: GetState) => { - dispatch({ type: EDIT_SHORT_URL_START }); + const { reducer } = createSlice({ + name: 'shortUrlEditionReducer', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(editShortUrl.pending, (state) => ({ ...state, saving: true, error: false, saved: false })); + builder.addCase( + editShortUrl.rejected, + (state, { error }) => ({ ...state, saving: false, error: true, saved: false, errorData: parseApiError(error) }), + ); + builder.addCase( + editShortUrl.fulfilled, + (_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false, saved: true }), + ); + }, + }); - const { updateShortUrl } = buildShlinkApiClient(getState); - - try { - const payload = await updateShortUrl(shortCode, domain, data as any); // FIXME parse dates; - - dispatch({ payload, type: SHORT_URL_EDITED }); - } catch (e: any) { - dispatch({ type: EDIT_SHORT_URL_ERROR, errorData: parseApiError(e) }); - - throw e; - } + return { reducer, editShortUrl }; }; diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts index 136bd164..e2246524 100644 --- a/src/short-urls/reducers/shortUrlsList.ts +++ b/src/short-urls/reducers/shortUrlsList.ts @@ -89,7 +89,7 @@ export default buildReducer({ state, )), ), - [SHORT_URL_EDITED]: (state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath( + [`${SHORT_URL_EDITED}/fulfilled`]: (state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath( ['shortUrls', 'data'], state.shortUrls.data.map((shortUrl) => { const { shortCode, domain } = editedShortUrl; diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts index d357e0fb..aab487c4 100644 --- a/src/short-urls/services/provideServices.ts +++ b/src/short-urls/services/provideServices.ts @@ -10,7 +10,7 @@ import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult'; import { listShortUrls } from '../reducers/shortUrlsList'; import { shortUrlCreationReducerCreator } from '../reducers/shortUrlCreation'; import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion'; -import { editShortUrl } from '../reducers/shortUrlEdition'; +import { shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition'; import { ConnectDecorator } from '../../container/types'; import { ShortUrlsTable } from '../ShortUrlsTable'; import { QrCodeModal } from '../helpers/QrCodeModal'; @@ -60,6 +60,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('shortUrlCreationReducerCreator', shortUrlCreationReducerCreator, 'buildShlinkApiClient'); bottle.serviceFactory('shortUrlCreationReducer', prop('reducer'), 'shortUrlCreationReducerCreator'); + bottle.serviceFactory('shortUrlEditionReducerCreator', shortUrlEditionReducerCreator, 'buildShlinkApiClient'); + bottle.serviceFactory('shortUrlEditionReducer', prop('reducer'), 'shortUrlEditionReducerCreator'); + // Actions bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); @@ -71,7 +74,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('getShortUrlDetail', getShortUrlDetail, 'buildShlinkApiClient'); - bottle.serviceFactory('editShortUrl', editShortUrl, 'buildShlinkApiClient'); + bottle.serviceFactory('editShortUrl', prop('editShortUrl'), 'shortUrlEditionReducerCreator'); }; export default provideServices; diff --git a/test/short-urls/EditShortUrl.test.tsx b/test/short-urls/EditShortUrl.test.tsx index e97152fd..8af59969 100644 --- a/test/short-urls/EditShortUrl.test.tsx +++ b/test/short-urls/EditShortUrl.test.tsx @@ -46,9 +46,16 @@ describe('', () => { }); it('shows error when saving data has failed', () => { - setUp({}, { error: true }); + setUp({}, { error: true, saved: true }); expect(screen.getByText('An error occurred while updating short URL :(')).toBeInTheDocument(); expect(screen.getByText('ShortUrlForm')).toBeInTheDocument(); }); + + it('shows message when saving data succeeds', () => { + setUp({}, { error: false, saved: true }); + + expect(screen.getByText('Short URL properly edited.')).toBeInTheDocument(); + expect(screen.getByText('ShortUrlForm')).toBeInTheDocument(); + }); }); diff --git a/test/short-urls/reducers/shortUrlEdition.test.ts b/test/short-urls/reducers/shortUrlEdition.test.ts index 792b33ab..7b0ecc43 100644 --- a/test/short-urls/reducers/shortUrlEdition.test.ts +++ b/test/short-urls/reducers/shortUrlEdition.test.ts @@ -1,11 +1,5 @@ import { Mock } from 'ts-mockery'; -import reducer, { - EDIT_SHORT_URL_START, - EDIT_SHORT_URL_ERROR, - SHORT_URL_EDITED, - editShortUrl, - ShortUrlEditedAction, -} from '../../../src/short-urls/reducers/shortUrlEdition'; +import { ShortUrlEditedAction, shortUrlEditionReducerCreator } from '../../../src/short-urls/reducers/shortUrlEdition'; import { ShlinkState } from '../../../src/container/types'; import { ShortUrl } from '../../../src/short-urls/data'; import { SelectedServer } from '../../../src/servers/data'; @@ -14,48 +8,59 @@ describe('shortUrlEditionReducer', () => { const longUrl = 'https://shlink.io'; const shortCode = 'abc123'; const shortUrl = Mock.of({ longUrl, shortCode }); + const updateShortUrl = jest.fn().mockResolvedValue(shortUrl); + const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl }); + const { reducer, editShortUrl } = shortUrlEditionReducerCreator(buildShlinkApiClient); + + afterEach(jest.clearAllMocks); describe('reducer', () => { it('returns loading on EDIT_SHORT_URL_START', () => { - expect(reducer(undefined, Mock.of({ type: EDIT_SHORT_URL_START }))).toEqual({ + expect(reducer(undefined, Mock.of({ type: editShortUrl.pending.toString() }))).toEqual({ saving: true, + saved: false, error: false, }); }); it('returns error on EDIT_SHORT_URL_ERROR', () => { - expect(reducer(undefined, Mock.of({ type: EDIT_SHORT_URL_ERROR }))).toEqual({ + expect(reducer(undefined, Mock.of({ type: editShortUrl.rejected.toString() }))).toEqual({ saving: false, + saved: false, error: true, }); }); it('returns provided tags and shortCode on SHORT_URL_EDITED', () => { - expect(reducer(undefined, { type: SHORT_URL_EDITED, payload: shortUrl })).toEqual({ + expect(reducer(undefined, { type: editShortUrl.fulfilled.toString(), payload: shortUrl })).toEqual({ shortUrl, saving: false, + saved: true, error: false, }); }); }); describe('editShortUrl', () => { - const updateShortUrl = jest.fn().mockResolvedValue(shortUrl); - const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl }); const dispatch = jest.fn(); const createGetState = (selectedServer: SelectedServer = null) => () => Mock.of({ selectedServer }); afterEach(jest.clearAllMocks); it.each([[undefined], [null], ['example.com']])('dispatches short URL on success', async (domain) => { - await editShortUrl(buildShlinkApiClient)({ shortCode, domain, data: { longUrl } })(dispatch, createGetState()); + await editShortUrl({ shortCode, domain, data: { longUrl } })(dispatch, createGetState(), {}); expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); expect(updateShortUrl).toHaveBeenCalledTimes(1); expect(updateShortUrl).toHaveBeenCalledWith(shortCode, domain, { longUrl }); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_EDITED, payload: shortUrl }); + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ + type: editShortUrl.pending.toString(), + })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ + type: editShortUrl.fulfilled.toString(), + payload: shortUrl, + })); }); it('dispatches error on failure', async () => { @@ -63,18 +68,14 @@ describe('shortUrlEditionReducer', () => { updateShortUrl.mockRejectedValue(error); - try { - await editShortUrl(buildShlinkApiClient)({ shortCode, data: { longUrl } })(dispatch, createGetState()); - } catch (e) { - expect(e).toBe(error); - } + await editShortUrl({ shortCode, data: { longUrl } })(dispatch, createGetState(), {}); expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); expect(updateShortUrl).toHaveBeenCalledTimes(1); expect(updateShortUrl).toHaveBeenCalledWith(shortCode, undefined, { longUrl }); expect(dispatch).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_ERROR }); + expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: editShortUrl.pending.toString() })); + expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: editShortUrl.rejected.toString() })); }); }); }); diff --git a/test/short-urls/reducers/shortUrlsList.test.ts b/test/short-urls/reducers/shortUrlsList.test.ts index bd2dbb93..38c4a24d 100644 --- a/test/short-urls/reducers/shortUrlsList.test.ts +++ b/test/short-urls/reducers/shortUrlsList.test.ts @@ -181,7 +181,7 @@ describe('shortUrlsListReducer', () => { error: false, }; - const result = reducer(state, { type: SHORT_URL_EDITED, payload: editedShortUrl } as any); + const result = reducer(state, { type: `${SHORT_URL_EDITED}/fulfilled`, payload: editedShortUrl } as any); expect(result.shortUrls?.data).toEqual(expectedList); });