Added support for tag mode on short URLs list

This commit is contained in:
Alejandro Celaya 2022-01-31 10:15:25 +01:00
parent 1011b062ae
commit 2de0276195
7 changed files with 67 additions and 8 deletions

View file

@ -86,6 +86,8 @@ export interface ShlinkDomainsResponse {
defaultRedirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.10 defaultRedirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.10
} }
export type TagsFilteringMode = 'all' | 'any';
export interface ShlinkShortUrlsListParams { export interface ShlinkShortUrlsListParams {
page?: string; page?: string;
itemsPerPage?: number; itemsPerPage?: number;
@ -94,6 +96,7 @@ export interface ShlinkShortUrlsListParams {
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
orderBy?: ShortUrlsOrder; orderBy?: ShortUrlsOrder;
tagsMode?: TagsFilteringMode;
} }
export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> { export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> {

View file

@ -9,15 +9,22 @@ import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
import { formatIsoDate } from '../utils/helpers/date'; import { formatIsoDate } from '../utils/helpers/date';
import ColorGenerator from '../utils/services/ColorGenerator'; import ColorGenerator from '../utils/services/ColorGenerator';
import { DateRange } from '../utils/dates/types'; 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 { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
import './ShortUrlsFilteringBar.scss'; import './ShortUrlsFilteringBar.scss';
export type ShortUrlsFilteringProps = RouteChildrenProps<ShortUrlListRouteParams>; export type ShortUrlsFilteringProps = RouteChildrenProps<ShortUrlListRouteParams> & {
selectedServer: SelectedServer;
};
const dateOrNull = (date?: string) => date ? parseISO(date) : null; const dateOrNull = (date?: string) => date ? parseISO(date) : null;
const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortUrlsFilteringProps) => { const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (
const [{ search, tags, startDate, endDate }, toFirstPage ] = useShortUrlsQuery(props); { selectedServer, ...rest }: ShortUrlsFilteringProps,
) => {
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery(rest);
const selectedTags = tags?.split(',') ?? []; const selectedTags = tags?.split(',') ?? [];
const setDates = pipe( const setDates = pipe(
({ startDate, endDate }: DateRange) => ({ ({ startDate, endDate }: DateRange) => ({
@ -35,6 +42,11 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortU
(tagsList) => tagsList.length === 0 ? undefined : tagsList.join(','), (tagsList) => tagsList.length === 0 ? undefined : tagsList.join(','),
(tags) => toFirstPage({ tags }), (tags) => toFirstPage({ tags }),
); );
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
const toggleTagsMode = pipe(
() => tagsMode === 'any' ? 'all' : 'any',
(tagsMode) => toFirstPage({ tagsMode }),
);
return ( return (
<div className="short-urls-filtering-bar-container"> <div className="short-urls-filtering-bar-container">
@ -56,9 +68,20 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortU
</div> </div>
{selectedTags.length > 0 && ( {selectedTags.length > 0 && (
<h4 className="short-urls-filtering-bar__selected-tag mt-3"> <h4 className="mt-3">
<FontAwesomeIcon icon={tagsIcon} className="short-urls-filtering-bar__tags-icon" /> {canChangeTagsMode && selectedTags.length > 1 && (
&nbsp; <div className="float-right ml-2 mt-1">
<TooltipToggleSwitch
checked={tagsMode === 'all'}
tooltip={{ placement: 'left' }}
onChange={toggleTagsMode}
>
{tagsMode === 'all' && 'Short URLs including all tags.'}
{tagsMode !== 'all' && 'Short URLs including any tag.'}
</TooltipToggleSwitch>
</div>
)}
<FontAwesomeIcon icon={tagsIcon} className="short-urls-filtering-bar__tags-icon mr-1" />
{selectedTags.map((tag) => {selectedTags.map((tag) =>
<Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)} <Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)}
</h4> </h4>

View file

@ -33,7 +33,10 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteri
settings, settings,
}: ShortUrlsListProps) => { }: ShortUrlsListProps) => {
const serverId = getServerId(selectedServer); 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( 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,
@ -61,8 +64,9 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteri
startDate, startDate,
endDate, endDate,
orderBy: actualOrderBy, orderBy: actualOrderBy,
tagsMode,
}); });
}, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy ]); }, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy, tagsMode ]);
return ( return (
<> <>

View file

@ -4,6 +4,7 @@ import { isEmpty, pipe } from 'ramda';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query'; 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';
type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>; type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>;
type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void; type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
@ -18,6 +19,7 @@ interface ShortUrlsQueryCommon {
search?: string; search?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
tagsMode?: TagsFilteringMode;
} }
interface ShortUrlsQuery extends ShortUrlsQueryCommon { interface ShortUrlsQuery extends ShortUrlsQueryCommon {

View file

@ -51,6 +51,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
// Services // Services
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator'); bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
bottle.decorator('ShortUrlsFilteringBar', connect([ 'selectedServer' ]));
bottle.decorator('ShortUrlsFilteringBar', withRouter); bottle.decorator('ShortUrlsFilteringBar', withRouter);
// Actions // Actions

View file

@ -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<BooleanControlProps & { tooltip?: Omit<UncontrolledTooltipProps, 'target'> }> = (
{ children, tooltip = {}, ...rest },
) => {
const ref = useRef<HTMLSpanElement>();
return (
<>
<span
ref={(el) => {
ref.current = el ?? undefined;
}}
>
<ToggleSwitch {...rest} />
</span>
<UncontrolledTooltip target={(() => ref.current) as any} {...tooltip}>{children}</UncontrolledTooltip>
</>
);
};

View file

@ -25,3 +25,5 @@ export const supportsDomainRedirects = supportsQrErrorCorrection;
export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' }); export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' });
export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' }); export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' });
export const supportsAllTagsFiltering = serverMatchesVersions({ minVersion: '3.0.0' });