From 2de027619504fef8f0ba63c36e6a46239b8f6cab Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 31 Jan 2022 10:15:25 +0100 Subject: [PATCH 1/5] Added support for tag mode on short URLs list --- src/api/types/index.ts | 3 ++ src/short-urls/ShortUrlsFilteringBar.tsx | 35 ++++++++++++++++++---- src/short-urls/ShortUrlsList.tsx | 8 +++-- src/short-urls/helpers/hooks.ts | 2 ++ src/short-urls/services/provideServices.ts | 1 + src/utils/TooltipToggleSwitch.tsx | 24 +++++++++++++++ src/utils/helpers/features.ts | 2 ++ 7 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 src/utils/TooltipToggleSwitch.tsx 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' }); From f0b42cdc09b471142489108b8e3d837c44e2da59 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 22 Feb 2022 19:23:57 +0100 Subject: [PATCH 2/5] Added missing prop --- test/short-urls/SearchBar.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/short-urls/SearchBar.test.tsx b/test/short-urls/SearchBar.test.tsx index 2567b8e1..833893d0 100644 --- a/test/short-urls/SearchBar.test.tsx +++ b/test/short-urls/SearchBar.test.tsx @@ -7,6 +7,7 @@ import SearchField from '../../src/utils/SearchField'; import Tag from '../../src/tags/helpers/Tag'; import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector'; import ColorGenerator from '../../src/utils/services/ColorGenerator'; +import { SelectedServer } from '../../src/servers/data'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -24,7 +25,7 @@ describe('', () => { (useLocation as any).mockReturnValue({ search }); (useNavigate as any).mockReturnValue(navigate); - wrapper = shallow(); + wrapper = shallow(()} />); return wrapper; }; From 8fd07070b868909b6d39b5ab3ba126a225e9d37a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 26 Feb 2022 11:25:40 +0100 Subject: [PATCH 3/5] Created TooltipToggleSwitch test --- src/utils/TooltipToggleSwitch.tsx | 6 ++-- test/utils/TooltipToggleSwitch.test.tsx | 38 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 test/utils/TooltipToggleSwitch.test.tsx diff --git a/src/utils/TooltipToggleSwitch.tsx b/src/utils/TooltipToggleSwitch.tsx index 6acfba70..0c708bcc 100644 --- a/src/utils/TooltipToggleSwitch.tsx +++ b/src/utils/TooltipToggleSwitch.tsx @@ -4,9 +4,9 @@ import { UncontrolledTooltipProps } from 'reactstrap/lib/Tooltip'; import { BooleanControlProps } from './BooleanControl'; import ToggleSwitch from './ToggleSwitch'; -export const TooltipToggleSwitch: FC }> = ( - { children, tooltip = {}, ...rest }, -) => { +export type TooltipToggleSwitchProps = BooleanControlProps & { tooltip?: Omit }; + +export const TooltipToggleSwitch: FC = ({ children, tooltip = {}, ...rest }) => { const ref = useRef(); return ( diff --git a/test/utils/TooltipToggleSwitch.test.tsx b/test/utils/TooltipToggleSwitch.test.tsx new file mode 100644 index 00000000..d35906d5 --- /dev/null +++ b/test/utils/TooltipToggleSwitch.test.tsx @@ -0,0 +1,38 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { PropsWithChildren } from 'react'; +import { UncontrolledTooltip } from 'reactstrap'; +import { TooltipToggleSwitch, TooltipToggleSwitchProps } from '../../src/utils/TooltipToggleSwitch'; +import ToggleSwitch from '../../src/utils/ToggleSwitch'; + +describe('', () => { + let wrapper: ShallowWrapper; + const createWrapper = (props: PropsWithChildren = {}) => { + wrapper = shallow(); + + return wrapper; + }; + + afterEach(() => wrapper?.unmount()); + + it.each([ + [ 'foo' ], + [ 'bar' ], + [ 'baz' ], + ])('shows children inside tooltip', (children) => { + const wrapper = createWrapper({ children }); + const tooltip = wrapper.find(UncontrolledTooltip); + + expect(tooltip.prop('children')).toEqual(children); + }); + + it('properly propagates corresponding props to every component', () => { + const expectedTooltipProps = { placement: 'left', delay: 30 }; + const expectedToggleProps = { checked: true, className: 'foo' }; + const wrapper = createWrapper({ tooltip: expectedTooltipProps, ...expectedToggleProps }); + const tooltip = wrapper.find(UncontrolledTooltip); + const toggle = wrapper.find(ToggleSwitch); + + expect(tooltip.props()).toEqual(expect.objectContaining(expectedTooltipProps)); + expect(toggle.props()).toEqual(expect.objectContaining(expectedToggleProps)); + }); +}); From 248f887fb35432d27616fce10cb17e0b0945c2a6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 26 Feb 2022 11:55:35 +0100 Subject: [PATCH 4/5] Added missing tests on ShortUrlsFilteringBar test --- src/short-urls/ShortUrlsFilteringBar.tsx | 3 +- ...est.tsx => ShortUrlsFilteringBar.test.tsx} | 49 +++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) rename test/short-urls/{SearchBar.test.tsx => ShortUrlsFilteringBar.test.tsx} (56%) diff --git a/src/short-urls/ShortUrlsFilteringBar.tsx b/src/short-urls/ShortUrlsFilteringBar.tsx index 48ef9d50..a5380be0 100644 --- a/src/short-urls/ShortUrlsFilteringBar.tsx +++ b/src/short-urls/ShortUrlsFilteringBar.tsx @@ -73,8 +73,7 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => ({ selectedSer tooltip={{ placement: 'left' }} onChange={toggleTagsMode} > - {tagsMode === 'all' && 'Short URLs including all tags.'} - {tagsMode !== 'all' && 'Short URLs including any tag.'} + {tagsMode === 'all' ? 'Short URLs including all tags.' : 'Short URLs including any tag.'} )} diff --git a/test/short-urls/SearchBar.test.tsx b/test/short-urls/ShortUrlsFilteringBar.test.tsx similarity index 56% rename from test/short-urls/SearchBar.test.tsx rename to test/short-urls/ShortUrlsFilteringBar.test.tsx index 833893d0..e8718216 100644 --- a/test/short-urls/SearchBar.test.tsx +++ b/test/short-urls/ShortUrlsFilteringBar.test.tsx @@ -7,7 +7,8 @@ import SearchField from '../../src/utils/SearchField'; import Tag from '../../src/tags/helpers/Tag'; import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector'; import ColorGenerator from '../../src/utils/services/ColorGenerator'; -import { SelectedServer } from '../../src/servers/data'; +import { ReachableServer, SelectedServer } from '../../src/servers/data'; +import { TooltipToggleSwitch } from '../../src/utils/TooltipToggleSwitch'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -21,11 +22,11 @@ describe('', () => { const ShortUrlsFilteringBar = filteringBarCreator(Mock.all()); const navigate = jest.fn(); const now = new Date(); - const createWrapper = (search = '') => { + const createWrapper = (search = '', selectedServer?: SelectedServer) => { (useLocation as any).mockReturnValue({ search }); (useNavigate as any).mockReturnValue(navigate); - wrapper = shallow(()} />); + wrapper = shallow(()} />); return wrapper; }; @@ -84,4 +85,46 @@ describe('', () => { dateRange.simulate('datesChange', dates); expect(navigate).toHaveBeenCalledWith(`/server/1/list-short-urls/1?${expectedQuery}`); }); + + it.each([ + [ 'tags=foo,bar,baz', Mock.of({ version: '3.0.0' }), 1 ], + [ 'tags=foo,bar', Mock.of({ version: '3.1.0' }), 1 ], + [ 'tags=foo', Mock.of({ version: '3.0.0' }), 0 ], + [ '', Mock.of({ version: '3.0.0' }), 0 ], + [ 'tags=foo,bar,baz', Mock.of({ version: '2.10.0' }), 0 ], + [ '', Mock.of({ version: '2.10.0' }), 0 ], + ])( + 'renders tags mode toggle if the server supports it and there is more than one tag selected', + (search, selectedServer, expectedTagToggleComponents) => { + const wrapper = createWrapper(search, selectedServer); + const toggle = wrapper.find(TooltipToggleSwitch); + + expect(toggle).toHaveLength(expectedTagToggleComponents); + }, + ); + + it.each([ + [ '', 'Short URLs including any tag.', false ], + [ '&tagsMode=all', 'Short URLs including all tags.', true ], + [ '&tagsMode=any', 'Short URLs including any tag.', false ], + ])('expected tags mode tooltip title', (initialTagsMode, expectedToggleText, expectedChecked) => { + const wrapper = createWrapper(`tags=foo,bar${initialTagsMode}`, Mock.of({ version: '3.0.0' })); + const toggle = wrapper.find(TooltipToggleSwitch); + + expect(toggle.prop('children')).toEqual(expectedToggleText); + expect(toggle.prop('checked')).toEqual(expectedChecked); + }); + + it.each([ + [ '', 'tagsMode=all' ], + [ '&tagsMode=all', 'tagsMode=any' ], + [ '&tagsMode=any', 'tagsMode=all' ], + ])('redirects to first page when tags mode changes', (initialTagsMode, expectedRedirectTagsMode) => { + const wrapper = createWrapper(`tags=foo,bar${initialTagsMode}`, Mock.of({ version: '3.0.0' })); + const toggle = wrapper.find(TooltipToggleSwitch); + + expect(navigate).not.toHaveBeenCalled(); + toggle.simulate('change'); + expect(navigate).toHaveBeenCalledWith(expect.stringContaining(expectedRedirectTagsMode)); + }); }); From 987c27a221b1e248fbdc33a9a43f63bc0c639f8e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 26 Feb 2022 11:59:52 +0100 Subject: [PATCH 5/5] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab0d574b..e152ad87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added * [#558](https://github.com/shlinkio/shlink-web-client/pull/558) Added dark text for tags where the generated background is too light, improving its legibility. * [#570](https://github.com/shlinkio/shlink-web-client/pull/570) Added new section to load non-orphan visits all together. +* [#556](https://github.com/shlinkio/shlink-web-client/pull/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0. ### Changed * [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section.