diff --git a/src/short-urls/SearchBar.tsx b/src/short-urls/SearchBar.tsx index 97225485..0590a183 100644 --- a/src/short-urls/SearchBar.tsx +++ b/src/short-urls/SearchBar.tsx @@ -2,6 +2,7 @@ import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEmpty, pipe } from 'ramda'; import { parseISO } from 'date-fns'; +import { RouteChildrenProps } from 'react-router-dom'; import SearchField from '../utils/SearchField'; import Tag from '../tags/helpers/Tag'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; @@ -9,17 +10,21 @@ import { formatIsoDate } from '../utils/helpers/date'; import ColorGenerator from '../utils/services/ColorGenerator'; import { DateRange } from '../utils/dates/types'; import { ShortUrlsListParams } from './reducers/shortUrlsListParams'; +import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; import './SearchBar.scss'; -interface SearchBarProps { +export interface SearchBarProps extends RouteChildrenProps { listShortUrls: (params: ShortUrlsListParams) => void; shortUrlsListParams: ShortUrlsListParams; } const dateOrNull = (date?: string) => date ? parseISO(date) : null; -const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrlsListParams }: SearchBarProps) => { - const selectedTags = shortUrlsListParams.tags ?? []; +const SearchBar = (colorGenerator: ColorGenerator) => ( + { listShortUrls, shortUrlsListParams, ...rest }: SearchBarProps, +) => { + const [{ search, tags }, toFirstPage ] = useShortUrlsQuery(rest); + const selectedTags = tags?.split(',').map(decodeURIComponent) ?? []; const setDates = pipe( ({ startDate, endDate }: DateRange) => ({ startDate: formatIsoDate(startDate) ?? undefined, @@ -27,10 +32,19 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl }), (dates) => listShortUrls({ ...shortUrlsListParams, ...dates }), ); + const setSearch = pipe( + (searchTerm: string) => isEmpty(searchTerm) ? undefined : searchTerm, + (search) => toFirstPage({ search }), + ); + const removeTag = pipe( + (tag: string) => selectedTags.filter((selectedTag) => selectedTag !== tag), + (tagsList) => tagsList.length === 0 ? undefined : tagsList.join(','), + (tags) => toFirstPage({ tags }), + ); return (
- listShortUrls({ ...shortUrlsListParams, searchTerm })} /> +
@@ -47,24 +61,12 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl
- {!isEmpty(selectedTags) && ( + {selectedTags.length > 0 && (

  - {selectedTags.map((tag) => ( - listShortUrls( - { - ...shortUrlsListParams, - tags: selectedTags.filter((selectedTag) => selectedTag !== tag), - }, - )} - /> - ))} + {selectedTags.map((tag) => + removeTag(tag)} />)}

)}
diff --git a/src/short-urls/helpers/hooks.ts b/src/short-urls/helpers/hooks.ts new file mode 100644 index 00000000..e7ff9f78 --- /dev/null +++ b/src/short-urls/helpers/hooks.ts @@ -0,0 +1,29 @@ +import { RouteChildrenProps } from 'react-router-dom'; +import { useMemo } from 'react'; +import { isEmpty } from 'ramda'; +import { parseQuery, stringifyQuery } from '../../utils/helpers/query'; + +type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>; +type ToFirstPage = (extra: Partial) => void; + +export interface ShortUrlListRouteParams { + page: string; + serverId: string; +} + +interface ShortUrlsQuery { + tags?: string; + search?: string; +} + +export const useShortUrlsQuery = ({ history, location, match }: ServerIdRouteProps): [ShortUrlsQuery, ToFirstPage] => { + const query = useMemo(() => parseQuery(location.search), [ location ]); + const toFirstPageWithExtra = (extra: Partial) => { + const evolvedQuery = stringifyQuery({ ...query, ...extra }); + const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`; + + history.push(`/server/${match?.params.serverId}/list-short-urls/1${queryString}`); + }; + + return [ query, toFirstPageWithExtra ]; +}; diff --git a/src/utils/helpers/query.ts b/src/utils/helpers/query.ts index 7d378f04..8c59f6eb 100644 --- a/src/utils/helpers/query.ts +++ b/src/utils/helpers/query.ts @@ -3,6 +3,3 @@ import qs from 'qs'; export const parseQuery = (search: string) => qs.parse(search, { ignoreQueryPrefix: true }) as unknown as T; export const stringifyQuery = (query: any): string => qs.stringify(query, { arrayFormat: 'brackets' }); - -export const evolveStringifiedQuery = (currentQuery: string, extra: any): string => - stringifyQuery({ ...parseQuery(currentQuery), ...extra }); diff --git a/test/short-urls/SearchBar.test.tsx b/test/short-urls/SearchBar.test.tsx index f16ba98d..cc609369 100644 --- a/test/short-urls/SearchBar.test.tsx +++ b/test/short-urls/SearchBar.test.tsx @@ -1,69 +1,75 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; -import searchBarCreator from '../../src/short-urls/SearchBar'; +import { History, Location } from 'history'; +import { match } from 'react-router'; +import searchBarCreator, { SearchBarProps } from '../../src/short-urls/SearchBar'; 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 { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks'; describe('', () => { let wrapper: ShallowWrapper; const listShortUrlsMock = jest.fn(); const SearchBar = searchBarCreator(Mock.all()); + const push = jest.fn(); + const createWrapper = (props: Partial = {}) => { + wrapper = shallow( + ({ push })} + location={Mock.of({ search: '' })} + match={Mock.of>({ params: { serverId: '1' } })} + {...props} + />, + ); + + return wrapper; + }; afterEach(jest.clearAllMocks); afterEach(() => wrapper?.unmount()); - it('renders a SearchField', () => { - wrapper = shallow(); + it('renders some children components SearchField', () => { + const wrapper = createWrapper(); expect(wrapper.find(SearchField)).toHaveLength(1); - }); - - it('renders a DateRangeSelector', () => { - wrapper = shallow(); - expect(wrapper.find(DateRangeSelector)).toHaveLength(1); }); - it('renders no tags when the list of tags is empty', () => { - wrapper = shallow(); + it.each([ + [ 'tags=foo,bar,baz', 3 ], + [ 'tags=foo,baz', 2 ], + [ '', 0 ], + [ 'foo=bar', 0 ], + ])('renders the proper amount of tags', (search, expectedTagComps) => { + const wrapper = createWrapper({ location: Mock.of({ search }) }); - expect(wrapper.find(Tag)).toHaveLength(0); - }); - - it('renders the proper amount of tags', () => { - const tags = [ 'foo', 'bar', 'baz' ]; - - wrapper = shallow(); - - expect(wrapper.find(Tag)).toHaveLength(tags.length); + expect(wrapper.find(Tag)).toHaveLength(expectedTagComps); }); it('updates short URLs list when search field changes', () => { - wrapper = shallow(); + const wrapper = createWrapper(); const searchField = wrapper.find(SearchField); - expect(listShortUrlsMock).not.toHaveBeenCalled(); - searchField.simulate('change'); - expect(listShortUrlsMock).toHaveBeenCalledTimes(1); + expect(push).not.toHaveBeenCalled(); + searchField.simulate('change', 'search-term'); + expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?search=search-term'); }); it('updates short URLs list when a tag is removed', () => { - wrapper = shallow( - , - ); + const wrapper = createWrapper({ location: Mock.of({ search: 'tags=foo,bar' }) }); const tag = wrapper.find(Tag).first(); - expect(listShortUrlsMock).not.toHaveBeenCalled(); + expect(push).not.toHaveBeenCalled(); tag.simulate('close'); - expect(listShortUrlsMock).toHaveBeenCalledTimes(1); + expect(push).toHaveBeenCalledWith('/server/1/list-short-urls/1?tags=bar'); }); it('updates short URLs list when date range changes', () => { - wrapper = shallow( - , - ); + const wrapper = createWrapper(); const dateRange = wrapper.find(DateRangeSelector); expect(listShortUrlsMock).not.toHaveBeenCalled(); diff --git a/test/utils/helpers/query.test.ts b/test/utils/helpers/query.test.ts index 585af91a..90969bce 100644 --- a/test/utils/helpers/query.test.ts +++ b/test/utils/helpers/query.test.ts @@ -1,4 +1,4 @@ -import { evolveStringifiedQuery, parseQuery, stringifyQuery } from '../../../src/utils/helpers/query'; +import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query'; describe('query', () => { describe('parseQuery', () => { @@ -22,15 +22,4 @@ describe('query', () => { expect(stringifyQuery(queryObj)).toEqual(expectedResult); }); }); - - describe('evolveStringifiedQuery', () => { - it.each([ - [ '?foo=bar', { baz: 123 }, 'foo=bar&baz=123' ], - [ 'foo=bar', { baz: 123 }, 'foo=bar&baz=123' ], - [ 'foo=bar&baz=hello', { baz: 'world' }, 'foo=bar&baz=world' ], - [ '?', { foo: 'some', bar: 'thing' }, 'foo=some&bar=thing' ], - ])('adds and overwrites params on provided query string', (query, extra, expected) => { - expect(evolveStringifiedQuery(query, extra)).toEqual(expected); - }); - }); });