mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +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;
|
||||
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 { 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 = (
|
|||
<td className="responsive-table__cell short-urls-row__cell text-lg-end" data-th="Visits">
|
||||
<ShortUrlVisitsCount
|
||||
visitsCount={(
|
||||
visits?.excludeBots ? shortUrl.visitsSummary?.nonBots : shortUrl.visitsSummary?.total
|
||||
doExcludeBots ? shortUrl.visitsSummary?.nonBots : shortUrl.visitsSummary?.total
|
||||
) ?? shortUrl.visitsCount}
|
||||
shortUrl={shortUrl}
|
||||
selectedServer={selectedServer}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
|
|||
import { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
||||
import { orderToString, stringToOrder } from '../../utils/helpers/ordering';
|
||||
import { TagsFilteringMode } from '../../api/types';
|
||||
import { BooleanString, parseBooleanToString } from '../../utils/utils';
|
||||
|
||||
interface ShortUrlsQueryCommon {
|
||||
search?: string;
|
||||
|
@ -16,11 +17,13 @@ interface ShortUrlsQueryCommon {
|
|||
interface ShortUrlsQuery extends ShortUrlsQueryCommon {
|
||||
orderBy?: string;
|
||||
tags?: string;
|
||||
excludeBots?: BooleanString;
|
||||
}
|
||||
|
||||
interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
|
||||
orderBy?: ShortUrlsOrder;
|
||||
tags: string[];
|
||||
excludeBots?: boolean;
|
||||
}
|
||||
|
||||
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
||||
|
@ -33,20 +36,26 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
|||
const filtering = useMemo(
|
||||
pipe(
|
||||
() => parseQuery<ShortUrlsQuery>(search),
|
||||
({ orderBy, tags, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
|
||||
({ orderBy, tags, excludeBots, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
|
||||
const parsedOrderBy = orderBy ? stringToOrder<ShortUrlsOrderableFields>(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<ShortUrlsFiltering>) => {
|
||||
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}`;
|
||||
|
|
|
@ -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 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 { 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Settings>;
|
||||
}
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
describe('<ShortUrlsRow />', () => {
|
||||
const timeoutToggle = jest.fn(() => true);
|
||||
const useTimeoutToggle = jest.fn(() => [false, timeoutToggle]) as TimeoutToggle;
|
||||
|
@ -43,18 +49,24 @@ describe('<ShortUrlsRow />', () => {
|
|||
},
|
||||
};
|
||||
const ShortUrlsRow = createShortUrlsRow(() => <span>ShortUrlsRowMenu</span>, colorGeneratorMock, useTimeoutToggle);
|
||||
const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}) => renderWithEvents(
|
||||
<table>
|
||||
<tbody>
|
||||
<ShortUrlsRow
|
||||
selectedServer={server}
|
||||
shortUrl={{ ...shortUrl, title, tags, meta: { ...shortUrl.meta, ...meta } }}
|
||||
onTagClick={() => null}
|
||||
settings={Mock.of<Settings>(settings)}
|
||||
/>
|
||||
</tbody>
|
||||
</table>,
|
||||
);
|
||||
|
||||
const setUp = ({ title, tags = [], meta = {}, settings = {} }: SetUpOptions = {}, search = '') => {
|
||||
(useLocation as any).mockReturnValue({ search });
|
||||
return renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<table>
|
||||
<tbody>
|
||||
<ShortUrlsRow
|
||||
selectedServer={server}
|
||||
shortUrl={{ ...shortUrl, title, tags, meta: { ...shortUrl.meta, ...meta } }}
|
||||
onTagClick={() => null}
|
||||
settings={Mock.of<Settings>(settings)}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
};
|
||||
|
||||
it.each([
|
||||
[null, 7],
|
||||
|
@ -105,11 +117,17 @@ describe('<ShortUrlsRow />', () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
[{}, shortUrl.visitsSummary?.total],
|
||||
[Mock.of<Settings>({ visits: { excludeBots: false } }), shortUrl.visitsSummary?.total],
|
||||
[Mock.of<Settings>({ visits: { excludeBots: true } }), shortUrl.visitsSummary?.nonBots],
|
||||
])('renders visits count in fifth row', (settings, expectedAmount) => {
|
||||
setUp({ settings });
|
||||
[{}, '', 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: false } }), 'excludeBots=true', shortUrl.visitsSummary?.nonBots],
|
||||
[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}`);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue