Replaced redux action to create one visit by action that allows multiple visits at once

This commit is contained in:
Alejandro Celaya 2020-09-12 11:31:44 +02:00
parent ad437f655e
commit 6fc4963663
14 changed files with 73 additions and 55 deletions

View file

@ -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<T = {}>(
const pendingUpdates = new Set<CreateVisit>();
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);

View file

@ -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<string> {
}
export type ListShortUrlsCombinedAction = (
ListShortUrlsAction & EditShortUrlTagsAction & ShortUrlEditedAction & ShortUrlMetaEditedAction & CreateVisitAction
ListShortUrlsAction & EditShortUrlTagsAction & ShortUrlEditedAction & ShortUrlMetaEditedAction & CreateVisitsAction
);
const initialState: ShortUrlsList = {
@ -63,12 +63,17 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
[SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl<EditShortUrlTagsAction>('tags'),
[SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl<ShortUrlMetaEditedAction>('meta'),
[SHORT_URL_EDITED]: setPropFromActionOnMatchingShortUrl<ShortUrlEditedAction>('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,
),

View file

@ -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');

View file

@ -56,6 +56,7 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
</React.Fragment>
)}
onSuggestionsFetchRequested={() => {}}
onSuggestionsClearRequested={() => {}}
onSuggestionSelected={(_, { suggestion }: SuggestionSelectedEventData<string>) => {
addTag(suggestion);
}}

View file

@ -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<string> {
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<TagsList, ListTagsCombinedAction>({
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
@ -78,9 +90,9 @@ export default buildReducer<TagsList, ListTagsCombinedAction>({
...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);

View file

@ -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

View file

@ -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<string>, ShortUrlIdentifier {
visits: Visit[];
}
type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction & VisitsLoadProgressChangedAction & CreateVisitAction;
type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction & VisitsLoadProgressChangedAction & CreateVisitsAction;
const initialState: ShortUrlVisits = {
visits: [],
@ -49,14 +49,14 @@ export default buildReducer<ShortUrlVisits, ShortUrlVisitsCombinedAction>({
[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);

View file

@ -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<TagVisits, TagVisitsAction & VisitsLoadProgressChangedAction & CreateVisitAction>({
export default buildReducer<TagVisits, TagVisitsAction & VisitsLoadProgressChangedAction & CreateVisitsAction>({
[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);

View file

@ -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<typeof CREATE_VISIT> & CreateVisit;
export interface CreateVisitsAction extends Action<typeof CREATE_VISITS> {
createdVisits: CreateVisit[];
}
export const createNewVisit = ({ shortUrl, visit }: CreateVisit): CreateVisitAction => ({
type: CREATE_VISIT,
shortUrl,
visit,
export const createNewVisits = (createdVisits: CreateVisit[]): CreateVisitsAction => ({
type: CREATE_VISITS,
createdVisits,
});

View file

@ -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;

View file

@ -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 },

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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<ShortUrl>();
const visit = Mock.all<Visit>();
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 }] },
));
});
});