Implemented short URLs exporting

This commit is contained in:
Alejandro Celaya 2022-03-13 18:56:42 +01:00
parent e632c5b04f
commit 92ddcad753
23 changed files with 168 additions and 81 deletions

View file

@ -10,7 +10,7 @@ declare module 'event-source-polyfill' {
declare module 'csvjson' {
export declare class CsvJson {
public toObject<T>(content: string): T[];
public toCSV<T>(data: T[], options: { headers: 'full' | 'none' | 'relative' | 'key' }): string;
public toCSV<T>(data: T[], options: { headers: 'full' | 'none' | 'relative' | 'key'; wrap?: true }): string;
}
}

View file

@ -0,0 +1,33 @@
import { CsvJson } from 'csvjson';
import { NormalizedVisit } from '../../visits/types';
import { ExportableShortUrl } from '../../short-urls/data';
import { saveCsv } from '../../utils/helpers/files';
export class ReportExporter {
public constructor(
private readonly window: Window,
private readonly csvjson: CsvJson,
) {}
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
if (!visits.length) {
return;
}
this.exportCsv(filename, visits);
};
public readonly exportShortUrls = (shortUrls: ExportableShortUrl[]) => {
if (!shortUrls.length) {
return;
}
this.exportCsv('short_urls.csv', shortUrls);
};
private readonly exportCsv = (filename: string, rows: object[]) => {
const csv = this.csvjson.toCSV(rows, { headers: 'key', wrap: true });
saveCsv(this.window, csv, filename);
};
}

View file

@ -11,6 +11,7 @@ import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
import { ImageDownloader } from './ImageDownloader';
import { ReportExporter } from './ReportExporter';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services
@ -19,6 +20,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.constant('axios', axios);
bottle.service('ImageDownloader', ImageDownloader, 'axios', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'csvjson');
// Components
bottle.serviceFactory('ScrollToTop', ScrollToTop);

View file

