Merge pull request #447 from acelaya-forks/feature/visits-filter-reducer

Feature/visits filter reducer
This commit is contained in:
Alejandro Celaya 2021-07-02 20:10:36 +02:00 committed by GitHub
commit b3e79f4219
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 80 additions and 63 deletions

View file

@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7. * [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
### Changed ### Changed
* [#442](https://github.com/shlinkio/shlink-web-client/pull/442) Visits filtering now goes through the corresponding reducer.
* [#337](https://github.com/shlinkio/shlink-web-client/pull/337) Replaced moment.js with date-fns. * [#337](https://github.com/shlinkio/shlink-web-client/pull/337) Replaced moment.js with date-fns.
* [#360](https://github.com/shlinkio/shlink-web-client/pull/360) Changed component used to generate a tags selector, switching from `react-tagsinput`, which is no longer maintained, to `react-tag-autocomplete`. * [#360](https://github.com/shlinkio/shlink-web-client/pull/360) Changed component used to generate a tags selector, switching from `react-tagsinput`, which is no longer maintained, to `react-tag-autocomplete`.

View file

@ -55,6 +55,7 @@ export interface ShlinkVisitsParams {
itemsPerPage?: number; itemsPerPage?: number;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
excludeBots?: boolean;
} }
export interface ShlinkShortUrlData extends ShortUrlMeta { export interface ShlinkShortUrlData extends ShortUrlMeta {

View file

@ -4,12 +4,13 @@ import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
import { OrphanVisitsHeader } from './OrphanVisitsHeader'; import { OrphanVisitsHeader } from './OrphanVisitsHeader';
import { NormalizedVisit, VisitsInfo } from './types'; import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
import { VisitsExporter } from './services/VisitsExporter'; import { VisitsExporter } from './services/VisitsExporter';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps { export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps {
getOrphanVisits: (params: ShlinkVisitsParams) => void; getOrphanVisits: (params?: ShlinkVisitsParams, orphanVisitsType?: OrphanVisitType) => void;
orphanVisits: VisitsInfo; orphanVisits: VisitsInfo;
cancelGetOrphanVisits: () => void; cancelGetOrphanVisits: () => void;
} }
@ -24,10 +25,11 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
selectedServer, selectedServer,
}: OrphanVisitsProps) => { }: OrphanVisitsProps) => {
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits); const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
const loadVisits = (params: VisitsParams) => getOrphanVisits(toApiParams(params), params.filter?.orphanVisitsType);
return ( return (
<VisitsStats <VisitsStats
getVisits={getOrphanVisits} getVisits={loadVisits}
cancelGetVisits={cancelGetOrphanVisits} cancelGetVisits={cancelGetOrphanVisits}
visitsInfo={orphanVisits} visitsInfo={orphanVisits}
baseUrl={url} baseUrl={url}

View file

@ -9,8 +9,9 @@ import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader'; import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
import VisitsStats from './VisitsStats'; import VisitsStats from './VisitsStats';
import { VisitsExporter } from './services/VisitsExporter'; import { VisitsExporter } from './services/VisitsExporter';
import { NormalizedVisit } from './types'; import { NormalizedVisit, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> { export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> {
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void; getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
@ -34,7 +35,7 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
}: ShortUrlVisitsProps) => { }: ShortUrlVisitsProps) => {
const { shortCode } = params; const { shortCode } = params;
const { domain } = parseQuery<{ domain?: string }>(search); const { domain } = parseQuery<{ domain?: string }>(search);
const loadVisits = (params: Partial<ShlinkVisitsParams>) => getShortUrlVisits(shortCode, { ...params, domain }); const loadVisits = (params: VisitsParams) => getShortUrlVisits(shortCode, { ...toApiParams(params), domain });
const exportCsv = (visits: NormalizedVisit[]) => exportVisits( const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`, `short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
visits, visits,

View file

@ -9,9 +9,10 @@ import VisitsStats from './VisitsStats';
import { VisitsExporter } from './services/VisitsExporter'; import { VisitsExporter } from './services/VisitsExporter';
import { NormalizedVisit } from './types'; import { NormalizedVisit } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> { export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> {
getTagVisits: (tag: string, query: any) => void; getTagVisits: (tag: string, query?: ShlinkVisitsParams) => void;
tagVisits: TagVisitsState; tagVisits: TagVisitsState;
cancelGetTagVisits: () => void; cancelGetTagVisits: () => void;
} }
@ -26,7 +27,7 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
selectedServer, selectedServer,
}: TagVisitsProps) => { }: TagVisitsProps) => {
const { tag } = params; const { tag } = params;
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params); const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, toApiParams(params));
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits); const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits);
return ( return (

View file

@ -9,8 +9,6 @@ import { Location } from 'history';
import classNames from 'classnames'; import classNames from 'classnames';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
import Message from '../utils/Message'; import Message from '../utils/Message';
import { formatIsoDate } from '../utils/helpers/date';
import { ShlinkVisitsParams } from '../api/types';
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types'; import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
import { Result } from '../utils/Result'; import { Result } from '../utils/Result';
import { ShlinkApiError } from '../api/ShlinkApiError'; import { ShlinkApiError } from '../api/ShlinkApiError';
@ -21,15 +19,15 @@ import SortableBarGraph from './helpers/SortableBarGraph';
import GraphCard from './helpers/GraphCard'; import GraphCard from './helpers/GraphCard';
import LineChartCard from './helpers/LineChartCard'; import LineChartCard from './helpers/LineChartCard';
import VisitsTable from './VisitsTable'; import VisitsTable from './VisitsTable';
import { NormalizedOrphanVisit, NormalizedVisit, VisitsInfo } from './types'; import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
import OpenMapModalBtn from './helpers/OpenMapModalBtn'; import OpenMapModalBtn from './helpers/OpenMapModalBtn';
import { processStatsFromVisits } from './services/VisitsParser'; import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
import { VisitsFilter, VisitsFilterDropdown } from './helpers/VisitsFilterDropdown'; import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown';
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers'; import { HighlightableProps, highlightedVisitsToStats } from './types/helpers';
import './VisitsStats.scss'; import './VisitsStats.scss';
export interface VisitsStatsProps { export interface VisitsStatsProps {
getVisits: (params: Partial<ShlinkVisitsParams>) => void; getVisits: (params: VisitsParams) => void;
visitsInfo: VisitsInfo; visitsInfo: VisitsInfo;
settings: Settings; settings: Settings;
selectedServer: SelectedServer; selectedServer: SelectedServer;
@ -95,7 +93,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`; return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`;
}; };
const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo; const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo;
const normalizedVisits = useMemo(() => normalizeAndFilterVisits(visits, visitsFilter), [ visits, visitsFilter ]); const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo( const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
() => processStatsFromVisits(normalizedVisits), () => processStatsFromVisits(normalizedVisits),
[ normalizedVisits ], [ normalizedVisits ],
@ -122,10 +120,8 @@ const VisitsStats: FC<VisitsStatsProps> = ({
useEffect(() => cancelGetVisits, []); useEffect(() => cancelGetVisits, []);
useEffect(() => { useEffect(() => {
const { startDate, endDate } = dateRange; getVisits({ dateRange, filter: visitsFilter });
}, [ dateRange, visitsFilter ]);
getVisits({ startDate: formatIsoDate(startDate) ?? undefined, endDate: formatIsoDate(endDate) ?? undefined });
}, [ dateRange ]);
const renderVisitsContent = () => { const renderVisitsContent = () => {
if (loadingLarge) { if (loadingLarge) {

View file

@ -1,13 +1,8 @@
import { DropdownItem, DropdownItemProps } from 'reactstrap'; // eslint-disable-line import/named import { DropdownItem, DropdownItemProps } from 'reactstrap'; // eslint-disable-line import/named
import { OrphanVisitType } from '../types'; import { OrphanVisitType, VisitsFilter } from '../types';
import { DropdownBtn } from '../../utils/DropdownBtn'; import { DropdownBtn } from '../../utils/DropdownBtn';
import { hasValue } from '../../utils/utils'; import { hasValue } from '../../utils/utils';
export interface VisitsFilter {
orphanVisitsType?: OrphanVisitType | undefined;
excludeBots?: boolean;
}
interface VisitsFilterDropdownProps { interface VisitsFilterDropdownProps {
onChange: (filters: VisitsFilter) => void; onChange: (filters: VisitsFilter) => void;
selected?: VisitsFilter; selected?: VisitsFilter;
@ -26,7 +21,7 @@ export const VisitsFilterDropdown = (
const { orphanVisitsType, excludeBots = false } = selected; const { orphanVisitsType, excludeBots = false } = selected;
const propsForOrphanVisitsTypeItem = (type: OrphanVisitType): DropdownItemProps => ({ const propsForOrphanVisitsTypeItem = (type: OrphanVisitType): DropdownItemProps => ({
active: orphanVisitsType === type, active: orphanVisitsType === type,
onClick: () => onChange({ ...selected, orphanVisitsType: type }), onClick: () => onChange({ ...selected, orphanVisitsType: type === selected?.orphanVisitsType ? undefined : type }),
}); });
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots }); const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });

View file

@ -1,8 +1,17 @@
import { Action, Dispatch } from 'redux'; import { Action, Dispatch } from 'redux';
import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAction } from '../types'; import {
OrphanVisit,
OrphanVisitType,
Visit,
VisitsInfo,
VisitsLoadFailedAction,
VisitsLoadProgressChangedAction,
} from '../types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { GetState } from '../../container/types'; import { GetState } from '../../container/types';
import { ShlinkVisitsParams } from '../../api/types';
import { isOrphanVisit } from '../types/helpers';
import { getVisitsWithLoader } from './common'; import { getVisitsWithLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
@ -48,12 +57,20 @@ export default buildReducer<VisitsInfo, OrphanVisitsCombinedAction>({
}, },
}, initialState); }, initialState);
export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (query = {}) => async ( const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) =>
dispatch: Dispatch, !orphanVisitsType || orphanVisitsType === visit.type;
getState: GetState,
) => { export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
query: ShlinkVisitsParams = {},
orphanVisitsType?: OrphanVisitType,
) => async (dispatch: Dispatch, getState: GetState) => {
const { getOrphanVisits } = buildShlinkApiClient(getState); const { getOrphanVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage }); const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage })
.then((result) => {
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
return { ...result, data: visits };
});
const shouldCancel = () => getState().orphanVisits.cancelLoad; const shouldCancel = () => getState().orphanVisits.cancelLoad;
const actionMap = { const actionMap = {
start: GET_ORPHAN_VISITS_START, start: GET_ORPHAN_VISITS_START,

View file

@ -5,7 +5,7 @@ import { ShortUrlIdentifier } from '../../short-urls/data';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { GetState } from '../../container/types'; import { GetState } from '../../container/types';
import { OptionalString } from '../../utils/utils'; import { ShlinkVisitsParams } from '../../api/types';
import { getVisitsWithLoader } from './common'; import { getVisitsWithLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
@ -64,7 +64,7 @@ export default buildReducer<ShortUrlVisits, ShortUrlVisitsCombinedAction>({
export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
shortCode: string, shortCode: string,
query: { domain?: OptionalString } = {}, query: ShlinkVisitsParams = {},
) => async (dispatch: Dispatch, getState: GetState) => { ) => async (dispatch: Dispatch, getState: GetState) => {
const { getShortUrlVisits } = buildShlinkApiClient(getState); const { getShortUrlVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits( const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits(

View file

@ -3,6 +3,7 @@ import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAct
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { GetState } from '../../container/types'; import { GetState } from '../../container/types';
import { ShlinkVisitsParams } from '../../api/types';
import { getVisitsWithLoader } from './common'; import { getVisitsWithLoader } from './common';
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
@ -56,10 +57,10 @@ export default buildReducer<TagVisits, TagsVisitsCombinedAction>({
}, },
}, initialState); }, initialState);
export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: string, query = {}) => async ( export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
dispatch: Dispatch, tag: string,
getState: GetState, query: ShlinkVisitsParams = {},
) => { ) => async (dispatch: Dispatch, getState: GetState) => {
const { getTagVisits } = buildShlinkApiClient(getState); const { getTagVisits } = buildShlinkApiClient(getState);
const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits( const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits(
tag, tag,

View file

@ -1,8 +1,7 @@
import { countBy, filter, groupBy, pipe, prop } from 'ramda'; import { countBy, groupBy, pipe, prop } from 'ramda';
import { normalizeVisits } from '../services/VisitsParser'; import { formatIsoDate } from '../../utils/helpers/date';
import { VisitsFilter } from '../helpers/VisitsFilterDropdown'; import { ShlinkVisitsParams } from '../../api/types';
import { hasValue } from '../../utils/utils'; import { CreateVisit, NormalizedOrphanVisit, NormalizedVisit, OrphanVisit, Stats, Visit, VisitsParams } from './index';
import { Visit, OrphanVisit, CreateVisit, NormalizedVisit, NormalizedOrphanVisit, Stats } from './index';
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl'); export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl');
@ -29,19 +28,10 @@ export const highlightedVisitsToStats = <T extends NormalizedVisit>(
property: HighlightableProps<T>, property: HighlightableProps<T>,
): Stats => countBy(prop(property) as any, highlightedVisits); ): Stats => countBy(prop(property) as any, highlightedVisits);
export const normalizeAndFilterVisits = (visits: Visit[], filters: VisitsFilter) => pipe( export const toApiParams = ({ page, itemsPerPage, filter, dateRange }: VisitsParams): ShlinkVisitsParams => {
normalizeVisits, const startDate = (dateRange?.startDate && formatIsoDate(dateRange?.startDate)) ?? undefined;
filter((normalizedVisit: NormalizedVisit) => { const endDate = (dateRange?.endDate && formatIsoDate(dateRange?.endDate)) ?? undefined;
if (!hasValue(filters)) { const excludeBots = filter?.excludeBots || undefined; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
return true;
}
const { orphanVisitsType, excludeBots } = filters; return { page, itemsPerPage, startDate, endDate, excludeBots };
};
if (orphanVisitsType && orphanVisitsType !== (normalizedVisit as NormalizedOrphanVisit).type) {
return false;
}
return !(excludeBots && normalizedVisit.potentialBot);
}),
)(visits);

View file

@ -1,6 +1,7 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { ShortUrl } from '../../short-urls/data'; import { ShortUrl } from '../../short-urls/data';
import { ProblemDetailsError } from '../../api/types'; import { ProblemDetailsError } from '../../api/types';
import { DateRange } from '../../utils/dates/types';
export interface VisitsInfo { export interface VisitsInfo {
visits: Visit[]; visits: Visit[];
@ -94,3 +95,15 @@ export interface VisitsStats {
citiesForMap: Record<string, CityStats>; citiesForMap: Record<string, CityStats>;
visitedUrls: Stats; visitedUrls: Stats;
} }
export interface VisitsFilter {
orphanVisitsType?: OrphanVisitType | undefined;
excludeBots?: boolean;
}
export interface VisitsParams {
page?: number;
itemsPerPage?: number;
dateRange?: DateRange;
filter?: VisitsFilter;
}

View file

@ -37,7 +37,6 @@ describe('<OrphanVisits />', () => {
expect(stats).toHaveLength(1); expect(stats).toHaveLength(1);
expect(header).toHaveLength(1); expect(header).toHaveLength(1);
expect(stats.prop('getVisits')).toEqual(getOrphanVisits);
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits); expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits);
expect(stats.prop('visitsInfo')).toEqual(orphanVisits); expect(stats.prop('visitsInfo')).toEqual(orphanVisits);
expect(stats.prop('baseUrl')).toEqual('the_base_url'); expect(stats.prop('baseUrl')).toEqual('the_base_url');

View file

@ -1,7 +1,7 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import { OrphanVisitType } from '../../../src/visits/types'; import { OrphanVisitType, VisitsFilter } from '../../../src/visits/types';
import { VisitsFilter, VisitsFilterDropdown } from '../../../src/visits/helpers/VisitsFilterDropdown'; import { VisitsFilterDropdown } from '../../../src/visits/helpers/VisitsFilterDropdown';
describe('<VisitsFilterDropdown />', () => { describe('<VisitsFilterDropdown />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;

View file

@ -110,9 +110,9 @@ describe('orphanVisitsReducer', () => {
[ undefined ], [ undefined ],
[{}], [{}],
])('dispatches start and success when promise is resolved', async (query) => { ])('dispatches start and success when promise is resolved', async (query) => {
const visits = visitsMocks; const visits = visitsMocks.map((visit) => ({ ...visit, visitedUrl: '' }));
const ShlinkApiClient = buildApiClientMock(Promise.resolve({ const ShlinkApiClient = buildApiClientMock(Promise.resolve({
data: visitsMocks, data: visits,
pagination: { pagination: {
currentPage: 1, currentPage: 1,
pagesCount: 1, pagesCount: 1,