mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 09:47:28 +03:00
Added support to filter out disabled short URLs
This commit is contained in:
parent
c25b74de84
commit
33498ce903
4 changed files with 74 additions and 20 deletions
|
@ -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<ExportShortUrlsBtnProps>,
|
||||
TagsSelector: FC<TagsSelectorProps>,
|
||||
): 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(
|
||||
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
||||
startDate: formatIsoDate(theStartDate) ?? undefined,
|
||||
|
@ -82,8 +94,13 @@ export const ShortUrlsFilteringBar = (
|
|||
</div>
|
||||
<ShortUrlsFilterDropdown
|
||||
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}
|
||||
supportsDisabledFiltering={supportsDisabledFiltering}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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<ShortUrlsFiltering>) => void;
|
||||
|
@ -36,7 +40,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
|||
const filtering = useMemo(
|
||||
pipe(
|
||||
() => parseQuery<ShortUrlsQuery>(search),
|
||||
({ orderBy, tags, excludeBots, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => {
|
||||
({ orderBy, tags, excludeBots, excludeMaxVisitsReached, excludePastValidUntil, ...rest }): ShortUrlsFiltering => {
|
||||
const parsedOrderBy = orderBy ? stringToOrder<ShortUrlsOrderableFields>(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<ShortUrlsFiltering>) => {
|
||||
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}`;
|
||||
|
|
|
@ -117,20 +117,24 @@ describe('<ShortUrlsFilteringBar />', () => {
|
|||
});
|
||||
|
||||
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<ReachableServer>({ 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<ReachableServer>({ 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));
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue