diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 4dabd92b..ce94d65d 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -86,6 +86,8 @@ export interface ShlinkDomainsResponse { defaultRedirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.10 } +export type TagsFilteringMode = 'all' | 'any'; + export interface ShlinkShortUrlsListParams { page?: string; itemsPerPage?: number; @@ -94,6 +96,7 @@ export interface ShlinkShortUrlsListParams { startDate?: string; endDate?: string; orderBy?: ShortUrlsOrder; + tagsMode?: TagsFilteringMode; } export interface ShlinkShortUrlsListNormalizedParams extends Omit { diff --git a/src/short-urls/ShortUrlsFilteringBar.tsx b/src/short-urls/ShortUrlsFilteringBar.tsx index e99a9282..48bc76ad 100644 --- a/src/short-urls/ShortUrlsFilteringBar.tsx +++ b/src/short-urls/ShortUrlsFilteringBar.tsx @@ -9,15 +9,22 @@ import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { formatIsoDate } from '../utils/helpers/date'; import ColorGenerator from '../utils/services/ColorGenerator'; import { DateRange } from '../utils/dates/types'; +import { supportsAllTagsFiltering } from '../utils/helpers/features'; +import { SelectedServer } from '../servers/data'; +import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; import './ShortUrlsFilteringBar.scss'; -export type ShortUrlsFilteringProps = RouteChildrenProps; +export type ShortUrlsFilteringProps = RouteChildrenProps & { + selectedServer: SelectedServer; +}; const dateOrNull = (date?: string) => date ? parseISO(date) : null; -const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortUrlsFilteringProps) => { - const [{ search, tags, startDate, endDate }, toFirstPage ] = useShortUrlsQuery(props); +const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ( + { selectedServer, ...rest }: ShortUrlsFilteringProps, +) => { + const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery(rest); const selectedTags = tags?.split(',') ?? []; const setDates = pipe( ({ startDate, endDate }: DateRange) => ({ @@ -35,6 +42,11 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortU (tagsList) => tagsList.length === 0 ? undefined : tagsList.join(','), (tags) => toFirstPage({ tags }), ); + const canChangeTagsMode = supportsAllTagsFiltering(selectedServer); + const toggleTagsMode = pipe( + () => tagsMode === 'any' ? 'all' : 'any', + (tagsMode) => toFirstPage({ tagsMode }), + ); return (
@@ -56,9 +68,20 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortU
{selectedTags.length > 0 && ( -

- -   +

+ {canChangeTagsMode && selectedTags.length > 1 && ( +
+ + {tagsMode === 'all' && 'Short URLs including all tags.'} + {tagsMode !== 'all' && 'Short URLs including any tag.'} + +
+ )} + {selectedTags.map((tag) => removeTag(tag)} />)}

diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index 73eae8c3..10865de9 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -33,7 +33,10 @@ const ShortUrlsList = (ShortUrlsTable: FC, ShortUrlsFilteri settings, }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); - const [{ tags, search, startDate, endDate, orderBy }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); + const [ + { tags, search, startDate, endDate, orderBy, tagsMode }, + toFirstPage, + ] = useShortUrlsQuery({ history, match, location }); 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, @@ -61,8 +64,9 @@ const ShortUrlsList = (ShortUrlsTable: FC, ShortUrlsFilteri startDate, endDate, orderBy: actualOrderBy, + tagsMode, }); - }, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy ]); + }, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy, tagsMode ]); return ( <> diff --git a/src/short-urls/helpers/hooks.ts b/src/short-urls/helpers/hooks.ts index d38c341f..c046e202 100644 --- a/src/short-urls/helpers/hooks.ts +++ b/src/short-urls/helpers/hooks.ts @@ -4,6 +4,7 @@ import { isEmpty, pipe } from 'ramda'; import { parseQuery, stringifyQuery } from '../../utils/helpers/query'; import { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data'; import { orderToString, stringToOrder } from '../../utils/helpers/ordering'; +import { TagsFilteringMode } from '../../api/types'; type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>; type ToFirstPage = (extra: Partial) => void; @@ -18,6 +19,7 @@ interface ShortUrlsQueryCommon { search?: string; startDate?: string; endDate?: string; + tagsMode?: TagsFilteringMode; } interface ShortUrlsQuery extends ShortUrlsQueryCommon { diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts index 783234fa..71b42a7b 100644 --- a/src/short-urls/services/provideServices.ts +++ b/src/short-urls/services/provideServices.ts @@ -51,6 +51,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: // Services bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator'); + bottle.decorator('ShortUrlsFilteringBar', connect([ 'selectedServer' ])); bottle.decorator('ShortUrlsFilteringBar', withRouter); // Actions diff --git a/src/utils/TooltipToggleSwitch.tsx b/src/utils/TooltipToggleSwitch.tsx new file mode 100644 index 00000000..6acfba70 --- /dev/null +++ b/src/utils/TooltipToggleSwitch.tsx @@ -0,0 +1,24 @@ +import { FC, useRef } from 'react'; +import { UncontrolledTooltip } from 'reactstrap'; +import { UncontrolledTooltipProps } from 'reactstrap/lib/Tooltip'; +import { BooleanControlProps } from './BooleanControl'; +import ToggleSwitch from './ToggleSwitch'; + +export const TooltipToggleSwitch: FC }> = ( + { children, tooltip = {}, ...rest }, +) => { + const ref = useRef(); + + return ( + <> + { + ref.current = el ?? undefined; + }} + > + + + ref.current) as any} {...tooltip}>{children} + + ); +}; diff --git a/src/utils/helpers/features.ts b/src/utils/helpers/features.ts index 8706a0eb..54c46df4 100644 --- a/src/utils/helpers/features.ts +++ b/src/utils/helpers/features.ts @@ -25,3 +25,5 @@ export const supportsDomainRedirects = supportsQrErrorCorrection; export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' }); export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' }); + +export const supportsAllTagsFiltering = serverMatchesVersions({ minVersion: '3.0.0' });