mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Created button to use when anything needs to be exported
This commit is contained in:
parent
187e26810d
commit
7fd360495b
7 changed files with 57 additions and 35 deletions
|
@ -5,12 +5,12 @@ import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||||
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
|
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
|
||||||
|
|
||||||
interface ShortUrlsListProps {
|
interface ShortUrlsListSettingsProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
setShortUrlsListSettings: (settings: ShortUrlsSettings) => void;
|
setShortUrlsListSettings: (settings: ShortUrlsSettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShortUrlsListSettings: FC<ShortUrlsListProps> = (
|
export const ShortUrlsListSettings: FC<ShortUrlsListSettingsProps> = (
|
||||||
{ settings: { shortUrlsList }, setShortUrlsListSettings },
|
{ settings: { shortUrlsList }, setShortUrlsListSettings },
|
||||||
) => (
|
) => (
|
||||||
<SimpleCard title="Short URLs list" className="h-100">
|
<SimpleCard title="Short URLs list" className="h-100">
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import { FC } from 'react';
|
||||||
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
|
import { Row } from 'reactstrap';
|
||||||
|
import classNames from 'classnames';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import Tag from '../tags/helpers/Tag';
|
import Tag from '../tags/helpers/Tag';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
|
@ -11,16 +14,20 @@ import { DateRange } from '../utils/dates/types';
|
||||||
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
|
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
|
||||||
|
import { ExportBtn } from '../utils/ExportBtn';
|
||||||
import { useShortUrlsQuery } from './helpers/hooks';
|
import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import './ShortUrlsFilteringBar.scss';
|
import './ShortUrlsFilteringBar.scss';
|
||||||
|
|
||||||
interface ShortUrlsFilteringProps {
|
export interface ShortUrlsFilteringProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
||||||
|
|
||||||
const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ({ selectedServer }: ShortUrlsFilteringProps) => {
|
const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator): FC<ShortUrlsFilteringProps> => (
|
||||||
|
{ selectedServer, className },
|
||||||
|
) => {
|
||||||
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery();
|
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery();
|
||||||
const selectedTags = tags?.split(',') ?? [];
|
const selectedTags = tags?.split(',') ?? [];
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
|
@ -46,23 +53,24 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ({ selectedSer
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="short-urls-filtering-bar-container">
|
<div className={classNames('short-urls-filtering-bar-container', className)}>
|
||||||
<SearchField initialValue={search} onChange={setSearch} />
|
<SearchField initialValue={search} onChange={setSearch} />
|
||||||
|
|
||||||
<div className="mt-3">
|
<Row>
|
||||||
<div className="row">
|
<div className="col-lg-4 col-xl-6 mt-3">
|
||||||
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
|
<ExportBtn className="btn-md-block" amount={4} onClick={() => {}} />
|
||||||
<DateRangeSelector
|
|
||||||
defaultText="All short URLs"
|
|
||||||
initialDateRange={{
|
|
||||||
startDate: dateOrNull(startDate),
|
|
||||||
endDate: dateOrNull(endDate),
|
|
||||||
}}
|
|
||||||
onDatesChange={setDates}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="col-lg-8 col-xl-6 mt-3">
|
||||||
|
<DateRangeSelector
|
||||||
|
defaultText="All short URLs"
|
||||||
|
initialDateRange={{
|
||||||
|
startDate: dateOrNull(startDate),
|
||||||
|
endDate: dateOrNull(endDate),
|
||||||
|
}}
|
||||||
|
onDatesChange={setDates}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{selectedTags.length > 0 && (
|
{selectedTags.length > 0 && (
|
||||||
<h4 className="mt-3">
|
<h4 className="mt-3">
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { ShortUrlsTableProps } from './ShortUrlsTable';
|
||||||
import Paginator from './Paginator';
|
import Paginator from './Paginator';
|
||||||
import { useShortUrlsQuery } from './helpers/hooks';
|
import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import { ShortUrlsOrderableFields, SHORT_URLS_ORDERABLE_FIELDS } from './data';
|
import { ShortUrlsOrderableFields, SHORT_URLS_ORDERABLE_FIELDS } from './data';
|
||||||
|
import { ShortUrlsFilteringProps } from './ShortUrlsFilteringBar';
|
||||||
|
|
||||||
interface ShortUrlsListProps {
|
interface ShortUrlsListProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
@ -23,12 +24,10 @@ interface ShortUrlsListProps {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteringBar: FC) => boundToMercureHub(({
|
const ShortUrlsList = (
|
||||||
listShortUrls,
|
ShortUrlsTable: FC<ShortUrlsTableProps>,
|
||||||
shortUrlsList,
|
ShortUrlsFilteringBar: FC<ShortUrlsFilteringProps>,
|
||||||
selectedServer,
|
) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => {
|
||||||
settings,
|
|
||||||
}: ShortUrlsListProps) => {
|
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
const { page } = useParams();
|
const { page } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -66,7 +65,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteri
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3"><ShortUrlsFilteringBar /></div>
|
<ShortUrlsFilteringBar selectedServer={selectedServer} className="mb-3" />
|
||||||
<div className="d-block d-lg-none mb-3">
|
<div className="d-block d-lg-none mb-3">
|
||||||
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={actualOrderBy} onChange={handleOrderBy} />
|
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={actualOrderBy} onChange={handleOrderBy} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,7 +50,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
|
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
|
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
|
||||||
bottle.decorator('ShortUrlsFilteringBar', connect([ 'selectedServer' ]));
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
||||||
|
|
17
src/utils/ExportBtn.tsx
Normal file
17
src/utils/ExportBtn.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Button } 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;
|
||||||
|
amount?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExportBtn: FC<ExportBtnProps> = ({ onClick, className, amount = 0 }) => (
|
||||||
|
<Button outline color="primary" className={className} onClick={onClick}>
|
||||||
|
<FontAwesomeIcon icon={faFileDownload} /> Export ({prettify(amount)})
|
||||||
|
</Button>
|
||||||
|
);
|
|
@ -2,7 +2,7 @@ import { isEmpty, propEq, values } from 'ramda';
|
||||||
import { useState, useEffect, useMemo, FC, useRef } from 'react';
|
import { useState, useEffect, useMemo, FC, useRef } from 'react';
|
||||||
import { Button, Progress, Row } from 'reactstrap';
|
import { Button, Progress, Row } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie, faFileDownload } from '@fortawesome/free-solid-svg-icons';
|
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||||
import { Route, Routes, Navigate } from 'react-router-dom';
|
import { Route, Routes, Navigate } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -16,6 +16,7 @@ import { SelectedServer } from '../servers/data';
|
||||||
import { supportsBotVisits } from '../utils/helpers/features';
|
import { supportsBotVisits } from '../utils/helpers/features';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { NavPillItem, NavPills } from '../utils/NavPills';
|
import { NavPillItem, NavPills } from '../utils/NavPills';
|
||||||
|
import { ExportBtn } from '../utils/ExportBtn';
|
||||||
import LineChartCard from './charts/LineChartCard';
|
import LineChartCard from './charts/LineChartCard';
|
||||||
import VisitsTable from './VisitsTable';
|
import VisitsTable from './VisitsTable';
|
||||||
import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
|
import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
|
||||||
|
@ -308,14 +309,11 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
>
|
>
|
||||||
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
|
Clear selection {highlightedVisits.length > 0 && <>({prettify(highlightedVisits.length)})</>}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<ExportBtn
|
||||||
outline
|
|
||||||
color="primary"
|
|
||||||
className="btn-md-block"
|
className="btn-md-block"
|
||||||
|
amount={normalizedVisits.length}
|
||||||
onClick={() => exportCsv(normalizedVisits)}
|
onClick={() => exportCsv(normalizedVisits)}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faFileDownload} /> Export ({prettify(normalizedVisits.length)})
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Button, Progress } from 'reactstrap';
|
import { Progress } from 'reactstrap';
|
||||||
import { sum } from 'ramda';
|
import { sum } from 'ramda';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
|
@ -13,6 +13,7 @@ import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { SelectedServer } from '../../src/servers/data';
|
import { SelectedServer } from '../../src/servers/data';
|
||||||
import { SortableBarChartCard } from '../../src/visits/charts/SortableBarChartCard';
|
import { SortableBarChartCard } from '../../src/visits/charts/SortableBarChartCard';
|
||||||
import { DoughnutChartCard } from '../../src/visits/charts/DoughnutChartCard';
|
import { DoughnutChartCard } from '../../src/visits/charts/DoughnutChartCard';
|
||||||
|
import { ExportBtn } from '../../src/utils/ExportBtn';
|
||||||
|
|
||||||
describe('<VisitsStats />', () => {
|
describe('<VisitsStats />', () => {
|
||||||
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
||||||
|
@ -106,7 +107,7 @@ describe('<VisitsStats />', () => {
|
||||||
|
|
||||||
it('exports CSV when export btn is clicked', () => {
|
it('exports CSV when export btn is clicked', () => {
|
||||||
const wrapper = createComponent({ visits });
|
const wrapper = createComponent({ visits });
|
||||||
const exportBtn = wrapper.find(Button).last();
|
const exportBtn = wrapper.find(ExportBtn).last();
|
||||||
|
|
||||||
expect(exportBtn).toHaveLength(1);
|
expect(exportBtn).toHaveLength(1);
|
||||||
exportBtn.simulate('click');
|
exportBtn.simulate('click');
|
||||||
|
|
Loading…
Reference in a new issue