diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts
index 8c22bdec..343f385a 100644
--- a/src/short-urls/data/index.ts
+++ b/src/short-urls/data/index.ts
@@ -80,3 +80,7 @@ export interface ExportableShortUrl {
tags: string;
visits: number;
}
+
+export interface ShortUrlsFilter {
+ excludeBots?: boolean;
+}
diff --git a/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx b/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx
new file mode 100644
index 00000000..47b25814
--- /dev/null
+++ b/src/short-urls/helpers/ShortUrlsFilterDropdown.tsx
@@ -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 (
+
+ {botsSupported && (
+ <>
+ Bots:
+ Exclude bots visits
+ >
+ )}
+
+
+ onChange({ excludeBots: false })}>
+ Clear filters
+
+
+ );
+};
diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx
index 549266c4..1104ba83 100644
--- a/src/short-urls/helpers/ShortUrlsRow.tsx
+++ b/src/short-urls/helpers/ShortUrlsRow.tsx
@@ -11,6 +11,7 @@ import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu';
import { Tags } from './Tags';
import { ShortUrlStatus } from './ShortUrlStatus';
+import { useShortUrlsQuery } from './hooks';
import './ShortUrlsRow.scss';
interface ShortUrlsRowProps {
@@ -33,7 +34,9 @@ export const ShortUrlsRow = (
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
const [active, setActive] = useTimeoutToggle(false, 500);
const isFirstRun = useRef(true);
+ const [{ excludeBots }] = useShortUrlsQuery();
const { visits } = settings;
+ const doExcludeBots = excludeBots ?? visits?.excludeBots;
useEffect(() => {
!isFirstRun.current && setActive();
@@ -73,7 +76,7 @@ export const ShortUrlsRow = (
) => void;
@@ -33,20 +36,26 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
const filtering = useMemo(
pipe(
() => parseQuery(search),
- ({ orderBy, tags, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
+ ({ orderBy, tags, excludeBots, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
const parsedOrderBy = orderBy ? stringToOrder(orderBy) : undefined;
const parsedTags = tags?.split(',') ?? [];
- return { ...rest, orderBy: parsedOrderBy, tags: parsedTags };
+ return {
+ ...rest,
+ orderBy: parsedOrderBy,
+ tags: parsedTags,
+ excludeBots: excludeBots !== undefined ? excludeBots === 'true' : undefined,
+ };
},
),
[search],
);
const toFirstPageWithExtra = (extra: Partial) => {
- const { orderBy, tags, ...mergedFiltering } = { ...filtering, ...extra };
+ const { orderBy, tags, excludeBots, ...mergedFiltering } = { ...filtering, ...extra };
const query: ShortUrlsQuery = {
...mergedFiltering,
orderBy: orderBy && orderToString(orderBy),
tags: tags.length > 0 ? tags.join(',') : undefined,
+ excludeBots: excludeBots === undefined ? undefined : parseBooleanToString(excludeBots),
};
const stringifiedQuery = stringifyQuery(query);
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 8eda7256..504d450e 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -26,3 +26,7 @@ export const nonEmptyValueOrNull = (value: T): T | null => (isEmpty(value) ?
export const capitalize = (value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
export const equals = (value: any) => (otherValue: any) => value === otherValue;
+
+export type BooleanString = 'true' | 'false';
+
+export const parseBooleanToString = (value: boolean): BooleanString => (value ? 'true' : 'false');
diff --git a/src/visits/helpers/hooks.ts b/src/visits/helpers/hooks.ts
index 54ccff47..fd647023 100644
--- a/src/visits/helpers/hooks.ts
+++ b/src/visits/helpers/hooks.ts
@@ -6,12 +6,13 @@ import { DateRange, datesToDateRange } from '../../utils/helpers/dateIntervals';
import { OrphanVisitType, VisitsFilter } from '../types';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
import { formatIsoDate } from '../../utils/helpers/date';
+import { BooleanString } from '../../utils/utils';
interface VisitsQuery {
startDate?: string;
endDate?: string;
orphanVisitsType?: OrphanVisitType;
- excludeBots?: 'true' | 'false';
+ excludeBots?: BooleanString;
domain?: string;
}
diff --git a/test/short-urls/helpers/ShortUrlsRow.test.tsx b/test/short-urls/helpers/ShortUrlsRow.test.tsx
index 9884b0e1..83b9b4f8 100644
--- a/test/short-urls/helpers/ShortUrlsRow.test.tsx
+++ b/test/short-urls/helpers/ShortUrlsRow.test.tsx
@@ -2,6 +2,7 @@ import { screen } from '@testing-library/react';
import { last } from 'ramda';
import { Mock } from 'ts-mockery';
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 { TimeoutToggle } from '../../../src/utils/helpers/hooks';
import { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data';
@@ -19,6 +20,11 @@ interface SetUpOptions {
settings?: Partial;
}
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: jest.fn().mockReturnValue({}),
+}));
+
describe('', () => {
const timeoutToggle = jest.fn(() => true);
const useTimeoutToggle = jest.fn(() => [false, timeoutToggle]) as TimeoutToggle;
@@ -43,18 +49,24 @@ describe('', () => {
},
};
const ShortUrlsRow = createShortUrlsRow(() => ShortUrlsRowMenu, colorGeneratorMock, useTimeoutToggle);
- const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}) => renderWithEvents(
-
-
- null}
- settings={Mock.of(settings)}
- />
-
- ,
- );
+
+ const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}, search = '') => {
+ (useLocation as any).mockReturnValue({ search });
+ return renderWithEvents(
+
+
+
+ null}
+ settings={Mock.of(settings)}
+ />
+
+
+ ,
+ );
+ };
it.each([
[null, 7],
@@ -105,11 +117,17 @@ describe('', () => {
});
it.each([
- [{}, shortUrl.visitsSummary?.total],
- [Mock.of({ visits: { excludeBots: false } }), shortUrl.visitsSummary?.total],
- [Mock.of({ visits: { excludeBots: true } }), shortUrl.visitsSummary?.nonBots],
- ])('renders visits count in fifth row', (settings, expectedAmount) => {
- setUp({ settings });
+ [{}, '', shortUrl.visitsSummary?.total],
+ [Mock.of({ visits: { excludeBots: false } }), '', shortUrl.visitsSummary?.total],
+ [Mock.of({ visits: { excludeBots: true } }), '', shortUrl.visitsSummary?.nonBots],
+ [Mock.of({ visits: { excludeBots: false } }), 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
+ [Mock.of({ visits: { excludeBots: true } }), 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
+ [{}, 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
+ [Mock.of({ visits: { excludeBots: true } }), 'excludeBots=false', shortUrl.visitsSummary?.total],
+ [Mock.of({ 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}`);
});
|