mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-11 02:37:22 +03:00
Merge pull request #780 from acelaya-forks/feature/short-urls-filtering
Feature/short urls filtering
This commit is contained in:
commit
9b19113262
14 changed files with 184 additions and 35 deletions
|
@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
|
||||||
This feature also comes with a new setting to disable visits from bots by default, both on short URLs lists and visits sections.
|
This feature also comes with a new setting to disable visits from bots by default, both on short URLs lists and visits sections.
|
||||||
|
|
||||||
|
* [#760](https://github.com/shlinkio/shlink-web-client/issues/760) Added support to exclude short URLs which have reached the maximum amount of visits, or are valid until a date in the past.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#753](https://github.com/shlinkio/shlink-web-client/issues/753) Migrated from react-scripts/webpack to vite.
|
* [#753](https://github.com/shlinkio/shlink-web-client/issues/753) Migrated from react-scripts/webpack to vite.
|
||||||
* [#770](https://github.com/shlinkio/shlink-web-client/issues/770) Updated to latest dependencies.
|
* [#770](https://github.com/shlinkio/shlink-web-client/issues/770) Updated to latest dependencies.
|
||||||
|
|
|
@ -24,9 +24,14 @@ import { HttpClient } from '../../common/services/HttpClient';
|
||||||
|
|
||||||
const buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`;
|
const buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`;
|
||||||
const rejectNilProps = reject(isNil);
|
const rejectNilProps = reject(isNil);
|
||||||
const normalizeOrderByInParams = (
|
const normalizeListParams = (
|
||||||
{ orderBy = {}, ...rest }: ShlinkShortUrlsListParams,
|
{ orderBy = {}, excludeMaxVisitsReached, excludePastValidUntil, ...rest }: ShlinkShortUrlsListParams,
|
||||||
): ShlinkShortUrlsListNormalizedParams => ({ ...rest, orderBy: orderToString(orderBy) });
|
): ShlinkShortUrlsListNormalizedParams => ({
|
||||||
|
...rest,
|
||||||
|
excludeMaxVisitsReached: excludeMaxVisitsReached === true ? 'true' : undefined,
|
||||||
|
excludePastValidUntil: excludePastValidUntil === true ? 'true' : undefined,
|
||||||
|
orderBy: orderToString(orderBy),
|
||||||
|
});
|
||||||
|
|
||||||
export class ShlinkApiClient {
|
export class ShlinkApiClient {
|
||||||
private apiVersion: 2 | 3;
|
private apiVersion: 2 | 3;
|
||||||
|
@ -40,7 +45,7 @@ export class ShlinkApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly listShortUrls = async (params: ShlinkShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> =>
|
public readonly listShortUrls = async (params: ShlinkShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> =>
|
||||||
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', normalizeOrderByInParams(params))
|
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', normalizeListParams(params))
|
||||||
.then(({ shortUrls }) => shortUrls);
|
.then(({ shortUrls }) => shortUrls);
|
||||||
|
|
||||||
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => {
|
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => {
|
||||||
|
|
|
@ -96,14 +96,19 @@ export type ShlinkShortUrlsOrder = Order<ShlinkShortUrlsOrderableFields>;
|
||||||
export interface ShlinkShortUrlsListParams {
|
export interface ShlinkShortUrlsListParams {
|
||||||
page?: string;
|
page?: string;
|
||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
tags?: string[];
|
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
|
tags?: string[];
|
||||||
|
tagsMode?: TagsFilteringMode;
|
||||||
|
orderBy?: ShlinkShortUrlsOrder;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
orderBy?: ShlinkShortUrlsOrder;
|
excludeMaxVisitsReached?: boolean;
|
||||||
tagsMode?: TagsFilteringMode;
|
excludePastValidUntil?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> {
|
export interface ShlinkShortUrlsListNormalizedParams extends
|
||||||
|
Omit<ShlinkShortUrlsListParams, 'orderBy' | 'excludeMaxVisitsReached' | 'excludePastValidUntil'> {
|
||||||
orderBy?: string;
|
orderBy?: string;
|
||||||
|
excludeMaxVisitsReached?: 'true';
|
||||||
|
excludePastValidUntil?: 'true';
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { SearchField } from '../utils/SearchField';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { DateRange, datesToDateRange } from '../utils/helpers/dateIntervals';
|
import { DateRange, datesToDateRange } from '../utils/helpers/dateIntervals';
|
||||||
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
import { supportsAllTagsFiltering, supportsFilterDisabledUrls } from '../utils/helpers/features';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { OrderDir } from '../utils/helpers/ordering';
|
import { OrderDir } from '../utils/helpers/ordering';
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||||
|
@ -33,7 +33,19 @@ export const ShortUrlsFilteringBar = (
|
||||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||||
TagsSelector: FC<TagsSelectorProps>,
|
TagsSelector: FC<TagsSelectorProps>,
|
||||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy, settings }) => {
|
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy, settings }) => {
|
||||||
const [{ search, tags, startDate, endDate, excludeBots, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
const [filter, toFirstPage] = useShortUrlsQuery();
|
||||||
|
const {
|
||||||
|
search,
|
||||||
|
tags,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
excludeBots,
|
||||||
|
excludeMaxVisitsReached,
|
||||||
|
excludePastValidUntil,
|
||||||
|
tagsMode = 'any',
|
||||||
|
} = filter;
|
||||||
|
const supportsDisabledFiltering = supportsFilterDisabledUrls(selectedServer);
|
||||||
|
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
||||||
startDate: formatIsoDate(theStartDate) ?? undefined,
|
startDate: formatIsoDate(theStartDate) ?? undefined,
|
||||||
|
@ -82,8 +94,13 @@ export const ShortUrlsFilteringBar = (
|
||||||
</div>
|
</div>
|
||||||
<ShortUrlsFilterDropdown
|
<ShortUrlsFilterDropdown
|
||||||
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
||||||
selected={{ excludeBots: excludeBots ?? settings.visits?.excludeBots }}
|
selected={{
|
||||||
|
excludeBots: excludeBots ?? settings.visits?.excludeBots,
|
||||||
|
excludeMaxVisitsReached,
|
||||||
|
excludePastValidUntil,
|
||||||
|
}}
|
||||||
onChange={toFirstPage}
|
onChange={toFirstPage}
|
||||||
|
supportsDisabledFiltering={supportsDisabledFiltering}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,7 +31,18 @@ export const ShortUrlsList = (
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
const { page } = useParams();
|
const { page } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode, excludeBots }, toFirstPage] = useShortUrlsQuery();
|
const [filter, toFirstPage] = useShortUrlsQuery();
|
||||||
|
const {
|
||||||
|
tags,
|
||||||
|
search,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
orderBy,
|
||||||
|
tagsMode,
|
||||||
|
excludeBots,
|
||||||
|
excludePastValidUntil,
|
||||||
|
excludeMaxVisitsReached,
|
||||||
|
} = filter;
|
||||||
const [actualOrderBy, setActualOrderBy] = useState(
|
const [actualOrderBy, setActualOrderBy] = useState(
|
||||||
// This separated state handling is needed to be able to fall back to settings value, but only once when loaded
|
// This separated state handling is needed to be able to fall back to settings value, but only once when loaded
|
||||||
orderBy ?? settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING,
|
orderBy ?? settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING,
|
||||||
|
@ -67,8 +78,21 @@ export const ShortUrlsList = (
|
||||||
endDate,
|
endDate,
|
||||||
orderBy: parseOrderByForShlink(actualOrderBy),
|
orderBy: parseOrderByForShlink(actualOrderBy),
|
||||||
tagsMode,
|
tagsMode,
|
||||||
|
excludePastValidUntil,
|
||||||
|
excludeMaxVisitsReached,
|
||||||
});
|
});
|
||||||
}, [page, search, tags, startDate, endDate, actualOrderBy.field, actualOrderBy.dir, tagsMode]);
|
}, [
|
||||||
|
page,
|
||||||
|
search,
|
||||||
|
tags,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
actualOrderBy.field,
|
||||||
|
actualOrderBy.dir,
|
||||||
|
tagsMode,
|
||||||
|
excludePastValidUntil,
|
||||||
|
excludeMaxVisitsReached,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -83,4 +83,6 @@ export interface ExportableShortUrl {
|
||||||
|
|
||||||
export interface ShortUrlsFilter {
|
export interface ShortUrlsFilter {
|
||||||
excludeBots?: boolean;
|
excludeBots?: boolean;
|
||||||
|
excludeMaxVisitsReached?: boolean;
|
||||||
|
excludePastValidUntil?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,23 +5,40 @@ import { ShortUrlsFilter } from '../data';
|
||||||
|
|
||||||
interface ShortUrlsFilterDropdownProps {
|
interface ShortUrlsFilterDropdownProps {
|
||||||
onChange: (filters: ShortUrlsFilter) => void;
|
onChange: (filters: ShortUrlsFilter) => void;
|
||||||
|
supportsDisabledFiltering: boolean;
|
||||||
selected?: ShortUrlsFilter;
|
selected?: ShortUrlsFilter;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShortUrlsFilterDropdown = (
|
export const ShortUrlsFilterDropdown = (
|
||||||
{ onChange, selected = {}, className }: ShortUrlsFilterDropdownProps,
|
{ onChange, selected = {}, className, supportsDisabledFiltering }: ShortUrlsFilterDropdownProps,
|
||||||
) => {
|
) => {
|
||||||
const { excludeBots = false } = selected;
|
const { excludeBots = false, excludeMaxVisitsReached = false, excludePastValidUntil = false } = selected;
|
||||||
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
const onFilterClick = (key: keyof ShortUrlsFilter) => () => onChange({ ...selected, [key]: !selected?.[key] });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
||||||
<DropdownItem header>Bots:</DropdownItem>
|
<DropdownItem header>Visits:</DropdownItem>
|
||||||
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude bots visits</DropdownItem>
|
<DropdownItem active={excludeBots} onClick={onFilterClick('excludeBots')}>Ignore visits from bots</DropdownItem>
|
||||||
|
|
||||||
|
{supportsDisabledFiltering && (
|
||||||
|
<>
|
||||||
|
<DropdownItem divider />
|
||||||
|
<DropdownItem header>Short URLs:</DropdownItem>
|
||||||
|
<DropdownItem active={excludeMaxVisitsReached} onClick={onFilterClick('excludeMaxVisitsReached')}>
|
||||||
|
Exclude with visits reached
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem active={excludePastValidUntil} onClick={onFilterClick('excludePastValidUntil')}>
|
||||||
|
Exclude enabled in the past
|
||||||
|
</DropdownItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem disabled={!hasValue(selected)} onClick={() => onChange({ excludeBots: false })}>
|
<DropdownItem
|
||||||
|
disabled={!hasValue(selected)}
|
||||||
|
onClick={() => onChange({ excludeBots: false, excludeMaxVisitsReached: false, excludePastValidUntil: false })}
|
||||||
|
>
|
||||||
<i>Clear filters</i>
|
<i>Clear filters</i>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownBtn>
|
</DropdownBtn>
|
||||||
|
|
|
@ -5,7 +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';
|
import { BooleanString, parseOptionalBooleanToString } from '../../utils/utils';
|
||||||
|
|
||||||
interface ShortUrlsQueryCommon {
|
interface ShortUrlsQueryCommon {
|
||||||
search?: string;
|
search?: string;
|
||||||
|
@ -18,12 +18,16 @@ interface ShortUrlsQuery extends ShortUrlsQueryCommon {
|
||||||
orderBy?: string;
|
orderBy?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
excludeBots?: BooleanString;
|
excludeBots?: BooleanString;
|
||||||
|
excludeMaxVisitsReached?: BooleanString;
|
||||||
|
excludePastValidUntil?: BooleanString;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
|
interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
|
||||||
orderBy?: ShortUrlsOrder;
|
orderBy?: ShortUrlsOrder;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
excludeBots?: boolean;
|
excludeBots?: boolean;
|
||||||
|
excludeMaxVisitsReached?: boolean;
|
||||||
|
excludePastValidUntil?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
||||||
|
@ -36,7 +40,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
const filtering = useMemo(
|
const filtering = useMemo(
|
||||||
pipe(
|
pipe(
|
||||||
() => parseQuery<ShortUrlsQuery>(search),
|
() => parseQuery<ShortUrlsQuery>(search),
|
||||||
({ orderBy, tags, excludeBots, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
|
({ orderBy, tags, excludeBots, excludeMaxVisitsReached, excludePastValidUntil, ...rest }): ShortUrlsFiltering => {
|
||||||
const parsedOrderBy = orderBy ? stringToOrder<ShortUrlsOrderableFields>(orderBy) : undefined;
|
const parsedOrderBy = orderBy ? stringToOrder<ShortUrlsOrderableFields>(orderBy) : undefined;
|
||||||
const parsedTags = tags?.split(',') ?? [];
|
const parsedTags = tags?.split(',') ?? [];
|
||||||
return {
|
return {
|
||||||
|
@ -44,18 +48,23 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||||
orderBy: parsedOrderBy,
|
orderBy: parsedOrderBy,
|
||||||
tags: parsedTags,
|
tags: parsedTags,
|
||||||
excludeBots: excludeBots !== undefined ? excludeBots === 'true' : undefined,
|
excludeBots: excludeBots !== undefined ? excludeBots === 'true' : undefined,
|
||||||
|
excludeMaxVisitsReached: excludeMaxVisitsReached !== undefined ? excludeMaxVisitsReached === 'true' : undefined,
|
||||||
|
excludePastValidUntil: excludePastValidUntil !== undefined ? excludePastValidUntil === 'true' : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
[search],
|
[search],
|
||||||
);
|
);
|
||||||
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
|
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
|
||||||
const { orderBy, tags, excludeBots, ...mergedFiltering } = { ...filtering, ...extra };
|
const merged = { ...filtering, ...extra };
|
||||||
|
const { orderBy, tags, excludeBots, excludeMaxVisitsReached, excludePastValidUntil, ...mergedFiltering } = merged;
|
||||||
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),
|
excludeBots: parseOptionalBooleanToString(excludeBots),
|
||||||
|
excludeMaxVisitsReached: parseOptionalBooleanToString(excludeMaxVisitsReached),
|
||||||
|
excludePastValidUntil: parseOptionalBooleanToString(excludePastValidUntil),
|
||||||
};
|
};
|
||||||
const stringifiedQuery = stringifyQuery(query);
|
const stringifiedQuery = stringifyQuery(query);
|
||||||
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
||||||
|
|
|
@ -11,3 +11,4 @@ export const supportsNonOrphanVisits = serverMatchesMinVersion('3.0.0');
|
||||||
export const supportsAllTagsFiltering = supportsNonOrphanVisits;
|
export const supportsAllTagsFiltering = supportsNonOrphanVisits;
|
||||||
export const supportsDomainVisits = serverMatchesMinVersion('3.1.0');
|
export const supportsDomainVisits = serverMatchesMinVersion('3.1.0');
|
||||||
export const supportsExcludeBotsOnShortUrls = serverMatchesMinVersion('3.4.0');
|
export const supportsExcludeBotsOnShortUrls = serverMatchesMinVersion('3.4.0');
|
||||||
|
export const supportsFilterDisabledUrls = supportsExcludeBotsOnShortUrls;
|
||||||
|
|
|
@ -30,3 +30,7 @@ export const equals = (value: any) => (otherValue: any) => value === otherValue;
|
||||||
export type BooleanString = 'true' | 'false';
|
export type BooleanString = 'true' | 'false';
|
||||||
|
|
||||||
export const parseBooleanToString = (value: boolean): BooleanString => (value ? 'true' : 'false');
|
export const parseBooleanToString = (value: boolean): BooleanString => (value ? 'true' : 'false');
|
||||||
|
|
||||||
|
export const parseOptionalBooleanToString = (value?: boolean): BooleanString | undefined => (
|
||||||
|
value === undefined ? undefined : parseBooleanToString(value)
|
||||||
|
);
|
||||||
|
|
|
@ -46,6 +46,28 @@ describe('ShlinkApiClient', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{}, ''],
|
||||||
|
[{ excludeMaxVisitsReached: false }, ''],
|
||||||
|
[{ excludeMaxVisitsReached: true }, '?excludeMaxVisitsReached=true'],
|
||||||
|
[{ excludePastValidUntil: false }, ''],
|
||||||
|
[{ excludePastValidUntil: true }, '?excludePastValidUntil=true'],
|
||||||
|
[
|
||||||
|
{ excludePastValidUntil: true, excludeMaxVisitsReached: true },
|
||||||
|
'?excludeMaxVisitsReached=true&excludePastValidUntil=true',
|
||||||
|
],
|
||||||
|
])('parses disabled URLs params', async (params, expectedQuery) => {
|
||||||
|
fetchJson.mockResolvedValue({ data: expectedList });
|
||||||
|
const { listShortUrls } = buildApiClient();
|
||||||
|
|
||||||
|
await listShortUrls(params);
|
||||||
|
|
||||||
|
expect(fetchJson).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(`/short-urls${expectedQuery}`),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createShortUrl', () => {
|
describe('createShortUrl', () => {
|
||||||
|
|
|
@ -117,20 +117,24 @@ describe('<ShortUrlsFilteringBar />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['', 'excludeBots=true'],
|
['', /Ignore visits from bots/, 'excludeBots=true'],
|
||||||
['excludeBots=false', 'excludeBots=true'],
|
['excludeBots=false', /Ignore visits from bots/, 'excludeBots=true'],
|
||||||
['excludeBots=true', 'excludeBots=false'],
|
['excludeBots=true', /Ignore visits from bots/, 'excludeBots=false'],
|
||||||
])('allows to toggle excluding bots through filtering dropdown', async (search, expectedQuery) => {
|
['', /Exclude with visits reached/, 'excludeMaxVisitsReached=true'],
|
||||||
const { user } = setUp(
|
['excludeMaxVisitsReached=false', /Exclude with visits reached/, 'excludeMaxVisitsReached=true'],
|
||||||
search,
|
['excludeMaxVisitsReached=true', /Exclude with visits reached/, 'excludeMaxVisitsReached=false'],
|
||||||
Mock.of<ReachableServer>({ version: '3.4.0' }),
|
['', /Exclude enabled in the past/, 'excludePastValidUntil=true'],
|
||||||
);
|
['excludePastValidUntil=false', /Exclude enabled in the past/, 'excludePastValidUntil=true'],
|
||||||
const toggleBots = async (name = 'Exclude bots visits') => {
|
['excludePastValidUntil=true', /Exclude enabled in the past/, 'excludePastValidUntil=false'],
|
||||||
|
])('allows to toggle filters through filtering dropdown', async (search, menuItemName, expectedQuery) => {
|
||||||
|
const { user } = setUp(search, Mock.of<ReachableServer>({ version: '3.4.0' }));
|
||||||
|
const toggleFilter = async (name: RegExp) => {
|
||||||
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
||||||
await user.click(await screen.findByRole('menuitem', { name }));
|
await waitFor(() => screen.findByRole('menu'));
|
||||||
|
await user.click(screen.getByRole('menuitem', { name }));
|
||||||
};
|
};
|
||||||
|
|
||||||
await toggleBots();
|
await toggleFilter(menuItemName);
|
||||||
expect(navigate).toHaveBeenCalledWith(expect.stringContaining(expectedQuery));
|
expect(navigate).toHaveBeenCalledWith(expect.stringContaining(expectedQuery));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
21
test/short-urls/helpers/ShortUrlsFilterDropdown.test.tsx
Normal file
21
test/short-urls/helpers/ShortUrlsFilterDropdown.test.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
|
import { ShortUrlsFilterDropdown } from '../../../src/short-urls/helpers/ShortUrlsFilterDropdown';
|
||||||
|
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||||
|
|
||||||
|
describe('<ShortUrlsFilterDropdown />', () => {
|
||||||
|
const setUp = (supportsDisabledFiltering: boolean) => renderWithEvents(
|
||||||
|
<ShortUrlsFilterDropdown onChange={jest.fn()} supportsDisabledFiltering={supportsDisabledFiltering} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[true, 3],
|
||||||
|
[false, 1],
|
||||||
|
])('displays proper amount of menu items', async (supportsDisabledFiltering, expectedItems) => {
|
||||||
|
const { user } = setUp(supportsDisabledFiltering);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
||||||
|
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
||||||
|
|
||||||
|
expect(screen.getAllByRole('menuitem')).toHaveLength(expectedItems);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,10 @@
|
||||||
import { capitalize, nonEmptyValueOrNull, parseBooleanToString, rangeOf } from '../../src/utils/utils';
|
import {
|
||||||
|
capitalize,
|
||||||
|
nonEmptyValueOrNull,
|
||||||
|
parseBooleanToString,
|
||||||
|
parseOptionalBooleanToString,
|
||||||
|
rangeOf,
|
||||||
|
} from '../../src/utils/utils';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
describe('rangeOf', () => {
|
describe('rangeOf', () => {
|
||||||
|
@ -58,4 +64,14 @@ describe('utils', () => {
|
||||||
expect(parseBooleanToString(value)).toEqual(expectedResult);
|
expect(parseBooleanToString(value)).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseOptionalBooleanToString', () => {
|
||||||
|
it.each([
|
||||||
|
[undefined, undefined],
|
||||||
|
[true, 'true'],
|
||||||
|
[false, 'false'],
|
||||||
|
])('parses value as expected', (value, expectedResult) => {
|
||||||
|
expect(parseOptionalBooleanToString(value)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue