diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index 330bfb55..a9906302 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -19,17 +19,14 @@ import { ShlinkShortUrlsListNormalizedParams, } from '../types'; import { stringifyQuery } from '../../utils/helpers/query'; +import { orderToString } from '../../utils/helpers/ordering'; const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : ''; const rejectNilProps = reject(isNil); const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => { const { orderBy = {}, ...rest } = params; - const { field, dir } = orderBy; - return !dir ? rest : { - ...rest, - orderBy: `${field}-${dir}`, - }; + return { ...rest, orderBy: orderToString(orderBy) }; }; export default class ShlinkApiClient { diff --git a/src/short-urls/SearchBar.scss b/src/short-urls/SearchBar.scss deleted file mode 100644 index 3a3c64c1..00000000 --- a/src/short-urls/SearchBar.scss +++ /dev/null @@ -1,3 +0,0 @@ -.search-bar__tags-icon { - vertical-align: bottom; -} diff --git a/src/short-urls/ShortUrlsFilteringBar.scss b/src/short-urls/ShortUrlsFilteringBar.scss new file mode 100644 index 00000000..905210fd --- /dev/null +++ b/src/short-urls/ShortUrlsFilteringBar.scss @@ -0,0 +1,3 @@ +.short-urls-filtering-bar__tags-icon { + vertical-align: bottom; +} diff --git a/src/short-urls/SearchBar.tsx b/src/short-urls/ShortUrlsFilteringBar.tsx similarity index 82% rename from src/short-urls/SearchBar.tsx rename to src/short-urls/ShortUrlsFilteringBar.tsx index be650422..e99a9282 100644 --- a/src/short-urls/SearchBar.tsx +++ b/src/short-urls/ShortUrlsFilteringBar.tsx @@ -10,13 +10,13 @@ import { formatIsoDate } from '../utils/helpers/date'; import ColorGenerator from '../utils/services/ColorGenerator'; import { DateRange } from '../utils/dates/types'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; -import './SearchBar.scss'; +import './ShortUrlsFilteringBar.scss'; -export type SearchBarProps = RouteChildrenProps; +export type ShortUrlsFilteringProps = RouteChildrenProps; const dateOrNull = (date?: string) => date ? parseISO(date) : null; -const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) => { +const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator) => (props: ShortUrlsFilteringProps) => { const [{ search, tags, startDate, endDate }, toFirstPage ] = useShortUrlsQuery(props); const selectedTags = tags?.split(',') ?? []; const setDates = pipe( @@ -37,7 +37,7 @@ const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) => ); return ( -
+
@@ -56,8 +56,8 @@ const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) =>
{selectedTags.length > 0 && ( -

- +

+   {selectedTags.map((tag) => removeTag(tag)} />)} @@ -67,4 +67,4 @@ const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) => ); }; -export default SearchBar; +export default ShortUrlsFilteringBar; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index c0f12f6a..73eae8c3 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -14,7 +14,7 @@ import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsTableProps } from './ShortUrlsTable'; import Paginator from './Paginator'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; -import { ShortUrlsOrderableFields, ShortUrlsOrder, SHORT_URLS_ORDERABLE_FIELDS } from './data'; +import { ShortUrlsOrderableFields, SHORT_URLS_ORDERABLE_FIELDS } from './data'; interface ShortUrlsListProps extends RouteComponentProps { selectedServer: SelectedServer; @@ -23,7 +23,7 @@ interface ShortUrlsListProps extends RouteComponentProps, SearchBar: FC) => boundToMercureHub(({ +const ShortUrlsList = (ShortUrlsTable: FC, ShortUrlsFilteringBar: FC) => boundToMercureHub(({ listShortUrls, match, location, @@ -33,16 +33,21 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = settings, }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); - const initialOrderBy = settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; - const [ order, setOrder ] = useState(initialOrderBy); - const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); + const [{ tags, search, startDate, endDate, orderBy }, 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, + ); const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]); const { pagination } = shortUrlsList?.shortUrls ?? {}; - - const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => setOrder({ field, dir }); + const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => { + toFirstPage({ orderBy: { field, dir } }); + setActualOrderBy({ field, dir }); + }; const orderByColumn = (field: ShortUrlsOrderableFields) => () => - handleOrderBy(field, determineOrderDir(field, order.field, order.dir)); - const renderOrderIcon = (field: ShortUrlsOrderableFields) => ; + handleOrderBy(field, determineOrderDir(field, actualOrderBy.field, actualOrderBy.dir)); + const renderOrderIcon = (field: ShortUrlsOrderableFields) => + ; const addTag = pipe( (newTag: string) => [ ...new Set([ ...selectedTags, newTag ]) ].join(','), (tags) => toFirstPage({ tags }), @@ -53,18 +58,17 @@ const ShortUrlsList = (ShortUrlsTable: FC, SearchBar: FC) = page: match.params.page, searchTerm: search, tags: selectedTags, - itemsPerPage: undefined, startDate, endDate, - orderBy: order, + orderBy: actualOrderBy, }); - }, [ match.params.page, search, selectedTags, startDate, endDate, order ]); + }, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy ]); return ( <> -
+
- +
; -type ToFirstPage = (extra: Partial) => void; +type ToFirstPage = (extra: Partial) => void; export interface ShortUrlListRouteParams { page: string; serverId: string; } -interface ShortUrlsQuery { +interface ShortUrlsQueryCommon { tags?: string; search?: string; startDate?: string; endDate?: 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 }); +interface ShortUrlsQuery extends ShortUrlsQueryCommon { + orderBy?: string; +} + +interface ShortUrlsFiltering extends ShortUrlsQueryCommon { + orderBy?: ShortUrlsOrder; +} + +export const useShortUrlsQuery = ( + { history, location, match }: ServerIdRouteProps, +): [ShortUrlsFiltering, ToFirstPage] => { + const query = useMemo( + pipe( + () => parseQuery(location.search), + ({ orderBy, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => !orderBy ? rest : { + ...rest, + orderBy: stringToOrder(orderBy), + }, + ), + [ location.search ], + ); + const toFirstPageWithExtra = (extra: Partial) => { + const { orderBy, ...mergedQuery } = { ...query, ...extra }; + const normalizedQuery: ShortUrlsQuery = { ...mergedQuery, orderBy: orderBy && orderToString(orderBy) }; + const evolvedQuery = stringifyQuery(normalizedQuery); const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`; history.push(`/server/${match?.params.serverId}/list-short-urls/1${queryString}`); diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts index 45b745f2..783234fa 100644 --- a/src/short-urls/services/provideServices.ts +++ b/src/short-urls/services/provideServices.ts @@ -1,5 +1,5 @@ import Bottle, { Decorator } from 'bottlejs'; -import SearchBar from '../SearchBar'; +import ShortUrlsFilteringBar from '../ShortUrlsFilteringBar'; import ShortUrlsList from '../ShortUrlsList'; import ShortUrlsRow from '../helpers/ShortUrlsRow'; import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu'; @@ -19,7 +19,7 @@ import { getShortUrlDetail } from '../reducers/shortUrlDetail'; const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { // Components - bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar'); + bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar'); bottle.decorator('ShortUrlsList', connect( [ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ], [ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ], @@ -50,8 +50,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: bottle.decorator('QrCodeModal', connect([ 'selectedServer' ])); // Services - bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); - bottle.decorator('SearchBar', withRouter); + bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator'); + bottle.decorator('ShortUrlsFilteringBar', withRouter); // Actions bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); diff --git a/src/utils/helpers/ordering.ts b/src/utils/helpers/ordering.ts index f3f19cf3..478cdeed 100644 --- a/src/utils/helpers/ordering.ts +++ b/src/utils/helpers/ordering.ts @@ -30,3 +30,12 @@ export const sortList = (list: List[], { field, dir }: Order b[field] ? greaterThan : smallerThan; }); + +export const orderToString = (order: Order): string | undefined => + order.dir ? `${order.field}-${order.dir}` : undefined; + +export const stringToOrder = (order: string): Order => { + const [ field, dir ] = order.split('-') as [ T | undefined, OrderDir | undefined ]; + + return { field, dir }; +}; diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index c860bb9e..3db3bec2 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -33,20 +33,20 @@ describe('', () => { bar: createServerMock('bar'), baz: createServerMock('baz'), }); - const searchBar = wrapper.find(SearchField); + const searchField = wrapper.find(SearchField); expect(wrapper.find(ManageServersRow)).toHaveLength(3); expect(wrapper.find('tbody').find('tr')).toHaveLength(0); - searchBar.simulate('change', 'foo'); + searchField.simulate('change', 'foo'); expect(wrapper.find(ManageServersRow)).toHaveLength(1); expect(wrapper.find('tbody').find('tr')).toHaveLength(0); - searchBar.simulate('change', 'ba'); + searchField.simulate('change', 'ba'); expect(wrapper.find(ManageServersRow)).toHaveLength(2); expect(wrapper.find('tbody').find('tr')).toHaveLength(0); - searchBar.simulate('change', 'invalid'); + searchField.simulate('change', 'invalid'); expect(wrapper.find(ManageServersRow)).toHaveLength(0); expect(wrapper.find('tbody').find('tr')).toHaveLength(1); }); diff --git a/test/short-urls/SearchBar.test.tsx b/test/short-urls/SearchBar.test.tsx index c4697abe..b8ccdc4c 100644 --- a/test/short-urls/SearchBar.test.tsx +++ b/test/short-urls/SearchBar.test.tsx @@ -3,21 +3,21 @@ import { Mock } from 'ts-mockery'; import { History, Location } from 'history'; import { match } from 'react-router'; import { formatISO } from 'date-fns'; -import searchBarCreator, { SearchBarProps } from '../../src/short-urls/SearchBar'; +import filteringBarCreator, { ShortUrlsFilteringProps } from '../../src/short-urls/ShortUrlsFilteringBar'; 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('', () => { +describe('', () => { let wrapper: ShallowWrapper; - const SearchBar = searchBarCreator(Mock.all()); + const ShortUrlsFilteringBar = filteringBarCreator(Mock.all()); const push = jest.fn(); const now = new Date(); - const createWrapper = (props: Partial = {}) => { + const createWrapper = (props: Partial = {}) => { wrapper = shallow( - ({ push })} location={Mock.of({ search: '' })} match={Mock.of>({ params: { serverId: '1' } })} diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 1df03e6a..da6bfec3 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -12,11 +12,12 @@ import Paginator from '../../src/short-urls/Paginator'; import { ReachableServer } from '../../src/servers/data'; import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks'; import { Settings } from '../../src/settings/reducers/settings'; +import ShortUrlsFilteringBar from '../../src/short-urls/ShortUrlsFilteringBar'; describe('', () => { let wrapper: ShallowWrapper; const ShortUrlsTable = () => null; - const SearchBar = () => null; + const ShortUrlsFilteringBar = () => null; const listShortUrlsMock = jest.fn(); const push = jest.fn(); const shortUrlsList = Mock.of({ @@ -31,7 +32,7 @@ describe('', () => { ], }, }); - const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar); + const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, ShortUrlsFilteringBar); const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow( ({ mercureInfo: { loading: true } })} @@ -56,7 +57,7 @@ describe('', () => { expect(wrapper.find(ShortUrlsTable)).toHaveLength(1); expect(wrapper.find(OrderingDropdown)).toHaveLength(1); expect(wrapper.find(Paginator)).toHaveLength(1); - expect(wrapper.find(SearchBar)).toHaveLength(1); + expect(wrapper.find(ShortUrlsFilteringBar)).toHaveLength(1); }); it('passes current query to paginator', () => { diff --git a/test/utils/helpers/ordering.test.ts b/test/utils/helpers/ordering.test.ts index 147d8d1f..20be691b 100644 --- a/test/utils/helpers/ordering.test.ts +++ b/test/utils/helpers/ordering.test.ts @@ -22,4 +22,12 @@ describe('ordering', () => { expect(determineOrderDir('bar', 'bar', 'DESC')).toBeUndefined(); }); }); + + describe('orderToString', () => { + test.todo('casts the order to string'); + }); + + describe('stringToOrder', () => { + test.todo('casts a string to an order objects'); + }); });