@ -14,11 +14,11 @@ import { DateRange } from '../utils/dates/types';
import { supportsAllTagsFiltering } from '../utils/helpers/features';
import { SelectedServer } from '../servers/data';
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
import { ExportBtn } from '../utils/ExportBtn';
import { OrderDir } from '../utils/helpers/ordering';
import { OrderingDropdown } from '../utils/OrderingDropdown';
import { useShortUrlsQuery } from './helpers/hooks';
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
import './ShortUrlsFilteringBar.scss';
export interface ShortUrlsFilteringProps {
@ -26,13 +26,15 @@ export interface ShortUrlsFilteringProps {
order: ShortUrlsOrder;
handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void;
className?: string;
shortUrlsAmount?: number;
}
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator): FC<ShortUrlsFilteringProps> => (
{ selectedServer, className, order, handleOrderBy },
) => {
const ShortUrlsFilteringBar = (
colorGenerator: ColorGenerator,
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery();
const setDates = pipe(
({ startDate, endDate }: DateRange) => ({
@ -61,7 +63,7 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator): FC<ShortUrlsFilt
<Row className="flex-column-reverse flex-lg-row">
<div className="col-lg-4 col-xl-6 mt-3">
<ExportBtn className="btn-md-block" amount={4} onClick={() => {}} />
<ExportShortUrlsBtn amount={shortUrlsAmount} />
</div>
<div className="col-12 d-block d-lg-none mt-3">
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={order} onChange={handleOrderBy} />

View file

@ -65,6 +65,7 @@ const ShortUrlsList = (
<>
<ShortUrlsFilteringBar
selectedServer={selectedServer}
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
order={actualOrderBy}
handleOrderBy={handleOrderBy}
className="mb-3"

View file

@ -63,3 +63,12 @@ export const SHORT_URLS_ORDERABLE_FIELDS = {
export type ShortUrlsOrderableFields = keyof typeof SHORT_URLS_ORDERABLE_FIELDS;
export type ShortUrlsOrder = Order<ShortUrlsOrderableFields>;
export interface ExportableShortUrl {
createdAt: string;
title: string;
shortUrl: string;
longUrl: string;
tags: string;
visits: number;
}

View file

@ -0,0 +1,66 @@
import { FC } from 'react';
import { ExportBtn } from '../../utils/ExportBtn';
import { useToggle } from '../../utils/helpers/hooks';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { isServerWithId, SelectedServer } from '../../servers/data';
import { ShortUrl } from '../data';
import { ReportExporter } from '../../common/services/ReportExporter';
import { useShortUrlsQuery } from './hooks';
export interface ExportShortUrlsBtnProps {
amount?: number;
}
interface ExportShortUrlsBtnConnectProps extends ExportShortUrlsBtnProps {
selectedServer: SelectedServer;
}
const itemsPerPage = 10;
export const ExportShortUrlsBtn = (
buildShlinkApiClient: ShlinkApiClientBuilder,
{ exportShortUrls }: ReportExporter,
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
const [ loading,, startLoading, stopLoading ] = useToggle();
const exportAllUrls = () => {
if (!isServerWithId(selectedServer)) {
return;
}
const totalPages = amount / itemsPerPage;
const { listShortUrls } = buildShlinkApiClient(selectedServer);
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
const { data } = await listShortUrls(
{ page: `${page}`, tags, searchTerm: search, startDate, endDate, orderBy, tagsMode, itemsPerPage },
);
if (page >= totalPages) {
return data;
}
// TODO Support paralelization
return data.concat(await loadAllUrls(page + 1));
};
startLoading();
loadAllUrls()
.then((shortUrls) => {
exportShortUrls(shortUrls.map((shortUrl) => ({
createdAt: shortUrl.dateCreated,
shortUrl: shortUrl.shortUrl,
longUrl: shortUrl.longUrl,
title: shortUrl.title ?? '',
tags: shortUrl.tags.join(','),
visits: shortUrl.visitsCount,
})));
stopLoading();
})
.catch((e) => {
// TODO Handle error properly
console.error('An error occurred while exporting short URLs', e); // eslint-disable-line no-console
});
};
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
};

View file

@ -16,6 +16,7 @@ import QrCodeModal from '../helpers/QrCodeModal';
import { ShortUrlForm } from '../ShortUrlForm';
import { EditShortUrl } from '../EditShortUrl';
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
import { ExportShortUrlsBtn } from '../helpers/ExportShortUrlsBtn';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
@ -49,7 +50,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader', 'ForServerVersion');
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator', 'ExportShortUrlsBtn');
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
bottle.decorator('ExportShortUrlsBtn', connect([ 'selectedServer' ]));
// Actions
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');

View file

@ -1,17 +1,16 @@
import { FC } from 'react';
import { Button } from 'reactstrap';
import { Button, ButtonProps } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFileDownload } from '@fortawesome/free-solid-svg-icons';
import { prettify } from './helpers/numbers';
interface ExportBtnProps {
onClick: () => void;
interface ExportBtnProps extends Omit<ButtonProps, 'outline' | 'color' | 'disabled'> {
amount?: number;
className?: string;
loading?: boolean;
}
export const ExportBtn: FC<ExportBtnProps> = ({ onClick, className, amount = 0 }) => (
<Button outline color="primary" className={className} onClick={onClick}>
<FontAwesomeIcon icon={faFileDownload} /> Export ({prettify(amount)})
export const ExportBtn: FC<ExportBtnProps> = ({ amount = 0, loading = false, ...rest }) => (
<Button {...rest} outline color="primary" disabled={loading}>
<FontAwesomeIcon icon={faFileDownload} /> {loading ? 'Exporting...' : <>Export ({prettify(amount)})</>}
</Button>
);

View file

@ -2,9 +2,9 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import VisitsStats from './VisitsStats';
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
import { VisitsExporter } from './services/VisitsExporter';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
import { NonOrphanVisitsHeader } from './NonOrphanVisitsHeader';
@ -15,7 +15,7 @@ export interface NonOrphanVisitsProps extends CommonVisitsProps {
cancelGetNonOrphanVisits: () => void;
}
export const NonOrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
getNonOrphanVisits,
nonOrphanVisits,
cancelGetNonOrphanVisits,

View file

@ -2,10 +2,10 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import VisitsStats from './VisitsStats';
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
import { VisitsExporter } from './services/VisitsExporter';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
@ -19,7 +19,7 @@ export interface OrphanVisitsProps extends CommonVisitsProps {
cancelGetOrphanVisits: () => void;
}
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
getOrphanVisits,
orphanVisits,
cancelGetOrphanVisits,

View file

@ -6,10 +6,10 @@ import { parseQuery } from '../utils/helpers/query';
import { Topics } from '../mercure/helpers/Topics';
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
import VisitsStats from './VisitsStats';
import { VisitsExporter } from './services/VisitsExporter';
import { NormalizedVisit, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
@ -22,7 +22,7 @@ export interface ShortUrlVisitsProps extends CommonVisitsProps {
cancelGetShortUrlVisits: () => void;
}
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
shortUrlVisits,
shortUrlDetail,
getShortUrlVisits,

View file

@ -4,10 +4,10 @@ import ColorGenerator from '../utils/services/ColorGenerator';
import { ShlinkVisitsParams } from '../api/types';
import { Topics } from '../mercure/helpers/Topics';
import { useGoBack } from '../utils/helpers/hooks';
import { ReportExporter } from '../common/services/ReportExporter';
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
import TagVisitsHeader from './TagVisitsHeader';
import VisitsStats from './VisitsStats';
import { VisitsExporter } from './services/VisitsExporter';
import { NormalizedVisit } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers';
@ -18,7 +18,7 @@ export interface TagVisitsProps extends CommonVisitsProps {
cancelGetTagVisits: () => void;
}
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({
getTagVisits,
tagVisits,
cancelGetTagVisits,

View file

@ -1,20 +0,0 @@
import { CsvJson } from 'csvjson';
import { NormalizedVisit } from '../types';
import { saveCsv } from '../../utils/helpers/files';
export class VisitsExporter {
public constructor(
private readonly window: Window,
private readonly csvjson: CsvJson,
) {}
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
if (!visits.length) {
return;
}
const csv = this.csvjson.toCSV(visits, { headers: 'key' });
saveCsv(this.window, csv, filename);
};
}

View file

@ -12,31 +12,30 @@ import { cancelGetNonOrphanVisits, getNonOrphanVisits } from '../reducers/nonOrp
import { ConnectDecorator } from '../../container/types';
import { loadVisitsOverview } from '../reducers/visitsOverview';
import * as visitsParser from './VisitsParser';
import { VisitsExporter } from './VisitsExporter';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory('MapModal', () => MapModal);
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsExporter');
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'ReportExporter');
bottle.decorator('ShortUrlVisits', connect(
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer' ],
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
));
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'VisitsExporter');
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'ReportExporter');
bottle.decorator('TagVisits', connect(
[ 'tagVisits', 'mercureInfo', 'settings', 'selectedServer' ],
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
));
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'VisitsExporter');
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
bottle.decorator('OrphanVisits', connect(
[ 'orphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
));
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'VisitsExporter');
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'ReportExporter');
bottle.decorator('NonOrphanVisits', connect(
[ 'nonOrphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
[ 'getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
@ -44,7 +43,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Services
bottle.serviceFactory('VisitsParser', () => visitsParser);
bottle.service('VisitsExporter', VisitsExporter, 'window', 'csvjson');
// Actions
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'buildShlinkApiClient');

View file

@ -1,20 +1,20 @@
import { Mock } from 'ts-mockery';
import { CsvJson } from 'csvjson';
import { VisitsExporter } from '../../../src/visits/services/VisitsExporter';
import { ReportExporter } from '../../../src/common/services/ReportExporter';
import { NormalizedVisit } from '../../../src/visits/types';
import { windowMock } from '../../mocks/WindowMock';
describe('VisitsExporter', () => {
describe('ReportExporter', () => {
const toCSV = jest.fn();
const csvToJsonMock = Mock.of<CsvJson>({ toCSV });
let exporter: VisitsExporter;
let exporter: ReportExporter;
beforeEach(jest.clearAllMocks);
beforeEach(() => {
(global as any).Blob = class Blob {}; // eslint-disable-line @typescript-eslint/no-extraneous-class
(global as any).URL = { createObjectURL: () => '' };
exporter = new VisitsExporter(windowMock, csvToJsonMock);
exporter = new ReportExporter(windowMock, csvToJsonMock);
});
describe('exportVisits', () => {
@ -35,7 +35,7 @@ describe('VisitsExporter', () => {
exporter.exportVisits('my_visits.csv', visits);
expect(toCSV).toHaveBeenCalledWith(visits, { headers: 'key' });
expect(toCSV).toHaveBeenCalledWith(visits, { headers: 'key', wrap: true });
});
it('skips execution when list of visits is empty', () => {

View file

@ -20,7 +20,8 @@ jest.mock('react-router-dom', () => ({
describe('<ShortUrlsFilteringBar />', () => {
let wrapper: ShallowWrapper;
const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>());
const ExportShortUrlsBtn = () => null;
const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>(), ExportShortUrlsBtn);
const navigate = jest.fn();
const handleOrderBy = jest.fn();
const now = new Date();
@ -48,6 +49,7 @@ describe('<ShortUrlsFilteringBar />', () => {
expect(wrapper.find(SearchField)).toHaveLength(1);
expect(wrapper.find(DateRangeSelector)).toHaveLength(1);
expect(wrapper.find(OrderingDropdown)).toHaveLength(1);
expect(wrapper.find(ExportShortUrlsBtn)).toHaveLength(1);
});
it.each([

View file

@ -33,6 +33,7 @@ describe('<ShortUrlsList />', () => {
tags: [ 'test tag' ],
}),
],
pagination: {},
},
});
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, ShortUrlsFilteringBar);

View file

@ -5,9 +5,8 @@ import { ExportBtn } from '../../src/utils/ExportBtn';
describe('<ExportBtn />', () => {
let wrapper: ShallowWrapper;
const onClick = jest.fn();
const createWrapper = (className?: string, amount?: number) => {
wrapper = shallow(<ExportBtn className={className} amount={amount} onClick={onClick} />);
const createWrapper = (amount?: number, loading = false) => {
wrapper = shallow(<ExportBtn amount={amount} loading={loading} />);
return wrapper;
};
@ -16,16 +15,15 @@ describe('<ExportBtn />', () => {
afterEach(() => wrapper?.unmount());
it.each([
[ undefined ],
[ 'foo' ],
[ 'bar' ],
])('renders a button', (className) => {
const wrapper = createWrapper(className);
[ true, 'Exporting...' ],
[ false, 'Export (' ],
])('renders a button', (loading, text) => {
const wrapper = createWrapper(undefined, loading);
expect(wrapper.prop('outline')).toEqual(true);
expect(wrapper.prop('color')).toEqual('primary');
expect(wrapper.prop('onClick')).toEqual(onClick);
expect(wrapper.prop('className')).toEqual(className);
expect(wrapper.prop('disabled')).toEqual(loading);
expect(wrapper.html()).toContain(text);
});
it.each([
@ -34,7 +32,7 @@ describe('<ExportBtn />', () => {
[ 10_000, '10,000' ],
[ 10_000_000, '10,000,000' ],
])('renders expected amount', (amount, expectedRenderedAmount) => {
const wrapper = createWrapper(undefined, amount);
const wrapper = createWrapper(amount);
expect(wrapper.html()).toContain(`Export (${expectedRenderedAmount})`);
});
@ -46,12 +44,4 @@ describe('<ExportBtn />', () => {
expect(icon).toHaveLength(1);
expect(icon.prop('icon')).toEqual(faFileDownload);
});
it('invokes callback onClick', () => {
const wrapper = createWrapper();
expect(onClick).not.toHaveBeenCalled();
wrapper.simulate('click');
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -6,7 +6,7 @@ import { VisitsInfo } from '../../src/visits/types';
import VisitsStats from '../../src/visits/VisitsStats';
import { NonOrphanVisitsHeader } from '../../src/visits/NonOrphanVisitsHeader';
import { Settings } from '../../src/settings/reducers/settings';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
import { ReportExporter } from '../../src/common/services/ReportExporter';
import { SelectedServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
@ -20,7 +20,7 @@ describe('<NonOrphanVisits />', () => {
const getNonOrphanVisits = jest.fn();
const cancelGetNonOrphanVisits = jest.fn();
const nonOrphanVisits = Mock.all<VisitsInfo>();
const NonOrphanVisits = createNonOrphanVisits(Mock.all<VisitsExporter>());
const NonOrphanVisits = createNonOrphanVisits(Mock.all<ReportExporter>());
const wrapper = shallow(
<NonOrphanVisits

View file

@ -6,7 +6,7 @@ import { VisitsInfo } from '../../src/visits/types';
import VisitsStats from '../../src/visits/VisitsStats';
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
import { Settings } from '../../src/settings/reducers/settings';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
import { ReportExporter } from '../../src/common/services/ReportExporter';
import { SelectedServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
@ -20,7 +20,7 @@ describe('<OrphanVisits />', () => {
const getOrphanVisits = jest.fn();
const cancelGetOrphanVisits = jest.fn();
const orphanVisits = Mock.all<VisitsInfo>();
const OrphanVisits = createOrphanVisits(Mock.all<VisitsExporter>());
const OrphanVisits = createOrphanVisits(Mock.all<ReportExporter>());
const wrapper = shallow(
<OrphanVisits

View file

@ -7,7 +7,7 @@ import { ShortUrlVisits as ShortUrlVisitsState } from '../../src/visits/reducers
import { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail';
import VisitsStats from '../../src/visits/VisitsStats';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
import { ReportExporter } from '../../src/common/services/ReportExporter';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@ -19,7 +19,7 @@ jest.mock('react-router-dom', () => ({
describe('<ShortUrlVisits />', () => {
let wrapper: ShallowWrapper;
const getShortUrlVisitsMock = jest.fn();
const ShortUrlVisits = createShortUrlVisits(Mock.all<VisitsExporter>());
const ShortUrlVisits = createShortUrlVisits(Mock.all<ReportExporter>());
beforeEach(() => {
wrapper = shallow(

View file

@ -6,7 +6,7 @@ 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';
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
import { ReportExporter } from '../../src/common/services/ReportExporter';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@ -20,7 +20,7 @@ describe('<TagVisits />', () => {
const getTagVisitsMock = jest.fn();
beforeEach(() => {
const TagVisits = createTagVisits(Mock.all<ColorGenerator>(), Mock.all<VisitsExporter>());
const TagVisits = createTagVisits(Mock.all<ColorGenerator>(), Mock.all<ReportExporter>());
wrapper = shallow(
<TagVisits