Take into consideration exclñudeBots from query on short URLs row

This commit is contained in:
Alejandro Celaya 2022-12-23 20:00:59 +01:00
parent 80cea91339
commit 1d6f4bf5db
7 changed files with 99 additions and 22 deletions

View file

@ -80,3 +80,7 @@ export interface ExportableShortUrl {
tags: string; tags: string;
visits: number; visits: number;
} }
export interface ShortUrlsFilter {
excludeBots?: boolean;
}

View 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>
);
};

View file

@ -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}

View file

@ -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}`;

View file

@ -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');

View file

@ -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;
} }

View file

@ -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,7 +49,11 @@ 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(
const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}, search = '') => {
(useLocation as any).mockReturnValue({ search });
return renderWithEvents(
<MemoryRouter>
<table> <table>
<tbody> <tbody>
<ShortUrlsRow <ShortUrlsRow
@ -53,8 +63,10 @@ describe('<ShortUrlsRow />', () => {
settings={Mock.of<Settings>(settings)} settings={Mock.of<Settings>(settings)}
/> />
</tbody> </tbody>
</table>, </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}`);
}); });