mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 09:47:28 +03:00
Added filtering dropdown to short URLs filtering bar
This commit is contained in:
parent
b00f6fadf8
commit
e790360de9
4 changed files with 47 additions and 11 deletions
|
@ -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, supportsBotVisits } 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';
|
||||||
|
@ -16,11 +16,14 @@ import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||||
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
||||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
|
import { ShortUrlsFilterDropdown } from './helpers/ShortUrlsFilterDropdown';
|
||||||
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import './ShortUrlsFilteringBar.scss';
|
import './ShortUrlsFilteringBar.scss';
|
||||||
|
|
||||||
interface ShortUrlsFilteringProps {
|
interface ShortUrlsFilteringProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
order: ShortUrlsOrder;
|
order: ShortUrlsOrder;
|
||||||
|
settings: Settings;
|
||||||
handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void;
|
handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
shortUrlsAmount?: number;
|
shortUrlsAmount?: number;
|
||||||
|
@ -29,8 +32,8 @@ interface ShortUrlsFilteringProps {
|
||||||
export const ShortUrlsFilteringBar = (
|
export const ShortUrlsFilteringBar = (
|
||||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||||
TagsSelector: FC<TagsSelectorProps>,
|
TagsSelector: FC<TagsSelectorProps>,
|
||||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy, settings }) => {
|
||||||
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
const [{ search, tags, startDate, endDate, excludeBots, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
({ startDate: theStartDate, endDate: theEndDate }: DateRange) => ({
|
||||||
startDate: formatIsoDate(theStartDate) ?? undefined,
|
startDate: formatIsoDate(theStartDate) ?? undefined,
|
||||||
|
@ -44,6 +47,7 @@ export const ShortUrlsFilteringBar = (
|
||||||
);
|
);
|
||||||
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
||||||
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
||||||
|
const botsSupported = supportsBotVisits(selectedServer);
|
||||||
const toggleTagsMode = pipe(
|
const toggleTagsMode = pipe(
|
||||||
() => (tagsMode === 'any' ? 'all' : 'any'),
|
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||||
(mode) => toFirstPage({ tagsMode: mode }),
|
(mode) => toFirstPage({ tagsMode: mode }),
|
||||||
|
@ -69,11 +73,21 @@ export const ShortUrlsFilteringBar = (
|
||||||
|
|
||||||
<Row className="flex-lg-row-reverse">
|
<Row className="flex-lg-row-reverse">
|
||||||
<div className="col-lg-8 col-xl-6 mt-3">
|
<div className="col-lg-8 col-xl-6 mt-3">
|
||||||
<DateRangeSelector
|
<div className="d-md-flex">
|
||||||
defaultText="All short URLs"
|
<div className="flex-fill">
|
||||||
initialDateRange={datesToDateRange(startDate, endDate)}
|
<DateRangeSelector
|
||||||
onDatesChange={setDates}
|
defaultText="All short URLs"
|
||||||
/>
|
initialDateRange={datesToDateRange(startDate, endDate)}
|
||||||
|
onDatesChange={setDates}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ShortUrlsFilterDropdown
|
||||||
|
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
||||||
|
botsSupported={botsSupported}
|
||||||
|
selected={{ excludeBots: excludeBots ?? settings.visits?.excludeBots }}
|
||||||
|
onChange={toFirstPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-lg-4 col-xl-6 mt-3">
|
<div className="col-6 col-lg-4 col-xl-6 mt-3">
|
||||||
<ExportShortUrlsBtn amount={shortUrlsAmount} />
|
<ExportShortUrlsBtn amount={shortUrlsAmount} />
|
||||||
|
|
|
@ -31,12 +31,13 @@ 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 }, toFirstPage] = useShortUrlsQuery();
|
const [{ tags, search, startDate, endDate, orderBy, tagsMode, excludeBots }, toFirstPage] = useShortUrlsQuery();
|
||||||
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,
|
||||||
);
|
);
|
||||||
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
||||||
|
const doExcludeBots = excludeBots ?? settings.visits?.excludeBots;
|
||||||
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
|
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
|
||||||
toFirstPage({ orderBy: { field, dir } });
|
toFirstPage({ orderBy: { field, dir } });
|
||||||
setActualOrderBy({ field, dir });
|
setActualOrderBy({ field, dir });
|
||||||
|
@ -50,7 +51,7 @@ export const ShortUrlsList = (
|
||||||
(updatedTags) => toFirstPage({ tags: updatedTags }),
|
(updatedTags) => toFirstPage({ tags: updatedTags }),
|
||||||
);
|
);
|
||||||
const parseOrderByForShlink = ({ field, dir }: ShortUrlsOrder): ShlinkShortUrlsOrder => {
|
const parseOrderByForShlink = ({ field, dir }: ShortUrlsOrder): ShlinkShortUrlsOrder => {
|
||||||
if (supportsExcludeBotsOnShortUrls(selectedServer) && settings.visits?.excludeBots && field === 'visits') {
|
if (supportsExcludeBotsOnShortUrls(selectedServer) && doExcludeBots && field === 'visits') {
|
||||||
return { field: 'nonBotVisits', dir };
|
return { field: 'nonBotVisits', dir };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +77,7 @@ export const ShortUrlsList = (
|
||||||
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
|
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
|
||||||
order={actualOrderBy}
|
order={actualOrderBy}
|
||||||
handleOrderBy={handleOrderBy}
|
handleOrderBy={handleOrderBy}
|
||||||
|
settings={settings}
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
/>
|
/>
|
||||||
<Card body className="pb-0">
|
<Card body className="pb-0">
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const ExportShortUrlsBtn = (
|
||||||
longUrl: shortUrl.longUrl,
|
longUrl: shortUrl.longUrl,
|
||||||
title: shortUrl.title ?? '',
|
title: shortUrl.title ?? '',
|
||||||
tags: shortUrl.tags.join(','),
|
tags: shortUrl.tags.join(','),
|
||||||
visits: shortUrl.visitsCount,
|
visits: shortUrl?.visitsSummary?.total ?? shortUrl.visitsCount,
|
||||||
})));
|
})));
|
||||||
stopLoading();
|
stopLoading();
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { endOfDay, formatISO, startOfDay } from 'date-fns';
|
||||||
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
|
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-urls/ShortUrlsFilteringBar';
|
import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-urls/ShortUrlsFilteringBar';
|
||||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { DateRange } from '../../src/utils/helpers/dateIntervals';
|
import { DateRange } from '../../src/utils/helpers/dateIntervals';
|
||||||
import { formatDate } from '../../src/utils/helpers/date';
|
import { formatDate } from '../../src/utils/helpers/date';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
@ -30,6 +31,7 @@ describe('<ShortUrlsFilteringBar />', () => {
|
||||||
selectedServer={selectedServer ?? Mock.all<SelectedServer>()}
|
selectedServer={selectedServer ?? Mock.all<SelectedServer>()}
|
||||||
order={{}}
|
order={{}}
|
||||||
handleOrderBy={handleOrderBy}
|
handleOrderBy={handleOrderBy}
|
||||||
|
settings={Mock.of<Settings>({ visits: {} })}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
@ -114,6 +116,24 @@ describe('<ShortUrlsFilteringBar />', () => {
|
||||||
expect(navigate).toHaveBeenCalledWith(expect.stringContaining(expectedRedirectTagsMode));
|
expect(navigate).toHaveBeenCalledWith(expect.stringContaining(expectedRedirectTagsMode));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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') => {
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Filters' }));
|
||||||
|
await user.click(await screen.findByRole('menuitem', { name }));
|
||||||
|
};
|
||||||
|
|
||||||
|
await toggleBots();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expect.stringContaining(expectedQuery));
|
||||||
|
});
|
||||||
|
|
||||||
it('handles order through dropdown', async () => {
|
it('handles order through dropdown', async () => {
|
||||||
const { user } = setUp();
|
const { user } = setUp();
|
||||||
const clickMenuItem = async (name: string | RegExp) => {
|
const clickMenuItem = async (name: string | RegExp) => {
|
||||||
|
|
Loading…
Reference in a new issue