mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Take into consideration exclñudeBots from query on short URLs row
This commit is contained in:
parent
80cea91339
commit
1d6f4bf5db
7 changed files with 99 additions and 22 deletions
|
@ -80,3 +80,7 @@ export interface ExportableShortUrl {
|
||||||
tags: string;
|
tags: string;
|
||||||
visits: number;
|
visits: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlsFilter {
|
||||||
|
excludeBots?: boolean;
|
||||||
|
}
|
||||||
|
|
38
src/short-urls/helpers/ShortUrlsFilterDropdown.tsx
Normal file
38
src/short-urls/helpers/ShortUrlsFilterDropdown.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { DropdownItem } from 'reactstrap';
|
||||||
|
import { DropdownBtn } from '../../utils/DropdownBtn';
|
||||||
|
import { hasValue } from '../../utils/utils';
|
||||||
|
import { ShortUrlsFilter } from '../data';
|
||||||
|
|
||||||
|
interface ShortUrlsFilterDropdownProps {
|
||||||
|
onChange: (filters: ShortUrlsFilter) => void;
|
||||||
|
selected?: ShortUrlsFilter;
|
||||||
|
className?: string;
|
||||||
|
botsSupported: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShortUrlsFilterDropdown = (
|
||||||
|
{ onChange, selected = {}, className, botsSupported }: ShortUrlsFilterDropdownProps,
|
||||||
|
) => {
|
||||||
|
if (!botsSupported) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { excludeBots = false } = selected;
|
||||||
|
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
||||||
|
{botsSupported && (
|
||||||
|
<>
|
||||||
|
<DropdownItem header>Bots:</DropdownItem>
|
||||||
|
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude bots visits</DropdownItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownItem divider />
|
||||||
|
<DropdownItem disabled={!hasValue(selected)} onClick={() => onChange({ excludeBots: false })}>
|
||||||
|
<i>Clear filters</i>
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownBtn>
|
||||||
|
);
|
||||||
|
};
|
|
@ -11,6 +11,7 @@ import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
|
||||||
import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu';
|
import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu';
|
||||||
import { Tags } from './Tags';
|
import { Tags } from './Tags';
|
||||||
import { ShortUrlStatus } from './ShortUrlStatus';
|
import { ShortUrlStatus } from './ShortUrlStatus';
|
||||||
|
import { useShortUrlsQuery } from './hooks';
|
||||||
import './ShortUrlsRow.scss';
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
interface ShortUrlsRowProps {
|
interface ShortUrlsRowProps {
|
||||||
|
@ -33,7 +34,9 @@ export const ShortUrlsRow = (
|
||||||
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
|
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
|
||||||
const [active, setActive] = useTimeoutToggle(false, 500);
|
const [active, setActive] = useTimeoutToggle(false, 500);
|
||||||
const isFirstRun = useRef(true);
|
const isFirstRun = useRef(true);
|
||||||
|
const [{ excludeBots }] = useShortUrlsQuery();
|
||||||
const { visits } = settings;
|
const { visits } = settings;
|
||||||
|
const doExcludeBots = excludeBots ?? visits?.excludeBots;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!isFirstRun.current && setActive();
|
!isFirstRun.current && setActive();
|
||||||
|
@ -73,7 +76,7 @@ export const ShortUrlsRow = (
|
||||||
<td className="responsive-table__cell short-urls-row__cell text-lg-end" data-th="Visits">
|
<td className="responsive-table__cell short-urls-row__cell text-lg-end" data-th="Visits">
|
||||||
<ShortUrlVisitsCount
|
<ShortUrlVisitsCount
|
||||||
visitsCount={(
|
visitsCount={(
|
||||||
visits?.excludeBots ? shortUrl.visitsSummary?.nonBots : shortUrl.visitsSummary?.total
|
doExcludeBots ? shortUrl.visitsSummary?.nonBots : shortUrl.visitsSummary?.total
|
||||||
) ?? shortUrl.visitsCount}
|
) ?? shortUrl.visitsCount}
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
||||||
import { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
import { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
||||||
import { orderToString, stringToOrder } from '../../utils/helpers/ordering';
|
import { orderToString, stringToOrder } from '../../utils/helpers/ordering';
|
||||||
import { TagsFilteringMode } from '../../api/types';
|
import { TagsFilteringMode } from '../../api/types';
|
||||||
|
import { BooleanString, parseBooleanToString } from '../../utils/utils';
|
||||||
|
|
||||||
interface ShortUrlsQueryCommon {
|
interface ShortUrlsQueryCommon {
|
||||||
search?: string;
|
search?: string;
|
||||||
|
@ -16,11 +17,13 @@ interface ShortUrlsQueryCommon {
|
||||||
interface ShortUrlsQuery extends ShortUrlsQueryCommon {
|
interface ShortUrlsQuery extends ShortUrlsQueryCommon {
|
||||||
orderBy?: string;
|
orderBy?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
|
excludeBots?: BooleanString;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
|
interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
|
||||||
orderBy?: ShortUrlsOrder;
|
orderBy?: ShortUrlsOrder;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
excludeBots?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
||||||
|
@ -33,20 +36,26 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
const filtering = useMemo(
|
const filtering = useMemo(
|
||||||
pipe(
|
pipe(
|
||||||
() => parseQuery<ShortUrlsQuery>(search),
|
() => parseQuery<ShortUrlsQuery>(search),
|
||||||
({ orderBy, tags, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
|
({ orderBy, tags, excludeBots, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
|
||||||
const parsedOrderBy = orderBy ? stringToOrder<ShortUrlsOrderableFields>(orderBy) : undefined;
|
const parsedOrderBy = orderBy ? stringToOrder<ShortUrlsOrderableFields>(orderBy) : undefined;
|
||||||
const parsedTags = tags?.split(',') ?? [];
|
const parsedTags = tags?.split(',') ?? [];
|
||||||
return { ...rest, orderBy: parsedOrderBy, tags: parsedTags };
|
return {
|
||||||
|
...rest,
|
||||||
|
orderBy: parsedOrderBy,
|
||||||
|
tags: parsedTags,
|
||||||
|
excludeBots: excludeBots !== undefined ? excludeBots === 'true' : undefined,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
[search],
|
[search],
|
||||||
);
|
);
|
||||||
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
|
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
|
||||||
const { orderBy, tags, ...mergedFiltering } = { ...filtering, ...extra };
|
const { orderBy, tags, excludeBots, ...mergedFiltering } = { ...filtering, ...extra };
|
||||||
const query: ShortUrlsQuery = {
|
const query: ShortUrlsQuery = {
|
||||||
...mergedFiltering,
|
...mergedFiltering,
|
||||||
orderBy: orderBy && orderToString(orderBy),
|
orderBy: orderBy && orderToString(orderBy),
|
||||||
tags: tags.length > 0 ? tags.join(',') : undefined,
|
tags: tags.length > 0 ? tags.join(',') : undefined,
|
||||||
|
excludeBots: excludeBots === undefined ? undefined : parseBooleanToString(excludeBots),
|
||||||
};
|
};
|
||||||
const stringifiedQuery = stringifyQuery(query);
|
const stringifiedQuery = stringifyQuery(query);
|
||||||
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
||||||
|
|
|
@ -26,3 +26,7 @@ export const nonEmptyValueOrNull = <T>(value: T): T | null => (isEmpty(value) ?
|
||||||
export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
||||||
|
|
||||||
export const equals = (value: any) => (otherValue: any) => value === otherValue;
|
export const equals = (value: any) => (otherValue: any) => value === otherValue;
|
||||||
|
|
||||||
|
export type BooleanString = 'true' | 'false';
|
||||||
|
|
||||||
|
export const parseBooleanToString = (value: boolean): BooleanString => (value ? 'true' : 'false');
|
||||||
|
|
|
@ -6,12 +6,13 @@ import { DateRange, datesToDateRange } from '../../utils/helpers/dateIntervals';
|
||||||
import { OrphanVisitType, VisitsFilter } from '../types';
|
import { OrphanVisitType, VisitsFilter } from '../types';
|
||||||
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
||||||
import { formatIsoDate } from '../../utils/helpers/date';
|
import { formatIsoDate } from '../../utils/helpers/date';
|
||||||
|
import { BooleanString } from '../../utils/utils';
|
||||||
|
|
||||||
interface VisitsQuery {
|
interface VisitsQuery {
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
orphanVisitsType?: OrphanVisitType;
|
orphanVisitsType?: OrphanVisitType;
|
||||||
excludeBots?: 'true' | 'false';
|
excludeBots?: BooleanString;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
|
||||||
import { last } from 'ramda';
|
import { last } from 'ramda';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { addDays, formatISO, subDays } from 'date-fns';
|
import { addDays, formatISO, subDays } from 'date-fns';
|
||||||
|
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||||
import { ShortUrlsRow as createShortUrlsRow } from '../../../src/short-urls/helpers/ShortUrlsRow';
|
import { ShortUrlsRow as createShortUrlsRow } from '../../../src/short-urls/helpers/ShortUrlsRow';
|
||||||
import { TimeoutToggle } from '../../../src/utils/helpers/hooks';
|
import { TimeoutToggle } from '../../../src/utils/helpers/hooks';
|
||||||
import { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data';
|
import { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data';
|
||||||
|
@ -19,6 +20,11 @@ interface SetUpOptions {
|
||||||
settings?: Partial<Settings>;
|
settings?: Partial<Settings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: jest.fn().mockReturnValue({}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<ShortUrlsRow />', () => {
|
describe('<ShortUrlsRow />', () => {
|
||||||
const timeoutToggle = jest.fn(() => true);
|
const timeoutToggle = jest.fn(() => true);
|
||||||
const useTimeoutToggle = jest.fn(() => [false, timeoutToggle]) as TimeoutToggle;
|
const useTimeoutToggle = jest.fn(() => [false, timeoutToggle]) as TimeoutToggle;
|
||||||
|
@ -43,18 +49,24 @@ describe('<ShortUrlsRow />', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const ShortUrlsRow = createShortUrlsRow(() => <span>ShortUrlsRowMenu</span>, colorGeneratorMock, useTimeoutToggle);
|
const ShortUrlsRow = createShortUrlsRow(() => <span>ShortUrlsRowMenu</span>, colorGeneratorMock, useTimeoutToggle);
|
||||||
const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}) => renderWithEvents(
|
|
||||||
<table>
|
const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}, search = '') => {
|
||||||
<tbody>
|
(useLocation as any).mockReturnValue({ search });
|
||||||
<ShortUrlsRow
|
return renderWithEvents(
|
||||||
selectedServer={server}
|
<MemoryRouter>
|
||||||
shortUrl={{ ...shortUrl, title, tags, meta: { ...shortUrl.meta, ...meta } }}
|
<table>
|
||||||
onTagClick={() => null}
|
<tbody>
|
||||||
settings={Mock.of<Settings>(settings)}
|
<ShortUrlsRow
|
||||||
/>
|
selectedServer={server}
|
||||||
</tbody>
|
shortUrl={{ ...shortUrl, title, tags, meta: { ...shortUrl.meta, ...meta } }}
|
||||||
</table>,
|
onTagClick={() => null}
|
||||||
);
|
settings={Mock.of<Settings>(settings)}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[null, 7],
|
[null, 7],
|
||||||
|
@ -105,11 +117,17 @@ describe('<ShortUrlsRow />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{}, shortUrl.visitsSummary?.total],
|
[{}, '', shortUrl.visitsSummary?.total],
|
||||||
[Mock.of<Settings>({ visits: { excludeBots: false } }), shortUrl.visitsSummary?.total],
|
[Mock.of<Settings>({ visits: { excludeBots: false } }), '', shortUrl.visitsSummary?.total],
|
||||||
[Mock.of<Settings>({ visits: { excludeBots: true } }), shortUrl.visitsSummary?.nonBots],
|
[Mock.of<Settings>({ visits: { excludeBots: true } }), '', shortUrl.visitsSummary?.nonBots],
|
||||||
])('renders visits count in fifth row', (settings, expectedAmount) => {
|
[Mock.of<Settings>({ visits: { excludeBots: false } }), 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
|
||||||
setUp({ settings });
|
[Mock.of<Settings>({ visits: { excludeBots: true } }), 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
|
||||||
|
[{}, 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
|
||||||
|
[Mock.of<Settings>({ visits: { excludeBots: true } }), 'excludeBots=false', shortUrl.visitsSummary?.total],
|
||||||
|
[Mock.of<Settings>({ visits: { excludeBots: false } }), 'excludeBots=false', shortUrl.visitsSummary?.total],
|
||||||
|
[{}, 'excludeBots=false', shortUrl.visitsSummary?.total],
|
||||||
|
])('renders visits count in fifth row', (settings, search, expectedAmount) => {
|
||||||
|
setUp({ settings }, search);
|
||||||
expect(screen.getAllByRole('cell')[4]).toHaveTextContent(`${expectedAmount}`);
|
expect(screen.getAllByRole('cell')[4]).toHaveTextContent(`${expectedAmount}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue