Added real time updates to tags list page

This commit is contained in:
Alejandro Celaya 2020-05-13 18:32:27 +02:00
parent e47dfaf36f
commit 1d26cd93fb
5 changed files with 51 additions and 15 deletions

View file

@ -4,6 +4,9 @@ import PropTypes from 'prop-types';
import Message from '../utils/Message'; import Message from '../utils/Message';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import { serverType } from '../servers/prop-types'; 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'; import { TagsListType } from './reducers/tagsList';
const { ceil } = Math; const { ceil } = Math;
@ -14,15 +17,26 @@ const propTypes = {
forceListTags: PropTypes.func, forceListTags: PropTypes.func,
tagsList: TagsListType, tagsList: TagsListType,
selectedServer: serverType, selectedServer: serverType,
createNewVisit: PropTypes.func,
loadMercureInfo: PropTypes.func,
mercureInfo: MercureInfoType,
settings: SettingsType,
}; };
const TagsList = (TagCard) => { 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(); const [ displayedTag, setDisplayedTag ] = useState();
useEffect(() => { useEffect(() => {
forceListTags(); forceListTags();
}, []); }, []);
useEffect(
bindToMercureTopic(mercureInfo, realTimeUpdates, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo),
[ mercureInfo ]
);
const renderContent = () => { const renderContent = () => {
if (tagsList.loading) { if (tagsList.loading) {

View file

@ -1,6 +1,7 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import { isEmpty, reject } from 'ramda'; import { isEmpty, reject } from 'ramda';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CREATE_VISIT } from '../../visits/reducers/visitCreation';
import { TAG_DELETED } from './tagDelete'; import { TAG_DELETED } from './tagDelete';
import { TAG_EDITED } from './tagEdit'; 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'; export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
/* eslint-enable padding-line-between-statements */ /* eslint-enable padding-line-between-statements */
const TagStatsType = PropTypes.shape({
shortUrlsCount: PropTypes.number,
visitsCount: PropTypes.number,
});
export const TagsListType = PropTypes.shape({ export const TagsListType = PropTypes.shape({
tags: PropTypes.arrayOf(PropTypes.string), tags: PropTypes.arrayOf(PropTypes.string),
filteredTags: PropTypes.arrayOf(PropTypes.string), filteredTags: PropTypes.arrayOf(PropTypes.string),
stats: PropTypes.object, // Record stats: PropTypes.objectOf(TagStatsType), // Record
loading: PropTypes.bool, loading: PropTypes.bool,
error: PropTypes.bool, error: PropTypes.bool,
}); });
@ -29,11 +35,23 @@ const initialState = {
const renameTag = (oldName, newName) => (tag) => tag === oldName ? newName : tag; const renameTag = (oldName, newName) => (tag) => tag === oldName ? newName : tag;
const rejectTag = (tags, tagToReject) => reject((tag) => tag === tagToReject, tags); 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({ export default handleActions({
[LIST_TAGS_START]: (state) => ({ ...state, loading: true, error: false }), [LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
[LIST_TAGS_ERROR]: (state) => ({ ...state, loading: false, error: true }), [LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }),
[LIST_TAGS]: (state, { tags, stats }) => ({ stats, tags, filteredTags: tags, loading: false, error: false }), [LIST_TAGS]: (state, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
[TAG_DELETED]: (state, { tag }) => ({ [TAG_DELETED]: (state, { tag }) => ({
...state, ...state,
tags: rejectTag(state.tags, tag), tags: rejectTag(state.tags, tag),
@ -48,6 +66,10 @@ export default handleActions({
...state, ...state,
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)), filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)),
}), }),
[CREATE_VISIT]: (state, { shortUrl }) => ({
...state,
stats: increaseVisitsForTags(shortUrl.tags, state.stats),
}),
}, initialState); }, initialState);
export const listTags = (buildShlinkApiClient, force = true) => () => async (dispatch, getState) => { 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) => ({ export const filterTags = (searchTerm) => ({ type: FILTER_TAGS, searchTerm });
type: FILTER_TAGS,
searchTerm,
});

View file

@ -28,7 +28,10 @@ const provideServices = (bottle, connect) => {
bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ])); bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ]));
bottle.serviceFactory('TagsList', TagsList, 'TagCard'); 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 // Actions
const listTagsActionFactory = (force) => ({ buildShlinkApiClient }) => listTags(buildShlinkApiClient, force); const listTagsActionFactory = (force) => ({ buildShlinkApiClient }) => listTags(buildShlinkApiClient, force);

View file

@ -15,7 +15,7 @@ describe('<TagsList />', () => {
const TagsList = createTagsList(TagCard); const TagsList = createTagsList(TagCard);
wrapper = shallow( wrapper = shallow(
<TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} /> <TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} settings={{}} />
); );
return wrapper; return wrapper;

View file

@ -11,17 +11,17 @@ import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit';
describe('tagsListReducer', () => { describe('tagsListReducer', () => {
describe('reducer', () => { describe('reducer', () => {
it('returns loading on LIST_TAGS_START', () => { 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, loading: true,
error: false, error: false,
}); }));
}); });
it('returns error on LIST_TAGS_ERROR', () => { 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, loading: false,
error: true, error: true,
}); }));
}); });
it('returns provided tags as filtered and regular tags on LIST_TAGS', () => { it('returns provided tags as filtered and regular tags on LIST_TAGS', () => {