From 6fc4963663d858c877d101b01cad955b66a9a17e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 12 Sep 2020 11:31:44 +0200 Subject: [PATCH] Replaced redux action to create one visit by action that allows multiple visits at once --- src/mercure/helpers/boundToMercureHub.tsx | 8 +++---- src/short-urls/reducers/shortUrlsList.ts | 21 +++++++++------- src/short-urls/services/provideServices.ts | 2 +- src/tags/helpers/TagsSelector.tsx | 1 + src/tags/reducers/tagsList.ts | 24 ++++++++++++++----- src/tags/services/provideServices.ts | 2 +- src/visits/reducers/shortUrlVisits.ts | 14 +++++------ src/visits/reducers/tagVisits.ts | 15 ++++++------ src/visits/reducers/visitCreation.ts | 13 +++++----- src/visits/services/provideServices.ts | 8 +++---- .../short-urls/reducers/shortUrlsList.test.ts | 4 ++-- test/visits/reducers/shortUrlVisits.test.ts | 4 ++-- test/visits/reducers/tagVisits.test.ts | 4 ++-- test/visits/reducers/visitCreation.test.ts | 8 +++---- 14 files changed, 73 insertions(+), 55 deletions(-) diff --git a/src/mercure/helpers/boundToMercureHub.tsx b/src/mercure/helpers/boundToMercureHub.tsx index b71fa421..1960aec3 100644 --- a/src/mercure/helpers/boundToMercureHub.tsx +++ b/src/mercure/helpers/boundToMercureHub.tsx @@ -4,7 +4,7 @@ import { MercureInfo } from '../reducers/mercureInfo'; import { bindToMercureTopic } from './index'; export interface MercureBoundProps { - createNewVisit: (visitData: CreateVisit) => void; + createNewVisits: (createdVisits: CreateVisit[]) => void; loadMercureInfo: Function; mercureInfo: MercureInfo; } @@ -16,14 +16,14 @@ export function boundToMercureHub( const pendingUpdates = new Set(); return (props: MercureBoundProps & T) => { - const { createNewVisit, loadMercureInfo, mercureInfo } = props; + const { createNewVisits, loadMercureInfo, mercureInfo } = props; const { interval } = mercureInfo; useEffect(() => { - const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisit(visit); + const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]); interval && setInterval(() => { - pendingUpdates.forEach(createNewVisit); + createNewVisits([ ...pendingUpdates ]); pendingUpdates.clear(); }, interval * 1000 * 60); diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts index 4370fb20..a07e50b4 100644 --- a/src/short-urls/reducers/shortUrlsList.ts +++ b/src/short-urls/reducers/shortUrlsList.ts @@ -1,7 +1,7 @@ -import { assoc, assocPath, reject } from 'ramda'; +import { assoc, assocPath, last, reject } from 'ramda'; import { Action, Dispatch } from 'redux'; import { shortUrlMatches } from '../helpers'; -import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCreation'; +import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation'; import { ShortUrl, ShortUrlIdentifier } from '../data'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; @@ -31,7 +31,7 @@ export interface ListShortUrlsAction extends Action { } export type ListShortUrlsCombinedAction = ( - ListShortUrlsAction & EditShortUrlTagsAction & ShortUrlEditedAction & ShortUrlMetaEditedAction & CreateVisitAction + ListShortUrlsAction & EditShortUrlTagsAction & ShortUrlEditedAction & ShortUrlMetaEditedAction & CreateVisitsAction ); const initialState: ShortUrlsList = { @@ -63,12 +63,17 @@ export default buildReducer({ [SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl('tags'), [SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl('meta'), [SHORT_URL_EDITED]: setPropFromActionOnMatchingShortUrl('longUrl'), - [CREATE_VISIT]: (state, { shortUrl: { shortCode, domain, visitsCount } }) => assocPath( + [CREATE_VISITS]: (state, { createdVisits }) => assocPath( [ 'shortUrls', 'data' ], - state.shortUrls && state.shortUrls.data && state.shortUrls.data.map( - (shortUrl) => shortUrlMatches(shortUrl, shortCode, domain) - ? assoc('visitsCount', visitsCount, shortUrl) - : shortUrl, + state.shortUrls?.data?.map( + (currentShortUrl) => { + // Find the last of the new visit for this short URL, and pick the amount of visits from it + const lastVisit = last( + createdVisits.filter(({ shortUrl }) => shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain)), + ); + + return lastVisit ? assoc('visitsCount', lastVisit.shortUrl.visitsCount, currentShortUrl) : currentShortUrl; + }, ), state, ), diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts index 8e86e7c9..b7ee0102 100644 --- a/src/short-urls/services/provideServices.ts +++ b/src/short-urls/services/provideServices.ts @@ -35,7 +35,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); bottle.decorator('ShortUrlsList', connect( [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo' ], - [ 'listShortUrls', 'resetShortUrlParams', 'createNewVisit', 'loadMercureInfo' ], + [ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ], )); bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout'); diff --git a/src/tags/helpers/TagsSelector.tsx b/src/tags/helpers/TagsSelector.tsx index 63da0deb..361f4737 100644 --- a/src/tags/helpers/TagsSelector.tsx +++ b/src/tags/helpers/TagsSelector.tsx @@ -56,6 +56,7 @@ const TagsSelector = (colorGenerator: ColorGenerator) => ( )} onSuggestionsFetchRequested={() => {}} + onSuggestionsClearRequested={() => {}} onSuggestionSelected={(_, { suggestion }: SuggestionSelectedEventData) => { addTag(suggestion); }} diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index ea6a6711..e33ba2ff 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -1,11 +1,12 @@ import { isEmpty, reject } from 'ramda'; import { Action, Dispatch } from 'redux'; -import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCreation'; +import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation'; import { buildReducer } from '../../utils/helpers/redux'; import { ShlinkTags } from '../../utils/services/types'; import { GetState } from '../../container/types'; import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder'; import { TagStats } from '../data'; +import { CreateVisit, Stats } from '../../visits/types'; import { DeleteTagAction, TAG_DELETED } from './tagDelete'; import { EditTagAction, TAG_EDITED } from './tagEdit'; @@ -35,7 +36,7 @@ interface FilterTagsAction extends Action { searchTerm: string; } -type ListTagsCombinedAction = ListTagsAction & DeleteTagAction & CreateVisitAction & EditTagAction & FilterTagsAction; +type ListTagsCombinedAction = ListTagsAction & DeleteTagAction & CreateVisitsAction & EditTagAction & FilterTagsAction; const initialState = { tags: [], @@ -45,20 +46,31 @@ const initialState = { error: false, }; +type TagIncrease = [string, number]; + const renameTag = (oldName: string, newName: string) => (tag: string) => tag === oldName ? newName : tag; const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags); -const increaseVisitsForTags = (tags: string[], stats: TagsStatsMap) => tags.reduce((stats, tag) => { +const increaseVisitsForTags = (tags: TagIncrease[], stats: TagsStatsMap) => tags.reduce((stats, [ tag, increase ]) => { if (!stats[tag]) { return stats; } const tagStats = stats[tag]; - tagStats.visitsCount = tagStats.visitsCount + 1; + tagStats.visitsCount = tagStats.visitsCount + increase; stats[tag] = tagStats; return stats; }, { ...stats }); +const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => Object.entries( + createdVisits.reduce((acc, { shortUrl }) => { + shortUrl.tags.forEach((tag) => { + acc[tag] = (acc[tag] || 0) + 1; + }); + + return acc; + }, {} as Stats), +); export default buildReducer({ [LIST_TAGS_START]: () => ({ ...initialState, loading: true }), @@ -78,9 +90,9 @@ export default buildReducer({ ...state, filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)), }), - [CREATE_VISIT]: (state, { shortUrl }) => ({ + [CREATE_VISITS]: (state, { createdVisits }) => ({ ...state, - stats: increaseVisitsForTags(shortUrl.tags, state.stats), + stats: increaseVisitsForTags(calculateVisitsPerTag(createdVisits), state.stats), }), }, initialState); diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts index be38b20d..3e71c32f 100644 --- a/src/tags/services/provideServices.ts +++ b/src/tags/services/provideServices.ts @@ -32,7 +32,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('TagsList', TagsList, 'TagCard'); bottle.decorator('TagsList', connect( [ 'tagsList', 'selectedServer', 'mercureInfo' ], - [ 'forceListTags', 'filterTags', 'createNewVisit', 'loadMercureInfo' ], + [ 'forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo' ], )); // Actions diff --git a/src/visits/reducers/shortUrlVisits.ts b/src/visits/reducers/shortUrlVisits.ts index 5f56121e..a7ee4407 100644 --- a/src/visits/reducers/shortUrlVisits.ts +++ b/src/visits/reducers/shortUrlVisits.ts @@ -7,7 +7,7 @@ import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuil import { GetState } from '../../container/types'; import { OptionalString } from '../../utils/utils'; import { getVisitsWithLoader } from './common'; -import { CREATE_VISIT, CreateVisitAction } from './visitCreation'; +import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; /* eslint-disable padding-line-between-statements */ export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; @@ -24,7 +24,7 @@ interface ShortUrlVisitsAction extends Action, ShortUrlIdentifier { visits: Visit[]; } -type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction & VisitsLoadProgressChangedAction & CreateVisitAction; +type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction & VisitsLoadProgressChangedAction & CreateVisitsAction; const initialState: ShortUrlVisits = { visits: [], @@ -49,14 +49,14 @@ export default buildReducer({ [GET_SHORT_URL_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }), [GET_SHORT_URL_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_SHORT_URL_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), - [CREATE_VISIT]: (state, { shortUrl, visit }) => { // eslint-disable-line object-shorthand + [CREATE_VISITS]: (state, { createdVisits }) => { // eslint-disable-line object-shorthand const { shortCode, domain, visits } = state; - if (!shortUrlMatches(shortUrl, shortCode, domain)) { - return state; - } + const newVisits = createdVisits + .filter(({ shortUrl }) => shortUrlMatches(shortUrl, shortCode, domain)) + .map(({ visit }) => visit); - return { ...state, visits: [ ...visits, visit ] }; + return { ...state, visits: [ ...visits, ...newVisits ] }; }, }, initialState); diff --git a/src/visits/reducers/tagVisits.ts b/src/visits/reducers/tagVisits.ts index 76491994..04fcc498 100644 --- a/src/visits/reducers/tagVisits.ts +++ b/src/visits/reducers/tagVisits.ts @@ -4,7 +4,7 @@ import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder'; import { GetState } from '../../container/types'; import { getVisitsWithLoader } from './common'; -import { CREATE_VISIT, CreateVisitAction } from './visitCreation'; +import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; /* eslint-disable padding-line-between-statements */ export const GET_TAG_VISITS_START = 'shlink/tagVisits/GET_TAG_VISITS_START'; @@ -34,21 +34,20 @@ const initialState: TagVisits = { progress: 0, }; -export default buildReducer({ +export default buildReducer({ [GET_TAG_VISITS_START]: () => ({ ...initialState, loading: true }), [GET_TAG_VISITS_ERROR]: () => ({ ...initialState, error: true }), [GET_TAG_VISITS]: (_, { visits, tag }) => ({ ...initialState, visits, tag }), [GET_TAG_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }), [GET_TAG_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), [GET_TAG_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), - [CREATE_VISIT]: (state, { shortUrl, visit }) => { // eslint-disable-line object-shorthand + [CREATE_VISITS]: (state, { createdVisits }) => { // eslint-disable-line object-shorthand const { tag, visits } = state; + const newVisits = createdVisits + .filter(({ shortUrl }) => shortUrl.tags.includes(tag)) + .map(({ visit }) => visit); - if (!shortUrl.tags.includes(tag)) { - return state; - } - - return { ...state, visits: [ ...visits, visit ] }; + return { ...state, visits: [ ...visits, ...newVisits ] }; }, }, initialState); diff --git a/src/visits/reducers/visitCreation.ts b/src/visits/reducers/visitCreation.ts index 3b89318d..e2335fc4 100644 --- a/src/visits/reducers/visitCreation.ts +++ b/src/visits/reducers/visitCreation.ts @@ -1,12 +1,13 @@ import { Action } from 'redux'; import { CreateVisit } from '../types'; -export const CREATE_VISIT = 'shlink/visitCreation/CREATE_VISIT'; +export const CREATE_VISITS = 'shlink/visitCreation/CREATE_VISITS'; -export type CreateVisitAction = Action & CreateVisit; +export interface CreateVisitsAction extends Action { + createdVisits: CreateVisit[]; +} -export const createNewVisit = ({ shortUrl, visit }: CreateVisit): CreateVisitAction => ({ - type: CREATE_VISIT, - shortUrl, - visit, +export const createNewVisits = (createdVisits: CreateVisit[]): CreateVisitsAction => ({ + type: CREATE_VISITS, + createdVisits, }); diff --git a/src/visits/services/provideServices.ts b/src/visits/services/provideServices.ts index 80372ebc..5f51c685 100644 --- a/src/visits/services/provideServices.ts +++ b/src/visits/services/provideServices.ts @@ -3,7 +3,7 @@ import ShortUrlVisits from '../ShortUrlVisits'; import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits'; import { getShortUrlDetail } from '../reducers/shortUrlDetail'; import MapModal from '../helpers/MapModal'; -import { createNewVisit } from '../reducers/visitCreation'; +import { createNewVisits } from '../reducers/visitCreation'; import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits'; import TagVisits from '../TagVisits'; import { ConnectDecorator } from '../../container/types'; @@ -15,12 +15,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ShortUrlVisits', () => ShortUrlVisits); bottle.decorator('ShortUrlVisits', connect( [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ], - [ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisit', 'loadMercureInfo' ], + [ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ], )); bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator'); bottle.decorator('TagVisits', connect( [ 'tagVisits', 'mercureInfo' ], - [ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisit', 'loadMercureInfo' ], + [ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ], )); // Services @@ -34,7 +34,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient'); bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits); - bottle.serviceFactory('createNewVisit', () => createNewVisit); + bottle.serviceFactory('createNewVisits', () => createNewVisits); }; export default provideServices; diff --git a/test/short-urls/reducers/shortUrlsList.test.ts b/test/short-urls/reducers/shortUrlsList.test.ts index ea77b662..e677b352 100644 --- a/test/short-urls/reducers/shortUrlsList.test.ts +++ b/test/short-urls/reducers/shortUrlsList.test.ts @@ -8,7 +8,7 @@ import reducer, { import { SHORT_URL_TAGS_EDITED } from '../../../src/short-urls/reducers/shortUrlTags'; import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { SHORT_URL_META_EDITED } from '../../../src/short-urls/reducers/shortUrlMeta'; -import { CREATE_VISIT } from '../../../src/visits/reducers/visitCreation'; +import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { ShortUrl } from '../../../src/short-urls/data'; import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient'; import { ShlinkShortUrlsResponse } from '../../../src/utils/services/types'; @@ -135,7 +135,7 @@ describe('shortUrlsListReducer', () => { error: false, }; - expect(reducer(state, { type: CREATE_VISIT, shortUrl } as any)).toEqual({ + expect(reducer(state, { type: CREATE_VISITS, createdVisits: [{ shortUrl }] } as any)).toEqual({ shortUrls: { data: [ { shortCode, domain: 'example.com', visitsCount: 5 }, diff --git a/test/visits/reducers/shortUrlVisits.test.ts b/test/visits/reducers/shortUrlVisits.test.ts index e71bc38c..8863f4f1 100644 --- a/test/visits/reducers/shortUrlVisits.test.ts +++ b/test/visits/reducers/shortUrlVisits.test.ts @@ -10,7 +10,7 @@ import reducer, { GET_SHORT_URL_VISITS_PROGRESS_CHANGED, ShortUrlVisits, } from '../../../src/visits/reducers/shortUrlVisits'; -import { CREATE_VISIT } from '../../../src/visits/reducers/visitCreation'; +import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/utils/services/types'; @@ -77,7 +77,7 @@ describe('shortUrlVisitsReducer', () => { visits: visitsMocks, }); - const { visits } = reducer(prevState, { type: CREATE_VISIT, shortUrl, visit: {} } as any); + const { visits } = reducer(prevState, { type: CREATE_VISITS, createdVisits: [{ shortUrl, visit: {} }] } as any); expect(visits).toEqual(expectedVisits); }); diff --git a/test/visits/reducers/tagVisits.test.ts b/test/visits/reducers/tagVisits.test.ts index d7fc93c6..caf300bb 100644 --- a/test/visits/reducers/tagVisits.test.ts +++ b/test/visits/reducers/tagVisits.test.ts @@ -10,7 +10,7 @@ import reducer, { GET_TAG_VISITS_PROGRESS_CHANGED, TagVisits, } from '../../../src/visits/reducers/tagVisits'; -import { CREATE_VISIT } from '../../../src/visits/reducers/visitCreation'; +import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { rangeOf } from '../../../src/utils/utils'; import { Visit } from '../../../src/visits/types'; import { ShlinkVisits } from '../../../src/utils/services/types'; @@ -77,7 +77,7 @@ describe('tagVisitsReducer', () => { visits: visitsMocks, }); - const { visits } = reducer(prevState, { type: CREATE_VISIT, shortUrl, visit: {} } as any); + const { visits } = reducer(prevState, { type: CREATE_VISITS, createdVisits: [{ shortUrl, visit: {} }] } as any); expect(visits).toEqual(expectedVisits); }); diff --git a/test/visits/reducers/visitCreation.test.ts b/test/visits/reducers/visitCreation.test.ts index 4149f99c..ecfb1dd5 100644 --- a/test/visits/reducers/visitCreation.test.ts +++ b/test/visits/reducers/visitCreation.test.ts @@ -1,16 +1,16 @@ import { Mock } from 'ts-mockery'; -import { CREATE_VISIT, createNewVisit } from '../../../src/visits/reducers/visitCreation'; +import { CREATE_VISITS, createNewVisits } from '../../../src/visits/reducers/visitCreation'; import { ShortUrl } from '../../../src/short-urls/data'; import { Visit } from '../../../src/visits/types'; describe('visitCreationReducer', () => { - describe('createNewVisit', () => { + describe('createNewVisits', () => { const shortUrl = Mock.all(); const visit = Mock.all(); it('just returns the action with proper type', () => - expect(createNewVisit({ shortUrl, visit })).toEqual( - { type: CREATE_VISIT, shortUrl, visit }, + expect(createNewVisits([{ shortUrl, visit }])).toEqual( + { type: CREATE_VISITS, createdVisits: [{ shortUrl, visit }] }, )); }); });