mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Implemented short URLs exporting
This commit is contained in:
parent
e632c5b04f
commit
92ddcad753
23 changed files with 168 additions and 81 deletions
2
shlink-web-client.d.ts
vendored
2
shlink-web-client.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
33
src/common/services/ReportExporter.ts
Normal file
33
src/common/services/ReportExporter.ts
Normal 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);
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -65,6 +65,7 @@ const ShortUrlsList = (
|
|||
<>
|
||||
<ShortUrlsFilteringBar
|
||||
selectedServer={selectedServer}
|
||||
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
|
||||
order={actualOrderBy}
|
||||
handleOrderBy={handleOrderBy}
|
||||
className="mb-3"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
66
src/short-urls/helpers/ExportShortUrlsBtn.tsx
Normal file
66
src/short-urls/helpers/ExportShortUrlsBtn.tsx
Normal 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} />;
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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', () => {
|
|
@ -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([
|
||||
|
|
|
@ -33,6 +33,7 @@ describe('<ShortUrlsList />', () => {
|
|||
tags: [ 'test tag' ],
|
||||
}),
|
||||
],
|
||||
pagination: {},
|
||||
},
|
||||
});
|
||||
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, ShortUrlsFilteringBar);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue