From d8f3952920144f5f2f9e69f5743a0786c7418a1a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 24 Aug 2020 18:52:52 +0200 Subject: [PATCH] Migrated first short URL reducers to typescript --- .eslintrc | 3 +- src/container/types.ts | 6 ++- src/short-urls/data/index.ts | 29 ++++++++++++++ ...hortUrlCreation.js => shortUrlCreation.ts} | 24 +++++++++--- .../{shortUrlMeta.js => shortUrlMeta.ts} | 38 ++++++++++++++++--- src/short-urls/reducers/shortUrlsList.js | 9 ++++- src/utils/utils.ts | 4 ++ ...ation.test.js => shortUrlCreation.test.ts} | 33 +++++++++------- ...rtUrlMeta.test.js => shortUrlMeta.test.ts} | 23 +++++++---- 9 files changed, 134 insertions(+), 35 deletions(-) create mode 100644 src/short-urls/data/index.ts rename src/short-urls/reducers/{shortUrlCreation.js => shortUrlCreation.ts} (62%) rename src/short-urls/reducers/{shortUrlMeta.js => shortUrlMeta.ts} (55%) rename test/short-urls/reducers/{shortUrlCreation.test.js => shortUrlCreation.test.ts} (69%) rename test/short-urls/reducers/{shortUrlMeta.test.js => shortUrlMeta.test.ts} (78%) diff --git a/.eslintrc b/.eslintrc index f50c9716..ef7e0b47 100644 --- a/.eslintrc +++ b/.eslintrc @@ -64,7 +64,8 @@ "max-len": ["error", { "code": 120, "ignoreStrings": true, - "ignoreTemplateLiterals": true + "ignoreTemplateLiterals": true, + "ignoreTrailingComments": true }], "no-mixed-operators": "off", "react/display-name": "off" diff --git a/src/container/types.ts b/src/container/types.ts index 7a644498..618826dd 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -2,6 +2,8 @@ import { MercureInfo } from '../mercure/reducers/mercureInfo'; import { ServersMap } from '../servers/reducers/servers'; import { SelectedServer } from '../servers/data'; import { Settings } from '../settings/reducers/settings'; +import { ShortUrlMetaEdition } from '../short-urls/reducers/shortUrlMeta'; +import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation'; export type ConnectDecorator = (props: string[], actions?: string[]) => any; @@ -10,10 +12,10 @@ export interface ShlinkState { selectedServer: SelectedServer; shortUrlsList: any; shortUrlsListParams: any; - shortUrlCreationResult: any; + shortUrlCreationResult: ShortUrlCreation; shortUrlDeletion: any; shortUrlTags: any; - shortUrlMeta: any; + shortUrlMeta: ShortUrlMetaEdition; shortUrlEdition: any; shortUrlVisits: any; tagVisits: any; diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts new file mode 100644 index 00000000..7be9ac1a --- /dev/null +++ b/src/short-urls/data/index.ts @@ -0,0 +1,29 @@ +import { Nullable } from '../../utils/utils'; + +export interface ShortUrlData { + longUrl: string; + tags?: string[]; + customSlug?: string; + shortCodeLength?: number; + domain?: string; + validSince?: string; + validUntil?: string; + maxVisits?: number; + findIfExists?: boolean; +} + +export interface ShortUrl { + shortCode: string; + shortUrl: string; + longUrl: string; + visitsCount: number; + meta: Required>; + tags: string[]; + domain: string | null; +} + +export interface ShortUrlMeta { + validSince?: string; + validUntil?: string; + maxVisits?: number; +} diff --git a/src/short-urls/reducers/shortUrlCreation.js b/src/short-urls/reducers/shortUrlCreation.ts similarity index 62% rename from src/short-urls/reducers/shortUrlCreation.js rename to src/short-urls/reducers/shortUrlCreation.ts index 071e3d28..878dd143 100644 --- a/src/short-urls/reducers/shortUrlCreation.js +++ b/src/short-urls/reducers/shortUrlCreation.ts @@ -1,5 +1,9 @@ import PropTypes from 'prop-types'; import { createAction, handleActions } from 'redux-actions'; +import { Action, Dispatch } from 'redux'; +import { ShlinkApiClientBuilder } from '../../utils/services/types'; +import { GetState } from '../../container/types'; +import { ShortUrl, ShortUrlData } from '../data'; /* eslint-disable padding-line-between-statements */ export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START'; @@ -8,6 +12,7 @@ export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL'; export const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL'; /* eslint-enable padding-line-between-statements */ +/** @deprecated Use ShortUrlCreation interface instead */ export const createShortUrlResultType = PropTypes.shape({ result: PropTypes.shape({ shortUrl: PropTypes.string, @@ -16,27 +21,36 @@ export const createShortUrlResultType = PropTypes.shape({ error: PropTypes.bool, }); -const initialState = { +export interface ShortUrlCreation { + result: ShortUrl | null; + saving: boolean; + error: boolean; +} + +const initialState: ShortUrlCreation = { result: null, saving: false, error: false, }; -export default handleActions({ +export default handleActions({ [CREATE_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }), [CREATE_SHORT_URL_ERROR]: (state) => ({ ...state, saving: false, error: true }), - [CREATE_SHORT_URL]: (state, { result }) => ({ result, saving: false, error: false }), + [CREATE_SHORT_URL]: (_, { result }: any) => ({ result, saving: false, error: false }), [RESET_CREATE_SHORT_URL]: () => initialState, }, initialState); -export const createShortUrl = (buildShlinkApiClient) => (data) => async (dispatch, getState) => { +export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => (data: ShortUrlData) => async ( + dispatch: Dispatch, + getState: GetState, +) => { dispatch({ type: CREATE_SHORT_URL_START }); const { createShortUrl } = buildShlinkApiClient(getState); try { const result = await createShortUrl(data); - dispatch({ type: CREATE_SHORT_URL, result }); + dispatch({ type: CREATE_SHORT_URL, result }); } catch (e) { dispatch({ type: CREATE_SHORT_URL_ERROR }); diff --git a/src/short-urls/reducers/shortUrlMeta.js b/src/short-urls/reducers/shortUrlMeta.ts similarity index 55% rename from src/short-urls/reducers/shortUrlMeta.js rename to src/short-urls/reducers/shortUrlMeta.ts index 7582414f..aafdaab0 100644 --- a/src/short-urls/reducers/shortUrlMeta.js +++ b/src/short-urls/reducers/shortUrlMeta.ts @@ -1,5 +1,9 @@ -import { createAction, handleActions } from 'redux-actions'; +import { createAction, handleActions, Action } from 'redux-actions'; import PropTypes from 'prop-types'; +import { Dispatch } from 'redux'; +import { ShortUrlMeta } from '../data'; +import { ShlinkApiClientBuilder } from '../../utils/services/types'; +import { GetState } from '../../container/types'; /* eslint-disable padding-line-between-statements */ export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START'; @@ -8,12 +12,14 @@ export const SHORT_URL_META_EDITED = 'shlink/shortUrlMeta/SHORT_URL_META_EDITED' export const RESET_EDIT_SHORT_URL_META = 'shlink/shortUrlMeta/RESET_EDIT_SHORT_URL_META'; /* eslint-enable padding-line-between-statements */ +/** @deprecated Use ShortUrlMeta interface instead */ export const shortUrlMetaType = PropTypes.shape({ validSince: PropTypes.string, validUntil: PropTypes.string, maxVisits: PropTypes.number, }); +/** @deprecated Use ShortUrlMetaEdition interface instead */ export const shortUrlEditMetaType = PropTypes.shape({ shortCode: PropTypes.string, meta: shortUrlMetaType.isRequired, @@ -21,27 +27,47 @@ export const shortUrlEditMetaType = PropTypes.shape({ error: PropTypes.bool.isRequired, }); -const initialState = { +export interface ShortUrlMetaEdition { + shortCode: string | null; + meta: ShortUrlMeta; + saving: boolean; + error: boolean; +} + +interface ShortUrlMetaEditedAction { + shortCode: string; + domain?: string | null; + meta: ShortUrlMeta; +} + +const initialState: ShortUrlMetaEdition = { shortCode: null, meta: {}, saving: false, error: false, }; -export default handleActions({ +export default handleActions({ [EDIT_SHORT_URL_META_START]: (state) => ({ ...state, saving: true, error: false }), [EDIT_SHORT_URL_META_ERROR]: (state) => ({ ...state, saving: false, error: true }), - [SHORT_URL_META_EDITED]: (state, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }), + [SHORT_URL_META_EDITED]: (_, { payload }) => ({ ...payload, saving: false, error: false }), [RESET_EDIT_SHORT_URL_META]: () => initialState, }, initialState); -export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, domain, meta) => async (dispatch, getState) => { +export const editShortUrlMeta = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( + shortCode: string, + domain: string | null | undefined, + meta: ShortUrlMeta, +) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: EDIT_SHORT_URL_META_START }); const { updateShortUrlMeta } = buildShlinkApiClient(getState); try { await updateShortUrlMeta(shortCode, domain, meta); - dispatch({ shortCode, meta, domain, type: SHORT_URL_META_EDITED }); + dispatch>({ + type: SHORT_URL_META_EDITED, + payload: { shortCode, meta, domain }, + }); } catch (e) { dispatch({ type: EDIT_SHORT_URL_META_ERROR }); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index 90d2b1c8..26f35aef 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -30,6 +30,7 @@ const initialState = { error: false, }; +// TODO Make all actions fetch shortCode, domain and prop from payload const setPropFromActionOnMatchingShortUrl = (prop) => (state, { shortCode, domain, [prop]: propValue }) => assocPath( [ 'shortUrls', 'data' ], state.shortUrls.data.map( @@ -48,7 +49,13 @@ export default handleActions({ state, ), [SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl('tags'), - [SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl('meta'), + [SHORT_URL_META_EDITED]: (state, { payload: { shortCode, domain, meta } }) => assocPath( + [ 'shortUrls', 'data' ], + state.shortUrls.data.map( + (shortUrl) => shortUrlMatches(shortUrl, shortCode, domain) ? assoc('meta', meta, shortUrl) : shortUrl, + ), + state, + ), [SHORT_URL_EDITED]: setPropFromActionOnMatchingShortUrl('longUrl'), [CREATE_VISIT]: (state, { shortUrl: { shortCode, domain, visitsCount } }) => assocPath( [ 'shortUrls', 'data' ], diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 27521f1e..c968bf45 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -21,3 +21,7 @@ export const rangeOf = (size: number, mappingFn: (value: number) => T, startA export type Empty = null | undefined | '' | never[]; export const hasValue = (value: T | Empty): value is T => !isNil(value) && !isEmpty(value); + +export type Nullable = { + [P in keyof T]: T[P] | null +}; diff --git a/test/short-urls/reducers/shortUrlCreation.test.js b/test/short-urls/reducers/shortUrlCreation.test.ts similarity index 69% rename from test/short-urls/reducers/shortUrlCreation.test.js rename to test/short-urls/reducers/shortUrlCreation.test.ts index 0ebacbe5..22e24419 100644 --- a/test/short-urls/reducers/shortUrlCreation.test.js +++ b/test/short-urls/reducers/shortUrlCreation.test.ts @@ -1,3 +1,4 @@ +import { Mock } from 'ts-mockery'; import reducer, { CREATE_SHORT_URL_START, CREATE_SHORT_URL_ERROR, @@ -6,33 +7,40 @@ import reducer, { createShortUrl, resetCreateShortUrl, } from '../../../src/short-urls/reducers/shortUrlCreation'; +import { ShortUrl } from '../../../src/short-urls/data'; +import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient'; +import { ShlinkState } from '../../../src/container/types'; describe('shortUrlCreationReducer', () => { + const shortUrl = Mock.all(); + describe('reducer', () => { it('returns loading on CREATE_SHORT_URL_START', () => { - expect(reducer({}, { type: CREATE_SHORT_URL_START })).toEqual({ + expect(reducer(undefined, { type: CREATE_SHORT_URL_START } as any)).toEqual({ + result: null, saving: true, error: false, }); }); it('returns error on CREATE_SHORT_URL_ERROR', () => { - expect(reducer({}, { type: CREATE_SHORT_URL_ERROR })).toEqual({ + expect(reducer(undefined, { type: CREATE_SHORT_URL_ERROR } as any)).toEqual({ + result: null, saving: false, error: true, }); }); it('returns result on CREATE_SHORT_URL', () => { - expect(reducer({}, { type: CREATE_SHORT_URL, result: 'foo' })).toEqual({ + expect(reducer(undefined, { type: CREATE_SHORT_URL, result: shortUrl } as any)).toEqual({ + result: shortUrl, saving: false, error: false, - result: 'foo', }); }); it('returns default state on RESET_CREATE_SHORT_URL', () => { - expect(reducer({}, { type: RESET_CREATE_SHORT_URL })).toEqual({ + expect(reducer(undefined, { type: RESET_CREATE_SHORT_URL } as any)).toEqual({ result: null, saving: false, error: false, @@ -46,31 +54,30 @@ describe('shortUrlCreationReducer', () => { }); describe('createShortUrl', () => { - const createApiClientMock = (result) => ({ - createShortUrl: jest.fn(() => result), + const createApiClientMock = (result: Promise) => Mock.of({ + createShortUrl: jest.fn().mockReturnValue(result), }); const dispatch = jest.fn(); - const getState = () => ({}); + const getState = () => Mock.all(); afterEach(jest.resetAllMocks); it('calls API on success', async () => { - const result = 'foo'; - const apiClientMock = createApiClientMock(Promise.resolve(result)); - const dispatchable = createShortUrl(() => apiClientMock)({}); + const apiClientMock = createApiClientMock(Promise.resolve(shortUrl)); + const dispatchable = createShortUrl(() => apiClientMock)({ longUrl: 'foo' }); await dispatchable(dispatch, getState); expect(apiClientMock.createShortUrl).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: CREATE_SHORT_URL_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: CREATE_SHORT_URL, result }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: CREATE_SHORT_URL, result: shortUrl }); }); it('throws on error', async () => { const error = 'Error'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = createShortUrl(() => apiClientMock)({}); + const dispatchable = createShortUrl(() => apiClientMock)({ longUrl: 'foo' }); expect.assertions(5); diff --git a/test/short-urls/reducers/shortUrlMeta.test.js b/test/short-urls/reducers/shortUrlMeta.test.ts similarity index 78% rename from test/short-urls/reducers/shortUrlMeta.test.js rename to test/short-urls/reducers/shortUrlMeta.test.ts index a02b385a..d7c6e409 100644 --- a/test/short-urls/reducers/shortUrlMeta.test.js +++ b/test/short-urls/reducers/shortUrlMeta.test.ts @@ -1,4 +1,5 @@ import moment from 'moment'; +import { Mock } from 'ts-mockery'; import reducer, { EDIT_SHORT_URL_META_START, EDIT_SHORT_URL_META_ERROR, @@ -7,6 +8,7 @@ import reducer, { editShortUrlMeta, resetShortUrlMeta, } from '../../../src/short-urls/reducers/shortUrlMeta'; +import { ShlinkState } from '../../../src/container/types'; describe('shortUrlMetaReducer', () => { const meta = { @@ -17,21 +19,25 @@ describe('shortUrlMetaReducer', () => { describe('reducer', () => { it('returns loading on EDIT_SHORT_URL_META_START', () => { - expect(reducer({}, { type: EDIT_SHORT_URL_META_START })).toEqual({ + expect(reducer(undefined, { type: EDIT_SHORT_URL_META_START } as any)).toEqual({ + meta: {}, + shortCode: null, saving: true, error: false, }); }); it('returns error on EDIT_SHORT_URL_META_ERROR', () => { - expect(reducer({}, { type: EDIT_SHORT_URL_META_ERROR })).toEqual({ + expect(reducer(undefined, { type: EDIT_SHORT_URL_META_ERROR } as any)).toEqual({ + meta: {}, + shortCode: null, saving: false, error: true, }); }); it('returns provided tags and shortCode on SHORT_URL_META_EDITED', () => { - expect(reducer({}, { type: SHORT_URL_META_EDITED, meta, shortCode })).toEqual({ + expect(reducer(undefined, { type: SHORT_URL_META_EDITED, payload: { meta, shortCode } })).toEqual({ meta, shortCode, saving: false, @@ -40,7 +46,7 @@ describe('shortUrlMetaReducer', () => { }); it('goes back to initial state on RESET_EDIT_SHORT_URL_META', () => { - expect(reducer({}, { type: RESET_EDIT_SHORT_URL_META })).toEqual({ + expect(reducer(undefined, { type: RESET_EDIT_SHORT_URL_META } as any)).toEqual({ meta: {}, shortCode: null, saving: false, @@ -53,18 +59,21 @@ describe('shortUrlMetaReducer', () => { const updateShortUrlMeta = jest.fn().mockResolvedValue({}); const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrlMeta }); const dispatch = jest.fn(); + const getState = () => Mock.all(); afterEach(jest.clearAllMocks); it.each([[ undefined ], [ null ], [ 'example.com' ]])('dispatches metadata on success', async (domain) => { - await editShortUrlMeta(buildShlinkApiClient)(shortCode, domain, meta)(dispatch); + const payload = { meta, shortCode, domain }; + + await editShortUrlMeta(buildShlinkApiClient)(shortCode, domain, meta)(dispatch, getState); expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); expect(updateShortUrlMeta).toHaveBeenCalledTimes(1); expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, domain, meta); expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_META_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_META_EDITED, meta, shortCode, domain }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_META_EDITED, payload }); }); it('dispatches error on failure', async () => { @@ -73,7 +82,7 @@ describe('shortUrlMetaReducer', () => { updateShortUrlMeta.mockRejectedValue(error); try { - await editShortUrlMeta(buildShlinkApiClient)(shortCode, undefined, meta)(dispatch); + await editShortUrlMeta(buildShlinkApiClient)(shortCode, undefined, meta)(dispatch, getState); } catch (e) { expect(e).toBe(error); }