From 1d6f4bf5db9951638e82376df1259436fb4ef1f9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 23 Dec 2022 20:00:59 +0100 Subject: [PATCH] =?UTF-8?q?Take=20into=20consideration=20excl=C3=B1udeBots?= =?UTF-8?q?=20from=20query=20on=20short=20URLs=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/short-urls/data/index.ts | 4 ++ .../helpers/ShortUrlsFilterDropdown.tsx | 38 ++++++++++++++ src/short-urls/helpers/ShortUrlsRow.tsx | 5 +- src/short-urls/helpers/hooks.ts | 15 ++++-- src/utils/utils.ts | 4 ++ src/visits/helpers/hooks.ts | 3 +- test/short-urls/helpers/ShortUrlsRow.test.tsx | 52 +++++++++++++------ 7 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 src/short-urls/helpers/ShortUrlsFilterDropdown.tsx 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}`); });