Merge pull request #546 from acelaya-forks/feature/order-by-to-query

Feature/order by to query
This commit is contained in:
Alejandro Celaya 2021-12-25 20:04:40 +01:00 committed by GitHub
commit 729d9e4a39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 109 additions and 55 deletions

View file

@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer. * [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
* [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page. * [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page.
* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs. * [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
* [#542](https://github.com/shlinkio/shlink-web-client/pull/542) Added ordering for short URLs to the query, so that it is consistent with the rest of the filtering params.
### Changed ### Changed
* [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios. * [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios.

View file

@ -19,17 +19,14 @@ import {
ShlinkShortUrlsListNormalizedParams, ShlinkShortUrlsListNormalizedParams,
} from '../types'; } from '../types';
import { stringifyQuery } from '../../utils/helpers/query'; import { stringifyQuery } from '../../utils/helpers/query';
import { orderToString } from '../../utils/helpers/ordering';
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : ''; const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
const rejectNilProps = reject(isNil); const rejectNilProps = reject(isNil);
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => { const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
const { orderBy = {}, ...rest } = params; const { orderBy = {}, ...rest } = params;
const { field, dir } = orderBy;
return !dir ? rest : { return { ...rest, orderBy: orderToString(orderBy) };
...rest,
orderBy: `${field}-${dir}`,
};
}; };
export default class ShlinkApiClient { export default class ShlinkApiClient {

View file

@ -1,3 +0,0 @@
.search-bar__tags-icon {
vertical-align: bottom;
}

View file

@ -0,0 +1,3 @@
.short-urls-filtering-bar__tags-icon {
vertical-align: bottom;
}

View file

@ -10,13 +10,13 @@ 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 { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
import './SearchBar.scss'; import './ShortUrlsFilteringBar.scss';
export type SearchBarProps = RouteChildrenProps<ShortUrlListRouteParams>; export type ShortUrlsFilteringProps = RouteChildrenProps<ShortUrlListRouteParams>;
const dateOrNull = (date?: string) => date ? parseISO(date) : null; 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 [{ search, tags, startDate, endDate }, toFirstPage ] = useShortUrlsQuery(props);
const selectedTags = tags?.split(',') ?? []; const selectedTags = tags?.split(',') ?? [];
const setDates = pipe( const setDates = pipe(
@ -37,7 +37,7 @@ const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) =>
); );
return ( return (
<div className="search-bar-container"> <div className="short-urls-filtering-bar-container">
<SearchField initialValue={search} onChange={setSearch} /> <SearchField initialValue={search} onChange={setSearch} />
<div className="mt-3"> <div className="mt-3">
@ -56,8 +56,8 @@ const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) =>
</div> </div>
{selectedTags.length > 0 && ( {selectedTags.length > 0 && (
<h4 className="search-bar__selected-tag mt-3"> <h4 className="short-urls-filtering-bar__selected-tag mt-3">
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" /> <FontAwesomeIcon icon={tagsIcon} className="short-urls-filtering-bar__tags-icon" />
&nbsp; &nbsp;
{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)} />)}
@ -67,4 +67,4 @@ const SearchBar = (colorGenerator: ColorGenerator) => (props: SearchBarProps) =>
); );
}; };
export default SearchBar; export default ShortUrlsFilteringBar;

View file

@ -14,7 +14,7 @@ import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsTableProps } from './ShortUrlsTable'; import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator'; import Paginator from './Paginator';
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; 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<ShortUrlListRouteParams> { interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams> {
selectedServer: SelectedServer; selectedServer: SelectedServer;
@ -23,7 +23,7 @@ interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams
settings: Settings; settings: Settings;
} }
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) => boundToMercureHub(({ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, ShortUrlsFilteringBar: FC) => boundToMercureHub(({
listShortUrls, listShortUrls,
match, match,
location, location,
@ -33,16 +33,21 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) =
settings, settings,
}: ShortUrlsListProps) => { }: ShortUrlsListProps) => {
const serverId = getServerId(selectedServer); const serverId = getServerId(selectedServer);
const initialOrderBy = settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; const [{ tags, search, startDate, endDate, orderBy }, toFirstPage ] = useShortUrlsQuery({ history, match, location });
const [ order, setOrder ] = useState<ShortUrlsOrder>(initialOrderBy); const [ actualOrderBy, setActualOrderBy ] = useState(
const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); // 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 selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]);
const { pagination } = shortUrlsList?.shortUrls ?? {}; const { pagination } = shortUrlsList?.shortUrls ?? {};
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => {
const handleOrderBy = (field?: ShortUrlsOrderableFields, dir?: OrderDir) => setOrder({ field, dir }); toFirstPage({ orderBy: { field, dir } });
setActualOrderBy({ field, dir });
};
const orderByColumn = (field: ShortUrlsOrderableFields) => () => const orderByColumn = (field: ShortUrlsOrderableFields) => () =>
handleOrderBy(field, determineOrderDir(field, order.field, order.dir)); handleOrderBy(field, determineOrderDir(field, actualOrderBy.field, actualOrderBy.dir));
const renderOrderIcon = (field: ShortUrlsOrderableFields) => <TableOrderIcon currentOrder={order} field={field} />; const renderOrderIcon = (field: ShortUrlsOrderableFields) =>
<TableOrderIcon currentOrder={actualOrderBy} field={field} />;
const addTag = pipe( const addTag = pipe(
(newTag: string) => [ ...new Set([ ...selectedTags, newTag ]) ].join(','), (newTag: string) => [ ...new Set([ ...selectedTags, newTag ]) ].join(','),
(tags) => toFirstPage({ tags }), (tags) => toFirstPage({ tags }),
@ -53,18 +58,17 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) =
page: match.params.page, page: match.params.page,
searchTerm: search, searchTerm: search,
tags: selectedTags, tags: selectedTags,
itemsPerPage: undefined,
startDate, startDate,
endDate, endDate,
orderBy: order, orderBy: actualOrderBy,
}); });
}, [ match.params.page, search, selectedTags, startDate, endDate, order ]); }, [ match.params.page, search, selectedTags, startDate, endDate, actualOrderBy ]);
return ( return (
<> <>
<div className="mb-3"><SearchBar /></div> <div className="mb-3"><ShortUrlsFilteringBar /></div>
<div className="d-block d-lg-none mb-3"> <div className="d-block d-lg-none mb-3">
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={order} onChange={handleOrderBy} /> <OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={actualOrderBy} onChange={handleOrderBy} />
</div> </div>
<Card body className="pb-1"> <Card body className="pb-1">
<ShortUrlsTable <ShortUrlsTable

View file

@ -1,27 +1,50 @@
import { RouteChildrenProps } from 'react-router-dom'; import { RouteChildrenProps } from 'react-router-dom';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isEmpty } from 'ramda'; 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 { orderToString, stringToOrder } from '../../utils/helpers/ordering';
type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>; type ServerIdRouteProps = RouteChildrenProps<{ serverId: string }>;
type ToFirstPage = (extra: Partial<ShortUrlsQuery>) => void; type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
export interface ShortUrlListRouteParams { export interface ShortUrlListRouteParams {
page: string; page: string;
serverId: string; serverId: string;
} }
interface ShortUrlsQuery { interface ShortUrlsQueryCommon {
tags?: string; tags?: string;
search?: string; search?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
} }
export const useShortUrlsQuery = ({ history, location, match }: ServerIdRouteProps): [ShortUrlsQuery, ToFirstPage] => { interface ShortUrlsQuery extends ShortUrlsQueryCommon {
const query = useMemo(() => parseQuery<ShortUrlsQuery>(location.search), [ location ]); orderBy?: string;
const toFirstPageWithExtra = (extra: Partial<ShortUrlsQuery>) => { }
const evolvedQuery = stringifyQuery({ ...query, ...extra });
interface ShortUrlsFiltering extends ShortUrlsQueryCommon {
orderBy?: ShortUrlsOrder;
}
export const useShortUrlsQuery = (
{ history, location, match }: ServerIdRouteProps,
): [ShortUrlsFiltering, ToFirstPage] => {
const query = useMemo(
pipe(
() => parseQuery<ShortUrlsQuery>(location.search),
({ orderBy, ...rest }: ShortUrlsQuery): ShortUrlsFiltering => !orderBy ? rest : {
...rest,
orderBy: stringToOrder<ShortUrlsOrderableFields>(orderBy),
},
),
[ location.search ],
);
const toFirstPageWithExtra = (extra: Partial<ShortUrlsFiltering>) => {
const { orderBy, ...mergedQuery } = { ...query, ...extra };
const normalizedQuery: ShortUrlsQuery = { ...mergedQuery, orderBy: orderBy && orderToString(orderBy) };
const evolvedQuery = stringifyQuery(normalizedQuery);
const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`; const queryString = isEmpty(evolvedQuery) ? '' : `?${evolvedQuery}`;
history.push(`/server/${match?.params.serverId}/list-short-urls/1${queryString}`); history.push(`/server/${match?.params.serverId}/list-short-urls/1${queryString}`);

View file

@ -1,5 +1,5 @@
import Bottle, { Decorator } from 'bottlejs'; import Bottle, { Decorator } from 'bottlejs';
import SearchBar from '../SearchBar'; import ShortUrlsFilteringBar from '../ShortUrlsFilteringBar';
import ShortUrlsList from '../ShortUrlsList'; import ShortUrlsList from '../ShortUrlsList';
import ShortUrlsRow from '../helpers/ShortUrlsRow'; import ShortUrlsRow from '../helpers/ShortUrlsRow';
import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu'; import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu';
@ -19,7 +19,7 @@ import { getShortUrlDetail } from '../reducers/shortUrlDetail';
const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => {
// Components // Components
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar'); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar');
bottle.decorator('ShortUrlsList', connect( bottle.decorator('ShortUrlsList', connect(
[ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ], [ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ],
[ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ], [ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ],
@ -50,8 +50,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ])); bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
// Services // Services
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
bottle.decorator('SearchBar', withRouter); bottle.decorator('ShortUrlsFilteringBar', withRouter);
// Actions // Actions
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');

View file

@ -30,3 +30,12 @@ export const sortList = <List>(list: List[], { field, dir }: Order<Partial<keyof
return a[field] > b[field] ? greaterThan : smallerThan; return a[field] > b[field] ? greaterThan : smallerThan;
}); });
export const orderToString = <T>(order: Order<T>): string | undefined =>
order.dir ? `${order.field}-${order.dir}` : undefined;
export const stringToOrder = <T>(order: string): Order<T> => {
const [ field, dir ] = order.split('-') as [ T | undefined, OrderDir | undefined ];
return { field, dir };
};

View file

@ -33,20 +33,20 @@ describe('<ManageServers />', () => {
bar: createServerMock('bar'), bar: createServerMock('bar'),
baz: createServerMock('baz'), baz: createServerMock('baz'),
}); });
const searchBar = wrapper.find(SearchField); const searchField = wrapper.find(SearchField);
expect(wrapper.find(ManageServersRow)).toHaveLength(3); expect(wrapper.find(ManageServersRow)).toHaveLength(3);
expect(wrapper.find('tbody').find('tr')).toHaveLength(0); 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(ManageServersRow)).toHaveLength(1);
expect(wrapper.find('tbody').find('tr')).toHaveLength(0); 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(ManageServersRow)).toHaveLength(2);
expect(wrapper.find('tbody').find('tr')).toHaveLength(0); 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(ManageServersRow)).toHaveLength(0);
expect(wrapper.find('tbody').find('tr')).toHaveLength(1); expect(wrapper.find('tbody').find('tr')).toHaveLength(1);
}); });

View file

@ -3,21 +3,21 @@ import { Mock } from 'ts-mockery';
import { History, Location } from 'history'; import { History, Location } from 'history';
import { match } from 'react-router'; import { match } from 'react-router';
import { formatISO } from 'date-fns'; 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 SearchField from '../../src/utils/SearchField';
import Tag from '../../src/tags/helpers/Tag'; import Tag from '../../src/tags/helpers/Tag';
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector'; import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
import ColorGenerator from '../../src/utils/services/ColorGenerator'; import ColorGenerator from '../../src/utils/services/ColorGenerator';
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks'; import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
describe('<SearchBar />', () => { describe('<ShortUrlsFilteringBar />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const SearchBar = searchBarCreator(Mock.all<ColorGenerator>()); const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>());
const push = jest.fn(); const push = jest.fn();
const now = new Date(); const now = new Date();
const createWrapper = (props: Partial<SearchBarProps> = {}) => { const createWrapper = (props: Partial<ShortUrlsFilteringProps> = {}) => {
wrapper = shallow( wrapper = shallow(
<SearchBar <ShortUrlsFilteringBar
history={Mock.of<History>({ push })} history={Mock.of<History>({ push })}
location={Mock.of<Location>({ search: '' })} location={Mock.of<Location>({ search: '' })}
match={Mock.of<match<ShortUrlListRouteParams>>({ params: { serverId: '1' } })} match={Mock.of<match<ShortUrlListRouteParams>>({ params: { serverId: '1' } })}

View file

@ -16,7 +16,7 @@ import { Settings } from '../../src/settings/reducers/settings';
describe('<ShortUrlsList />', () => { describe('<ShortUrlsList />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const ShortUrlsTable = () => null; const ShortUrlsTable = () => null;
const SearchBar = () => null; const ShortUrlsFilteringBar = () => null;
const listShortUrlsMock = jest.fn(); const listShortUrlsMock = jest.fn();
const push = jest.fn(); const push = jest.fn();
const shortUrlsList = Mock.of<ShortUrlsListModel>({ const shortUrlsList = Mock.of<ShortUrlsListModel>({
@ -31,7 +31,7 @@ describe('<ShortUrlsList />', () => {
], ],
}, },
}); });
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar); const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, ShortUrlsFilteringBar);
const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow( const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow(
<ShortUrlsList <ShortUrlsList
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })} {...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
@ -56,7 +56,7 @@ describe('<ShortUrlsList />', () => {
expect(wrapper.find(ShortUrlsTable)).toHaveLength(1); expect(wrapper.find(ShortUrlsTable)).toHaveLength(1);
expect(wrapper.find(OrderingDropdown)).toHaveLength(1); expect(wrapper.find(OrderingDropdown)).toHaveLength(1);
expect(wrapper.find(Paginator)).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', () => { it('passes current query to paginator', () => {

View file

@ -34,7 +34,7 @@ describe('date', () => {
}); });
describe('isBetween', () => { describe('isBetween', () => {
test.each([ it.each([
[ now, undefined, undefined, true ], [ now, undefined, undefined, true ],
[ now, subDays(now, 1), undefined, true ], [ now, subDays(now, 1), undefined, true ],
[ now, now, undefined, true ], [ now, now, undefined, true ],
@ -52,7 +52,7 @@ describe('date', () => {
}); });
describe('isBeforeOrEqual', () => { describe('isBeforeOrEqual', () => {
test.each([ it.each([
[ now, now, true ], [ now, now, true ],
[ now, addDays(now, 1), true ], [ now, addDays(now, 1), true ],
[ now, subDays(now, 1), false ], [ now, subDays(now, 1), false ],

View file

@ -1,4 +1,4 @@
import { determineOrderDir } from '../../../src/utils/helpers/ordering'; import { determineOrderDir, OrderDir, orderToString, stringToOrder } from '../../../src/utils/helpers/ordering';
describe('ordering', () => { describe('ordering', () => {
describe('determineOrderDir', () => { describe('determineOrderDir', () => {
@ -22,4 +22,24 @@ describe('ordering', () => {
expect(determineOrderDir('bar', 'bar', 'DESC')).toBeUndefined(); expect(determineOrderDir('bar', 'bar', 'DESC')).toBeUndefined();
}); });
}); });
describe('orderToString', () => {
it.each([
[{}, undefined ],
[{ field: 'foo' }, undefined ],
[{ field: 'foo', dir: 'ASC' as OrderDir }, 'foo-ASC' ],
[{ field: 'bar', dir: 'DESC' as OrderDir }, 'bar-DESC' ],
])('casts the order to string', (order, expectedResult) => {
expect(orderToString(order)).toEqual(expectedResult);
});
});
describe('stringToOrder', () => {
it.each([
[ 'foo-ASC', { field: 'foo', dir: 'ASC' }],
[ 'bar-DESC', { field: 'bar', dir: 'DESC' }],
])('casts a string to an order objects', (order, expectedResult) => {
expect(stringToOrder(order)).toEqual(expectedResult);
});
});
}); });