diff --git a/src/short-urls/ShortUrlsFilteringBar.tsx b/src/short-urls/ShortUrlsFilteringBar.tsx index 85c70891..b1cdb5a8 100644 --- a/src/short-urls/ShortUrlsFilteringBar.tsx +++ b/src/short-urls/ShortUrlsFilteringBar.tsx @@ -8,7 +8,7 @@ import { SearchField } from '../utils/SearchField'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { formatIsoDate } from '../utils/helpers/date'; 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 { OrderDir } from '../utils/helpers/ordering'; import { OrderingDropdown } from '../utils/OrderingDropdown'; @@ -33,7 +33,19 @@ export const ShortUrlsFilteringBar = ( ExportShortUrlsBtn: FC, TagsSelector: FC, ): FC => ({ 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( ({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({ startDate: formatIsoDate(theStartDate) ?? undefined, @@ -82,8 +94,13 @@ export const ShortUrlsFilteringBar = ( diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index a124368c..b29dc925 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -31,7 +31,18 @@ export const ShortUrlsList = ( const serverId = getServerId(selectedServer); const { page } = useParams(); 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( // 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, @@ -67,8 +78,21 @@ export const ShortUrlsList = ( endDate, orderBy: parseOrderByForShlink(actualOrderBy), 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 ( <> diff --git a/src/short-urls/helpers/hooks.ts b/src/short-urls/helpers/hooks.ts index e9bd91f2..95cc5a43 100644 --- a/src/short-urls/helpers/hooks.ts +++ b/src/short-urls/helpers/hooks.ts @@ -5,7 +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'; +import { BooleanString, parseOptionalBooleanToString } from '../../utils/utils'; interface ShortUrlsQueryCommon { search?: string; @@ -18,12 +18,16 @@ interface ShortUrlsQuery extends ShortUrlsQueryCommon { orderBy?: string; tags?: string; excludeBots?: BooleanString; + excludeMaxVisitsReached?: BooleanString; + excludePastValidUntil?: BooleanString; } interface ShortUrlsFiltering extends ShortUrlsQueryCommon { orderBy?: ShortUrlsOrder; tags: string[]; excludeBots?: boolean; + excludeMaxVisitsReached?: boolean; + excludePastValidUntil?: boolean; } type ToFirstPage = (extra: Partial) => void; @@ -36,7 +40,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => { const filtering = useMemo( pipe( () => parseQuery(search), - ({ orderBy, tags, excludeBots, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => { + ({ orderBy, tags, excludeBots, excludeMaxVisitsReached, excludePastValidUntil, ...rest }): ShortUrlsFiltering => { const parsedOrderBy = orderBy ? stringToOrder(orderBy) : undefined; const parsedTags = tags?.split(',') ?? []; return { @@ -44,18 +48,23 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => { orderBy: parsedOrderBy, tags: parsedTags, excludeBots: excludeBots !== undefined ? excludeBots === 'true' : undefined, + excludeMaxVisitsReached: excludeMaxVisitsReached !== undefined ? excludeMaxVisitsReached === 'true' : undefined, + excludePastValidUntil: excludePastValidUntil !== undefined ? excludePastValidUntil === 'true' : undefined, }; }, ), [search], ); const toFirstPageWithExtra = (extra: Partial) => { - const { orderBy, tags, excludeBots, ...mergedFiltering } = { ...filtering, ...extra }; + const merged = { ...filtering, ...extra }; + const { orderBy, tags, excludeBots, excludeMaxVisitsReached, excludePastValidUntil, ...mergedFiltering } = merged; const query: ShortUrlsQuery = { ...mergedFiltering, orderBy: orderBy && orderToString(orderBy), 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 queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`; diff --git a/test/short-urls/ShortUrlsFilteringBar.test.tsx b/test/short-urls/ShortUrlsFilteringBar.test.tsx index ad366d98..5c89c865 100644 --- a/test/short-urls/ShortUrlsFilteringBar.test.tsx +++ b/test/short-urls/ShortUrlsFilteringBar.test.tsx @@ -117,20 +117,24 @@ describe('', () => { }); it.each([ - ['', 'excludeBots=true'], - ['excludeBots=false', 'excludeBots=true'], - ['excludeBots=true', 'excludeBots=false'], - ])('allows to toggle excluding bots through filtering dropdown', async (search, expectedQuery) => { - const { user } = setUp( - search, - Mock.of({ version: '3.4.0' }), - ); - const toggleBots = async (name = 'Exclude bots visits') => { + ['', /Ignore visits from bots/, 'excludeBots=true'], + ['excludeBots=false', /Ignore visits from bots/, 'excludeBots=true'], + ['excludeBots=true', /Ignore visits from bots/, 'excludeBots=false'], + ['', /Exclude with visits reached/, 'excludeMaxVisitsReached=true'], + ['excludeMaxVisitsReached=false', /Exclude with visits reached/, 'excludeMaxVisitsReached=true'], + ['excludeMaxVisitsReached=true', /Exclude with visits reached/, 'excludeMaxVisitsReached=false'], + ['', /Exclude enabled in the past/, 'excludePastValidUntil=true'], + ['excludePastValidUntil=false', /Exclude enabled in the past/, 'excludePastValidUntil=true'], + ['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({ version: '3.4.0' })); + const toggleFilter = async (name: RegExp) => { 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)); });