From 1d26cd93fb1a4b8362d4015a0f9bd28115d67fe2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 13 May 2020 18:32:27 +0200 Subject: [PATCH] Added real time updates to tags list page --- src/tags/TagsList.js | 16 ++++++++++++- src/tags/reducers/tagsList.js | 35 +++++++++++++++++++++------- src/tags/services/provideServices.js | 5 +++- test/tags/TagsList.test.js | 2 +- test/tags/reducers/tagsList.test.js | 8 +++---- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/tags/TagsList.js b/src/tags/TagsList.js index 54b18c97..f0e70874 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -4,6 +4,9 @@ import PropTypes from 'prop-types'; import Message from '../utils/Message'; import SearchField from '../utils/SearchField'; import { serverType } from '../servers/prop-types'; +import { MercureInfoType } from '../mercure/reducers/mercureInfo'; +import { SettingsType } from '../settings/reducers/settings'; +import { bindToMercureTopic } from '../mercure/helpers'; import { TagsListType } from './reducers/tagsList'; const { ceil } = Math; @@ -14,15 +17,26 @@ const propTypes = { forceListTags: PropTypes.func, tagsList: TagsListType, selectedServer: serverType, + createNewVisit: PropTypes.func, + loadMercureInfo: PropTypes.func, + mercureInfo: MercureInfoType, + settings: SettingsType, }; const TagsList = (TagCard) => { - const TagListComp = ({ filterTags, forceListTags, tagsList, selectedServer }) => { + const TagListComp = ( + { filterTags, forceListTags, tagsList, selectedServer, createNewVisit, loadMercureInfo, mercureInfo, settings } + ) => { + const { realTimeUpdates } = settings; const [ displayedTag, setDisplayedTag ] = useState(); useEffect(() => { forceListTags(); }, []); + useEffect( + bindToMercureTopic(mercureInfo, realTimeUpdates, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo), + [ mercureInfo ] + ); const renderContent = () => { if (tagsList.loading) { diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index e810f2c7..643b3a94 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -1,6 +1,7 @@ import { handleActions } from 'redux-actions'; import { isEmpty, reject } from 'ramda'; import PropTypes from 'prop-types'; +import { CREATE_VISIT } from '../../visits/reducers/visitCreation'; import { TAG_DELETED } from './tagDelete'; import { TAG_EDITED } from './tagEdit'; @@ -11,10 +12,15 @@ export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS'; export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS'; /* eslint-enable padding-line-between-statements */ +const TagStatsType = PropTypes.shape({ + shortUrlsCount: PropTypes.number, + visitsCount: PropTypes.number, +}); + export const TagsListType = PropTypes.shape({ tags: PropTypes.arrayOf(PropTypes.string), filteredTags: PropTypes.arrayOf(PropTypes.string), - stats: PropTypes.object, // Record + stats: PropTypes.objectOf(TagStatsType), // Record loading: PropTypes.bool, error: PropTypes.bool, }); @@ -29,11 +35,23 @@ const initialState = { const renameTag = (oldName, newName) => (tag) => tag === oldName ? newName : tag; const rejectTag = (tags, tagToReject) => reject((tag) => tag === tagToReject, tags); +const increaseVisitsForTags = (tags, stats) => tags.reduce((stats, tag) => { + if (!stats[tag]) { + return stats; + } + + const tagStats = stats[tag]; + + tagStats.visitsCount = tagStats.visitsCount + 1; + stats[tag] = tagStats; + + return stats; +}, { ...stats }); export default handleActions({ - [LIST_TAGS_START]: (state) => ({ ...state, loading: true, error: false }), - [LIST_TAGS_ERROR]: (state) => ({ ...state, loading: false, error: true }), - [LIST_TAGS]: (state, { tags, stats }) => ({ stats, tags, filteredTags: tags, loading: false, error: false }), + [LIST_TAGS_START]: () => ({ ...initialState, loading: true }), + [LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }), + [LIST_TAGS]: (state, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }), [TAG_DELETED]: (state, { tag }) => ({ ...state, tags: rejectTag(state.tags, tag), @@ -48,6 +66,10 @@ export default handleActions({ ...state, filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)), }), + [CREATE_VISIT]: (state, { shortUrl }) => ({ + ...state, + stats: increaseVisitsForTags(shortUrl.tags, state.stats), + }), }, initialState); export const listTags = (buildShlinkApiClient, force = true) => () => async (dispatch, getState) => { @@ -74,7 +96,4 @@ export const listTags = (buildShlinkApiClient, force = true) => () => async (dis } }; -export const filterTags = (searchTerm) => ({ - type: FILTER_TAGS, - searchTerm, -}); +export const filterTags = (searchTerm) => ({ type: FILTER_TAGS, searchTerm }); diff --git a/src/tags/services/provideServices.js b/src/tags/services/provideServices.js index 566791ea..7917d068 100644 --- a/src/tags/services/provideServices.js +++ b/src/tags/services/provideServices.js @@ -28,7 +28,10 @@ const provideServices = (bottle, connect) => { bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ])); bottle.serviceFactory('TagsList', TagsList, 'TagCard'); - bottle.decorator('TagsList', connect([ 'tagsList', 'selectedServer' ], [ 'forceListTags', 'filterTags' ])); + bottle.decorator('TagsList', connect( + [ 'tagsList', 'selectedServer', 'mercureInfo', 'settings' ], + [ 'forceListTags', 'filterTags', 'createNewVisit', 'loadMercureInfo' ] + )); // Actions const listTagsActionFactory = (force) => ({ buildShlinkApiClient }) => listTags(buildShlinkApiClient, force); diff --git a/test/tags/TagsList.test.js b/test/tags/TagsList.test.js index c3bd3393..b4ebcafa 100644 --- a/test/tags/TagsList.test.js +++ b/test/tags/TagsList.test.js @@ -15,7 +15,7 @@ describe('', () => { const TagsList = createTagsList(TagCard); wrapper = shallow( - + ); return wrapper; diff --git a/test/tags/reducers/tagsList.test.js b/test/tags/reducers/tagsList.test.js index c312fea8..da86b564 100644 --- a/test/tags/reducers/tagsList.test.js +++ b/test/tags/reducers/tagsList.test.js @@ -11,17 +11,17 @@ import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit'; describe('tagsListReducer', () => { describe('reducer', () => { it('returns loading on LIST_TAGS_START', () => { - expect(reducer({}, { type: LIST_TAGS_START })).toEqual({ + expect(reducer({}, { type: LIST_TAGS_START })).toEqual(expect.objectContaining({ loading: true, error: false, - }); + })); }); it('returns error on LIST_TAGS_ERROR', () => { - expect(reducer({}, { type: LIST_TAGS_ERROR })).toEqual({ + expect(reducer({}, { type: LIST_TAGS_ERROR })).toEqual(expect.objectContaining({ loading: false, error: true, - }); + })); }); it('returns provided tags as filtered and regular tags on LIST_TAGS', () => {