mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #299 from acelaya-forks/feature/updates-interval
Feature/updates interval
This commit is contained in:
commit
bd88e56331
33 changed files with 226 additions and 128 deletions
|
@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
* [#289](https://github.com/shlinkio/shlink-web-client/issues/289) Client and server version constraints are now links to the corresponding project release notes.
|
||||
* [#293](https://github.com/shlinkio/shlink-web-client/issues/293) Shlink versions are now always displayed in footer, hiding the server version when there's no connected server.
|
||||
* [#250](https://github.com/shlinkio/shlink-web-client/issues/250) Added support to group real time updates in fixed intervals.
|
||||
|
||||
The settings page now allows to provide the interval in which the UI should get updated, making that happen at once, with all the updates that have happened during that interval.
|
||||
|
||||
By default updates are immediately applied if real-time updates are enabled, to keep the behavior as it was.
|
||||
|
||||
#### Changed
|
||||
|
||||
|
|
41
src/mercure/helpers/boundToMercureHub.tsx
Normal file
41
src/mercure/helpers/boundToMercureHub.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { pipe } from 'ramda';
|
||||
import { CreateVisit } from '../../visits/types';
|
||||
import { MercureInfo } from '../reducers/mercureInfo';
|
||||
import { bindToMercureTopic } from './index';
|
||||
|
||||
export interface MercureBoundProps {
|
||||
createNewVisits: (createdVisits: CreateVisit[]) => void;
|
||||
loadMercureInfo: Function;
|
||||
mercureInfo: MercureInfo;
|
||||
}
|
||||
|
||||
export function boundToMercureHub<T = {}>(
|
||||
WrappedComponent: FC<MercureBoundProps & T>,
|
||||
getTopicForProps: (props: T) => string,
|
||||
) {
|
||||
const pendingUpdates = new Set<CreateVisit>();
|
||||
|
||||
return (props: MercureBoundProps & T) => {
|
||||
const { createNewVisits, loadMercureInfo, mercureInfo } = props;
|
||||
const { interval } = mercureInfo;
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (visit: CreateVisit) => interval ? pendingUpdates.add(visit) : createNewVisits([ visit ]);
|
||||
const closeEventSource = bindToMercureTopic(mercureInfo, getTopicForProps(props), onMessage, loadMercureInfo);
|
||||
|
||||
if (!interval) {
|
||||
return closeEventSource;
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
createNewVisits([ ...pendingUpdates ]);
|
||||
pendingUpdates.clear();
|
||||
}, interval * 1000 * 60);
|
||||
|
||||
return pipe(() => clearInterval(timer), () => closeEventSource?.());
|
||||
}, [ mercureInfo ]);
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
import { MercureInfo } from '../reducers/mercureInfo';
|
||||
|
||||
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => () => { // eslint-disable-line max-len
|
||||
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => { // eslint-disable-line max-len
|
||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||
|
||||
if (loading || error || !mercureHubUrl) {
|
||||
|
@ -23,18 +22,3 @@ export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, o
|
|||
|
||||
return () => es.close();
|
||||
};
|
||||
|
||||
export const useMercureTopicBinding = <T>(
|
||||
mercureInfo: MercureInfo,
|
||||
topic: string,
|
||||
onMessage: (message: T) => void,
|
||||
onTokenExpired: Function,
|
||||
) => {
|
||||
useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]);
|
||||
};
|
||||
|
||||
export interface MercureBoundProps {
|
||||
createNewVisit: (message: any) => void;
|
||||
loadMercureInfo: Function;
|
||||
mercureInfo: MercureInfo;
|
||||
}
|
||||
|
|
|
@ -13,11 +13,12 @@ export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
|
|||
export interface MercureInfo {
|
||||
token?: string;
|
||||
mercureHubUrl?: string;
|
||||
interval?: number;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export type GetMercureInfoAction = Action<string> & ShlinkMercureInfo;
|
||||
export type GetMercureInfoAction = Action<string> & ShlinkMercureInfo & { interval?: number };
|
||||
|
||||
const initialState: MercureInfo = {
|
||||
loading: true,
|
||||
|
@ -27,7 +28,7 @@ const initialState: MercureInfo = {
|
|||
export default buildReducer<MercureInfo, GetMercureInfoAction>({
|
||||
[GET_MERCURE_INFO_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||
[GET_MERCURE_INFO_ERROR]: (state) => ({ ...state, loading: false, error: true }),
|
||||
[GET_MERCURE_INFO]: (_, { token, mercureHubUrl }) => ({ token, mercureHubUrl, loading: false, error: false }),
|
||||
[GET_MERCURE_INFO]: (_, action) => ({ ...action, loading: false, error: false }),
|
||||
}, initialState);
|
||||
|
||||
export const loadMercureInfo = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
||||
|
@ -44,9 +45,9 @@ export const loadMercureInfo = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
|||
}
|
||||
|
||||
try {
|
||||
const result = await mercureInfo();
|
||||
const info = await mercureInfo();
|
||||
|
||||
dispatch<GetMercureInfoAction>({ type: GET_MERCURE_INFO, ...result });
|
||||
dispatch<GetMercureInfoAction>({ type: GET_MERCURE_INFO, interval: settings.realTimeUpdates.interval, ...info });
|
||||
} catch (e) {
|
||||
dispatch({ type: GET_MERCURE_INFO_ERROR });
|
||||
}
|
||||
|
|
|
@ -1,20 +1,51 @@
|
|||
import React from 'react';
|
||||
import { Card, CardBody, CardHeader } from 'reactstrap';
|
||||
import { Card, CardBody, CardHeader, FormGroup, Input } from 'reactstrap';
|
||||
import classNames from 'classnames';
|
||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
||||
import { Settings } from './reducers/settings';
|
||||
|
||||
interface RealTimeUpdatesProps {
|
||||
settings: Settings;
|
||||
setRealTimeUpdates: (enabled: boolean) => void;
|
||||
toggleRealTimeUpdates: (enabled: boolean) => void;
|
||||
setRealTimeUpdatesInterval: (interval: number) => void;
|
||||
}
|
||||
|
||||
const RealTimeUpdates = ({ settings: { realTimeUpdates }, setRealTimeUpdates }: RealTimeUpdatesProps) => (
|
||||
const intervalValue = (interval?: number) => !interval ? '' : `${interval}`;
|
||||
|
||||
const RealTimeUpdates = (
|
||||
{ settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps,
|
||||
) => (
|
||||
<Card>
|
||||
<CardHeader>Real-time updates</CardHeader>
|
||||
<CardBody>
|
||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={setRealTimeUpdates}>
|
||||
<FormGroup>
|
||||
<ToggleSwitch checked={realTimeUpdates.enabled} onChange={toggleRealTimeUpdates}>
|
||||
Enable or disable real-time updates, when using Shlink v2.2.0 or newer.
|
||||
</ToggleSwitch>
|
||||
</FormGroup>
|
||||
<FormGroup className="mb-0">
|
||||
<label className={classNames({ 'text-muted': !realTimeUpdates.enabled })}>
|
||||
Real-time updates frequency (in minutes):
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="Immediate"
|
||||
disabled={!realTimeUpdates.enabled}
|
||||
value={intervalValue(realTimeUpdates.interval)}
|
||||
onChange={(e) => setRealTimeUpdatesInterval(Number(e.target.value))}
|
||||
/>
|
||||
{realTimeUpdates.enabled && (
|
||||
<small className="form-text text-muted">
|
||||
{realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && (
|
||||
<span>
|
||||
Updates will be reflected in the UI every <b>{realTimeUpdates.interval}</b> minute{realTimeUpdates.interval > 1 && 's'}.
|
||||
</span>
|
||||
)}
|
||||
{!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'}
|
||||
</small>
|
||||
)}
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { Action } from 'redux';
|
||||
import { mergeDeepRight } from 'ramda';
|
||||
import { buildReducer } from '../../utils/helpers/redux';
|
||||
import { RecursivePartial } from '../../utils/utils';
|
||||
|
||||
export const SET_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/SET_REAL_TIME_UPDATES';
|
||||
|
||||
interface RealTimeUpdates {
|
||||
enabled: boolean;
|
||||
interval?: number;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
|
@ -19,11 +22,18 @@ const initialState: Settings = {
|
|||
|
||||
type SettingsAction = Action & Settings;
|
||||
|
||||
type PartialSettingsAction = Action & RecursivePartial<Settings>;
|
||||
|
||||
export default buildReducer<Settings, SettingsAction>({
|
||||
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
|
||||
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => mergeDeepRight(state, { realTimeUpdates }),
|
||||
}, initialState);
|
||||
|
||||
export const setRealTimeUpdates = (enabled: boolean): SettingsAction => ({
|
||||
export const toggleRealTimeUpdates = (enabled: boolean): PartialSettingsAction => ({
|
||||
type: SET_REAL_TIME_UPDATES,
|
||||
realTimeUpdates: { enabled },
|
||||
});
|
||||
|
||||
export const setRealTimeUpdatesInterval = (interval: number): PartialSettingsAction => ({
|
||||
type: SET_REAL_TIME_UPDATES,
|
||||
realTimeUpdates: { interval },
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Bottle from 'bottlejs';
|
||||
import RealTimeUpdates from '../RealTimeUpdates';
|
||||
import Settings from '../Settings';
|
||||
import { setRealTimeUpdates } from '../reducers/settings';
|
||||
import { setRealTimeUpdatesInterval, toggleRealTimeUpdates } from '../reducers/settings';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||
|
||||
|
@ -13,10 +13,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
|
||||
// Services
|
||||
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
|
||||
bottle.decorator('RealTimeUpdates', connect([ 'settings' ], [ 'setRealTimeUpdates' ]));
|
||||
bottle.decorator(
|
||||
'RealTimeUpdates',
|
||||
connect([ 'settings' ], [ 'toggleRealTimeUpdates', 'setRealTimeUpdatesInterval' ]),
|
||||
);
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('setRealTimeUpdates', () => setRealTimeUpdates);
|
||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
|
|
|
@ -6,8 +6,8 @@ import qs from 'qs';
|
|||
import { RouteComponentProps } from 'react-router';
|
||||
import SortingDropdown from '../utils/SortingDropdown';
|
||||
import { determineOrderDir, OrderDir } from '../utils/utils';
|
||||
import { MercureBoundProps, useMercureTopicBinding } from '../mercure/helpers';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
|
||||
import { ShortUrl } from './data';
|
||||
|
@ -31,14 +31,14 @@ export interface WithList {
|
|||
shortUrlsList: ShortUrl[];
|
||||
}
|
||||
|
||||
export interface ShortUrlsListProps extends ShortUrlsListState, RouteComponentProps<RouteParams>, MercureBoundProps {
|
||||
export interface ShortUrlsListProps extends ShortUrlsListState, RouteComponentProps<RouteParams> {
|
||||
selectedServer: SelectedServer;
|
||||
listShortUrls: (params: ShortUrlsListParams) => void;
|
||||
shortUrlsListParams: ShortUrlsListParams;
|
||||
resetShortUrlParams: () => void;
|
||||
}
|
||||
|
||||
const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
||||
const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => boundToMercureHub(({
|
||||
listShortUrls,
|
||||
resetShortUrlParams,
|
||||
shortUrlsListParams,
|
||||
|
@ -48,9 +48,6 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
|||
error,
|
||||
shortUrlsList,
|
||||
selectedServer,
|
||||
createNewVisit,
|
||||
loadMercureInfo,
|
||||
mercureInfo,
|
||||
}: ShortUrlsListProps & WithList) => {
|
||||
const { orderBy } = shortUrlsListParams;
|
||||
const [ order, setOrder ] = useState<{ orderField?: OrderableFields; orderDir?: OrderDir }>({
|
||||
|
@ -116,7 +113,6 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
|||
|
||||
return resetShortUrlParams;
|
||||
}, []);
|
||||
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -168,6 +164,6 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
|||
</table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
}, () => 'https://shlink.io/new-visit');
|
||||
|
||||
export default ShortUrlsList;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -2,30 +2,29 @@ import React, { FC, useEffect, useState } from 'react';
|
|||
import { splitEvery } from 'ramda';
|
||||
import Message from '../utils/Message';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import { MercureBoundProps, useMercureTopicBinding } from '../mercure/helpers';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { TagsList as TagsListState } from './reducers/tagsList';
|
||||
import { TagCardProps } from './TagCard';
|
||||
|
||||
const { ceil } = Math;
|
||||
const TAGS_GROUPS_AMOUNT = 4;
|
||||
|
||||
export interface TagsListProps extends MercureBoundProps {
|
||||
export interface TagsListProps {
|
||||
filterTags: (searchTerm: string) => void;
|
||||
forceListTags: Function;
|
||||
tagsList: TagsListState;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const TagsList = (TagCard: FC<TagCardProps>) => (
|
||||
{ filterTags, forceListTags, tagsList, selectedServer, createNewVisit, loadMercureInfo, mercureInfo }: TagsListProps,
|
||||
const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
||||
{ filterTags, forceListTags, tagsList, selectedServer }: TagsListProps,
|
||||
) => {
|
||||
const [ displayedTag, setDisplayedTag ] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
forceListTags();
|
||||
}, []);
|
||||
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
|
||||
|
||||
const renderContent = () => {
|
||||
if (tagsList.loading) {
|
||||
|
@ -76,6 +75,6 @@ const TagsList = (TagCard: FC<TagCardProps>) => (
|
|||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
}, () => 'https://shlink.io/new-visit');
|
||||
|
||||
export default TagsList;
|
||||
|
|
|
@ -56,6 +56,7 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
|||
</React.Fragment>
|
||||
)}
|
||||
onSuggestionsFetchRequested={() => {}}
|
||||
onSuggestionsClearRequested={() => {}}
|
||||
onSuggestionSelected={(_, { suggestion }: SuggestionSelectedEventData<string>) => {
|
||||
addTag(suggestion);
|
||||
}}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,3 +39,7 @@ export type Nullable<T> = {
|
|||
type Optional<T> = T | null | undefined;
|
||||
|
||||
export type OptionalString = Optional<string>;
|
||||
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: RecursivePartial<T[P]>;
|
||||
};
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import qs from 'qs';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { MercureBoundProps, useMercureTopicBinding } from '../mercure/helpers';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { ShlinkVisitsParams } from '../utils/services/types';
|
||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||
import { ShortUrlDetail } from './reducers/shortUrlDetail';
|
||||
import VisitsStats from './VisitsStats';
|
||||
|
||||
export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: string }>, MercureBoundProps {
|
||||
export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: string }> {
|
||||
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
||||
shortUrlVisits: ShortUrlVisitsState;
|
||||
getShortUrlDetail: Function;
|
||||
|
@ -16,7 +16,7 @@ export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: st
|
|||
cancelGetShortUrlVisits: () => void;
|
||||
}
|
||||
|
||||
const ShortUrlVisits = ({
|
||||
const ShortUrlVisits = boundToMercureHub(({
|
||||
history: { goBack },
|
||||
match,
|
||||
location: { search },
|
||||
|
@ -25,9 +25,6 @@ const ShortUrlVisits = ({
|
|||
getShortUrlVisits,
|
||||
getShortUrlDetail,
|
||||
cancelGetShortUrlVisits,
|
||||
createNewVisit,
|
||||
loadMercureInfo,
|
||||
mercureInfo,
|
||||
}: ShortUrlVisitsProps) => {
|
||||
const { params } = match;
|
||||
const { shortCode } = params;
|
||||
|
@ -38,13 +35,12 @@ const ShortUrlVisits = ({
|
|||
useEffect(() => {
|
||||
getShortUrlDetail(shortCode, domain);
|
||||
}, []);
|
||||
useMercureTopicBinding(mercureInfo, `https://shlink.io/new-visit/${shortCode}`, createNewVisit, loadMercureInfo);
|
||||
|
||||
return (
|
||||
<VisitsStats getVisits={loadVisits} cancelGetVisits={cancelGetShortUrlVisits} visitsInfo={shortUrlVisits}>
|
||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
);
|
||||
};
|
||||
}, ({ match }) => `https://shlink.io/new-visit/${match.params.shortCode}`);
|
||||
|
||||
export default ShortUrlVisits;
|
||||
|
|
|
@ -1,39 +1,34 @@
|
|||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { MercureBoundProps, useMercureTopicBinding } from '../mercure/helpers';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||
import { ShlinkVisitsParams } from '../utils/services/types';
|
||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||
import TagVisitsHeader from './TagVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
|
||||
export interface TagVisitsProps extends RouteComponentProps<{ tag: string }>, MercureBoundProps {
|
||||
export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
|
||||
getTagVisits: (tag: string, query: any) => void;
|
||||
tagVisits: TagVisitsState;
|
||||
cancelGetTagVisits: () => void;
|
||||
}
|
||||
|
||||
const TagVisits = (colorGenerator: ColorGenerator) => ({
|
||||
const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
||||
history: { goBack },
|
||||
match,
|
||||
getTagVisits,
|
||||
tagVisits,
|
||||
cancelGetTagVisits,
|
||||
createNewVisit,
|
||||
loadMercureInfo,
|
||||
mercureInfo,
|
||||
}: TagVisitsProps) => {
|
||||
const { params } = match;
|
||||
const { tag } = params;
|
||||
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
||||
|
||||
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
|
||||
|
||||
return (
|
||||
<VisitsStats getVisits={loadVisits} cancelGetVisits={cancelGetTagVisits} visitsInfo={tagVisits}>
|
||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||
</VisitsStats>
|
||||
);
|
||||
};
|
||||
}, () => 'https://shlink.io/new-visit');
|
||||
|
||||
export default TagVisits;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,10 +7,9 @@ import appFactory from '../src/App';
|
|||
describe('<App />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const MainHeader = () => null;
|
||||
const DummyComponent = () => null;
|
||||
|
||||
beforeEach(() => {
|
||||
const App = appFactory(MainHeader, DummyComponent, DummyComponent, DummyComponent, DummyComponent, DummyComponent);
|
||||
const App = appFactory(MainHeader, () => null, () => null, () => null, () => null, () => null, () => null);
|
||||
|
||||
wrapper = shallow(<App fetchServers={identity} servers={{}} />);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('helpers', () => {
|
|||
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
|
||||
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
|
||||
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
|
||||
bindToMercureTopic(mercureInfo, '', identity, identity)();
|
||||
bindToMercureTopic(mercureInfo, '', identity, identity);
|
||||
|
||||
expect(EventSource).not.toHaveBeenCalled();
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
|
@ -40,7 +40,7 @@ describe('helpers', () => {
|
|||
error: false,
|
||||
mercureHubUrl,
|
||||
token,
|
||||
}, topic, onMessage, onTokenExpired)();
|
||||
}, topic, onMessage, onTokenExpired);
|
||||
|
||||
expect(EventSource).toHaveBeenCalledWith(hubUrl, {
|
||||
headers: {
|
||||
|
|
|
@ -36,11 +36,11 @@ describe('mercureInfoReducer', () => {
|
|||
});
|
||||
|
||||
it('returns mercure info on GET_MERCURE_INFO', () => {
|
||||
expect(reducer(undefined, { type: GET_MERCURE_INFO, ...mercureInfo })).toEqual({
|
||||
expect(reducer(undefined, { type: GET_MERCURE_INFO, ...mercureInfo })).toEqual(expect.objectContaining({
|
||||
...mercureInfo,
|
||||
loading: false,
|
||||
error: false,
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import reducer, { SET_REAL_TIME_UPDATES, setRealTimeUpdates } from '../../../src/settings/reducers/settings';
|
||||
import reducer, { SET_REAL_TIME_UPDATES, toggleRealTimeUpdates, setRealTimeUpdatesInterval } from '../../../src/settings/reducers/settings';
|
||||
|
||||
describe('settingsReducer', () => {
|
||||
const realTimeUpdates = { enabled: true };
|
||||
|
@ -9,11 +9,19 @@ describe('settingsReducer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setRealTimeUpdates', () => {
|
||||
describe('toggleRealTimeUpdates', () => {
|
||||
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
|
||||
const result = setRealTimeUpdates(enabled);
|
||||
const result = toggleRealTimeUpdates(enabled);
|
||||
|
||||
expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { enabled } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRealTimeUpdatesInterval', () => {
|
||||
it.each([[ 0 ], [ 1 ], [ 2 ], [ 10 ]])('updates settings with provided value and then loads updates again', (interval) => {
|
||||
const result = setRealTimeUpdatesInterval(interval);
|
||||
|
||||
expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { interval } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawe
|
|||
import { Mock } from 'ts-mockery';
|
||||
import shortUrlsListCreator, { ShortUrlsListProps, SORTABLE_FIELDS } from '../../src/short-urls/ShortUrlsList';
|
||||
import { ShortUrl } from '../../src/short-urls/data';
|
||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
|
||||
describe('<ShortUrlsList />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
@ -18,6 +19,7 @@ describe('<ShortUrlsList />', () => {
|
|||
wrapper = shallow(
|
||||
<ShortUrlsList
|
||||
{...Mock.all<ShortUrlsListProps>()}
|
||||
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
|
||||
listShortUrls={listShortUrlsMock}
|
||||
resetShortUrlParams={resetShortUrlParamsMock}
|
||||
shortUrlsListParams={{
|
||||
|
@ -39,9 +41,8 @@ describe('<ShortUrlsList />', () => {
|
|||
}),
|
||||
]
|
||||
}
|
||||
mercureInfo={{ loading: true } as any}
|
||||
/>,
|
||||
);
|
||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
||||
});
|
||||
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -7,6 +7,7 @@ import Message from '../../src/utils/Message';
|
|||
import SearchField from '../../src/utils/SearchField';
|
||||
import { rangeOf } from '../../src/utils/utils';
|
||||
import { TagsList } from '../../src/tags/reducers/tagsList';
|
||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
|
||||
describe('<TagsList />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
@ -18,11 +19,12 @@ describe('<TagsList />', () => {
|
|||
wrapper = shallow(
|
||||
<TagsListComp
|
||||
{...Mock.all<TagsListProps>()}
|
||||
{...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
|
||||
forceListTags={identity}
|
||||
filterTags={filterTags}
|
||||
tagsList={Mock.of<TagsList>(tagsList)}
|
||||
/>,
|
||||
);
|
||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import ShortUrlVisitsHeader from '../../src/visits/ShortUrlVisitsHeader';
|
|||
import { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers/shortUrlVisits';
|
||||
import { ShortUrlDetail } from '../../src/visits/reducers/shortUrlDetail';
|
||||
import VisitsStats from '../../src/visits/VisitsStats';
|
||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
|
||||
describe('<ShortUrlVisits />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
@ -25,6 +26,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
wrapper = shallow(
|
||||
<ShortUrlVisits
|
||||
{...Mock.all<ShortUrlVisitsProps>()}
|
||||
{...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getShortUrlDetail={identity}
|
||||
getShortUrlVisits={getShortUrlVisitsMock}
|
||||
match={match}
|
||||
|
@ -34,7 +36,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
shortUrlDetail={Mock.all<ShortUrlDetail>()}
|
||||
cancelGetShortUrlVisits={() => {}}
|
||||
/>,
|
||||
);
|
||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
||||
});
|
||||
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -8,6 +8,7 @@ import TagVisitsHeader from '../../src/visits/TagVisitsHeader';
|
|||
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
||||
import { TagVisits as TagVisitsStats } from '../../src/visits/reducers/tagVisits';
|
||||
import VisitsStats from '../../src/visits/VisitsStats';
|
||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
|
||||
describe('<TagVisits />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
@ -25,13 +26,14 @@ describe('<TagVisits />', () => {
|
|||
wrapper = shallow(
|
||||
<TagVisits
|
||||
{...Mock.all<TagVisitsProps>()}
|
||||
{...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getTagVisits={getTagVisitsMock}
|
||||
match={match}
|
||||
history={history}
|
||||
tagVisits={Mock.of<TagVisitsStats>({ loading: true, visits: [] })}
|
||||
cancelGetTagVisits={() => {}}
|
||||
/>,
|
||||
);
|
||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
||||
});
|
||||
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 }] },
|
||||
));
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue