mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Updated sorting dropdown to accept an order object instead of two individual props
This commit is contained in:
parent
7169c6e083
commit
ee826458be
7 changed files with 41 additions and 64 deletions
|
@ -67,12 +67,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-block d-lg-none mb-3">
|
<div className="d-block d-lg-none mb-3">
|
||||||
<SortingDropdown
|
<SortingDropdown items={SORTABLE_FIELDS} order={order} onChange={handleOrderBy} />
|
||||||
items={SORTABLE_FIELDS}
|
|
||||||
orderField={order.field}
|
|
||||||
orderDir={order.dir}
|
|
||||||
onChange={handleOrderBy}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Card body className="pb-1">
|
<Card body className="pb-1">
|
||||||
<ShortUrlsTable
|
<ShortUrlsTable
|
||||||
|
|
|
@ -55,8 +55,11 @@ const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableP
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderByColumn = (field: OrderableFields) =>
|
const orderByColumn = (field: OrderableFields) => () => {
|
||||||
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
const dir = determineOrderDir(field, order.field, order.dir);
|
||||||
|
|
||||||
|
setOrder({ field: dir ? field : undefined, dir });
|
||||||
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (tagsList.filteredTags.length < 1) {
|
if (tagsList.filteredTags.length < 1) {
|
||||||
|
@ -85,12 +88,7 @@ const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableP
|
||||||
<TagsModeDropdown mode={mode} onChange={setMode} />
|
<TagsModeDropdown mode={mode} onChange={setMode} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-6 mt-3 mt-lg-0">
|
<div className="col-lg-6 mt-3 mt-lg-0">
|
||||||
<SortingDropdown
|
<SortingDropdown items={SORTABLE_FIELDS} order={order} onChange={(field, dir) => setOrder({ field, dir })} />
|
||||||
items={SORTABLE_FIELDS}
|
|
||||||
orderField={order.field}
|
|
||||||
orderDir={order.dir}
|
|
||||||
onChange={(field, dir) => setOrder({ field, dir })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
|
|
|
@ -3,23 +3,22 @@ import { toPairs } from 'ramda';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSortAmountUp as sortAscIcon, faSortAmountDown as sortDescIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faSortAmountUp as sortAscIcon, faSortAmountDown as sortDescIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { determineOrderDir, OrderDir } from './helpers/ordering';
|
import { determineOrderDir, Order, OrderDir } from './helpers/ordering';
|
||||||
import './SortingDropdown.scss';
|
import './SortingDropdown.scss';
|
||||||
|
|
||||||
export interface SortingDropdownProps<T extends string = string> {
|
export interface SortingDropdownProps<T extends string = string> {
|
||||||
items: Record<T, string>;
|
items: Record<T, string>;
|
||||||
orderField?: T;
|
order: Order<T>;
|
||||||
orderDir?: OrderDir;
|
|
||||||
onChange: (orderField?: T, orderDir?: OrderDir) => void;
|
onChange: (orderField?: T, orderDir?: OrderDir) => void;
|
||||||
isButton?: boolean;
|
isButton?: boolean;
|
||||||
right?: boolean;
|
right?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SortingDropdown<T extends string = string>(
|
export default function SortingDropdown<T extends string = string>(
|
||||||
{ items, orderField, orderDir, onChange, isButton = true, right = false }: SortingDropdownProps<T>,
|
{ items, order, onChange, isButton = true, right = false }: SortingDropdownProps<T>,
|
||||||
) {
|
) {
|
||||||
const handleItemClick = (fieldKey: T) => () => {
|
const handleItemClick = (fieldKey: T) => () => {
|
||||||
const newOrderDir = determineOrderDir(fieldKey, orderField, orderDir);
|
const newOrderDir = determineOrderDir(fieldKey, order.field, order.dir);
|
||||||
|
|
||||||
onChange(newOrderDir ? fieldKey : undefined, newOrderDir);
|
onChange(newOrderDir ? fieldKey : undefined, newOrderDir);
|
||||||
};
|
};
|
||||||
|
@ -32,26 +31,26 @@ export default function SortingDropdown<T extends string = string>(
|
||||||
className={classNames({ 'dropdown-btn__toggle btn-block': isButton, 'btn-sm p-0': !isButton })}
|
className={classNames({ 'dropdown-btn__toggle btn-block': isButton, 'btn-sm p-0': !isButton })}
|
||||||
>
|
>
|
||||||
{!isButton && <>Order by</>}
|
{!isButton && <>Order by</>}
|
||||||
{isButton && !orderField && <>Order by...</>}
|
{isButton && !order.field && <>Order by...</>}
|
||||||
{isButton && orderField && `Order by: "${items[orderField]}" - "${orderDir ?? 'DESC'}"`}
|
{isButton && order.field && `Order by: "${items[order.field]}" - "${order.dir ?? 'DESC'}"`}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
right={right}
|
right={right}
|
||||||
className={classNames('w-100', { 'sorting-dropdown__menu--link': !isButton })}
|
className={classNames('w-100', { 'sorting-dropdown__menu--link': !isButton })}
|
||||||
>
|
>
|
||||||
{toPairs(items).map(([ fieldKey, fieldValue ]) => (
|
{toPairs(items).map(([ fieldKey, fieldValue ]) => (
|
||||||
<DropdownItem key={fieldKey} active={orderField === fieldKey} onClick={handleItemClick(fieldKey as T)}>
|
<DropdownItem key={fieldKey} active={order.field === fieldKey} onClick={handleItemClick(fieldKey as T)}>
|
||||||
{fieldValue}
|
{fieldValue}
|
||||||
{orderField === fieldKey && (
|
{order.field === fieldKey && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={orderDir === 'ASC' ? sortAscIcon : sortDescIcon}
|
icon={order.dir === 'ASC' ? sortAscIcon : sortDescIcon}
|
||||||
className="sorting-dropdown__sort-icon"
|
className="sorting-dropdown__sort-icon"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem disabled={!orderField} onClick={() => onChange()}>
|
<DropdownItem disabled={!order.field} onClick={() => onChange()}>
|
||||||
<i>Clear selection</i>
|
<i>Clear selection</i>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
|
import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
|
||||||
import { rangeOf } from '../../utils/utils';
|
import { rangeOf } from '../../utils/utils';
|
||||||
import { OrderDir } from '../../utils/helpers/ordering';
|
import { Order } from '../../utils/helpers/ordering';
|
||||||
import SimplePaginator from '../../common/SimplePaginator';
|
import SimplePaginator from '../../common/SimplePaginator';
|
||||||
import { roundTen } from '../../utils/helpers/numbers';
|
import { roundTen } from '../../utils/helpers/numbers';
|
||||||
import SortingDropdown from '../../utils/SortingDropdown';
|
import SortingDropdown from '../../utils/SortingDropdown';
|
||||||
|
@ -30,24 +30,21 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||||
withPagination = true,
|
withPagination = true,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const [ order, setOrder ] = useState<{ orderField?: string; orderDir?: OrderDir }>({
|
const [ order, setOrder ] = useState<Order<string>>({});
|
||||||
orderField: undefined,
|
|
||||||
orderDir: undefined,
|
|
||||||
});
|
|
||||||
const [ currentPage, setCurrentPage ] = useState(1);
|
const [ currentPage, setCurrentPage ] = useState(1);
|
||||||
const [ itemsPerPage, setItemsPerPage ] = useState(50);
|
const [ itemsPerPage, setItemsPerPage ] = useState(50);
|
||||||
|
|
||||||
const getSortedPairsForStats = (stats: Stats, sortingItems: Record<string, string>) => {
|
const getSortedPairsForStats = (stats: Stats, sortingItems: Record<string, string>) => {
|
||||||
const pairs = toPairs(stats);
|
const pairs = toPairs(stats);
|
||||||
const sortedPairs = !order.orderField ? pairs : sortBy(
|
const sortedPairs = !order.field ? pairs : sortBy(
|
||||||
pipe<StatsRow, string | number, string | number>(
|
pipe<StatsRow, string | number, string | number>(
|
||||||
order.orderField === Object.keys(sortingItems)[0] ? pickKeyFromPair : pickValueFromPair,
|
order.field === Object.keys(sortingItems)[0] ? pickKeyFromPair : pickValueFromPair,
|
||||||
toLowerIfString,
|
toLowerIfString,
|
||||||
),
|
),
|
||||||
pairs,
|
pairs,
|
||||||
);
|
);
|
||||||
|
|
||||||
return !order.orderDir || order.orderDir === 'ASC' ? sortedPairs : reverse(sortedPairs);
|
return !order.dir || order.dir === 'ASC' ? sortedPairs : reverse(sortedPairs);
|
||||||
};
|
};
|
||||||
const determineCurrentPagePairs = (pages: StatsRow[][]): StatsRow[] => {
|
const determineCurrentPagePairs = (pages: StatsRow[][]): StatsRow[] => {
|
||||||
const page = pages[currentPage - 1];
|
const page = pages[currentPage - 1];
|
||||||
|
@ -103,10 +100,9 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||||
isButton={false}
|
isButton={false}
|
||||||
right
|
right
|
||||||
items={sortingItems}
|
items={sortingItems}
|
||||||
orderField={order.orderField}
|
order={order}
|
||||||
orderDir={order.orderDir}
|
onChange={(field, dir) => {
|
||||||
onChange={(orderField, orderDir) => {
|
setOrder({ field, dir });
|
||||||
setOrder({ orderField, orderDir });
|
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -108,23 +108,16 @@ describe('<ShortUrlsList />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles order by through dropdown', () => {
|
it('handles order by through dropdown', () => {
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).not.toBeDefined();
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).not.toBeDefined();
|
|
||||||
|
|
||||||
wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC');
|
wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC');
|
||||||
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'visits', dir: 'ASC' });
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual('visits');
|
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual('ASC');
|
|
||||||
|
|
||||||
wrapper.find(SortingDropdown).simulate('change', 'shortCode', 'DESC');
|
wrapper.find(SortingDropdown).simulate('change', 'shortCode', 'DESC');
|
||||||
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'shortCode', dir: 'DESC' });
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual('shortCode');
|
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual('DESC');
|
|
||||||
|
|
||||||
wrapper.find(SortingDropdown).simulate('change', undefined, undefined);
|
wrapper.find(SortingDropdown).simulate('change', undefined, undefined);
|
||||||
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual(undefined);
|
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual(undefined);
|
|
||||||
|
|
||||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(3);
|
expect(listShortUrlsMock).toHaveBeenCalledTimes(3);
|
||||||
expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||||
|
@ -140,10 +133,9 @@ describe('<ShortUrlsList />', () => {
|
||||||
[ Mock.of<OrderBy>({ visits: 'ASC' }), 'visits', 'ASC' ],
|
[ Mock.of<OrderBy>({ visits: 'ASC' }), 'visits', 'ASC' ],
|
||||||
[ Mock.of<OrderBy>({ title: 'DESC' }), 'title', 'DESC' ],
|
[ Mock.of<OrderBy>({ title: 'DESC' }), 'title', 'DESC' ],
|
||||||
[ Mock.of<OrderBy>(), undefined, undefined ],
|
[ Mock.of<OrderBy>(), undefined, undefined ],
|
||||||
])('has expected initial ordering', (initialOrderBy, expectedField, expectedDir) => {
|
])('has expected initial ordering', (initialOrderBy, field, dir) => {
|
||||||
const wrapper = createWrapper(initialOrderBy);
|
const wrapper = createWrapper(initialOrderBy);
|
||||||
|
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual(expectedField);
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field, dir });
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual(expectedDir);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,14 +89,11 @@ describe('<TagsList />', () => {
|
||||||
it('triggers ordering when sorting dropdown changes', () => {
|
it('triggers ordering when sorting dropdown changes', () => {
|
||||||
const wrapper = createWrapper({ filteredTags: [] });
|
const wrapper = createWrapper({ filteredTags: [] });
|
||||||
|
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).not.toBeDefined();
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).not.toBeDefined();
|
|
||||||
wrapper.find(SortingDropdown).simulate('change', 'tag', 'DESC');
|
wrapper.find(SortingDropdown).simulate('change', 'tag', 'DESC');
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual('tag');
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'tag', dir: 'DESC' });
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual('DESC');
|
|
||||||
wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC');
|
wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC');
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderField')).toEqual('visits');
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'visits', dir: 'ASC' });
|
||||||
expect(wrapper.find(SortingDropdown).prop('orderDir')).toEqual('ASC');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can update current order via orderByColumn from table component', () => {
|
it('can update current order via orderByColumn from table component', () => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe('<SortingDropdown />', () => {
|
||||||
baz: 'Hello World',
|
baz: 'Hello World',
|
||||||
};
|
};
|
||||||
const createWrapper = (props: Partial<SortingDropdownProps> = {}) => {
|
const createWrapper = (props: Partial<SortingDropdownProps> = {}) => {
|
||||||
wrapper = shallow(<SortingDropdown items={items} onChange={identity} {...props} />);
|
wrapper = shallow(<SortingDropdown items={items} order={{}} onChange={identity} {...props} />);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
@ -34,7 +34,7 @@ describe('<SortingDropdown />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly marks selected field as active with proper icon', () => {
|
it('properly marks selected field as active with proper icon', () => {
|
||||||
const wrapper = createWrapper({ orderField: 'bar', orderDir: 'DESC' });
|
const wrapper = createWrapper({ order: { field: 'bar', dir: 'DESC' } });
|
||||||
const activeItem = wrapper.find('DropdownItem[active=true]');
|
const activeItem = wrapper.find('DropdownItem[active=true]');
|
||||||
const activeItemIcon = activeItem.first().find(FontAwesomeIcon);
|
const activeItemIcon = activeItem.first().find(FontAwesomeIcon);
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ describe('<SortingDropdown />', () => {
|
||||||
|
|
||||||
it('triggers change function when item is clicked and an order field was provided', () => {
|
it('triggers change function when item is clicked and an order field was provided', () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const wrapper = createWrapper({ onChange, orderField: 'baz', orderDir: 'ASC' });
|
const wrapper = createWrapper({ onChange, order: { field: 'baz', dir: 'ASC' } });
|
||||||
const firstItem = wrapper.find(DropdownItem).first();
|
const firstItem = wrapper.find(DropdownItem).first();
|
||||||
|
|
||||||
firstItem.simulate('click');
|
firstItem.simulate('click');
|
||||||
|
@ -66,7 +66,7 @@ describe('<SortingDropdown />', () => {
|
||||||
|
|
||||||
it('updates order dir when already selected item is clicked', () => {
|
it('updates order dir when already selected item is clicked', () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const wrapper = createWrapper({ onChange, orderField: 'foo', orderDir: 'ASC' });
|
const wrapper = createWrapper({ onChange, order: { field: 'foo', dir: 'ASC' } });
|
||||||
const firstItem = wrapper.find(DropdownItem).first();
|
const firstItem = wrapper.find(DropdownItem).first();
|
||||||
|
|
||||||
firstItem.simulate('click');
|
firstItem.simulate('click');
|
||||||
|
@ -79,14 +79,14 @@ describe('<SortingDropdown />', () => {
|
||||||
[{ isButton: false }, <>Order by</> ],
|
[{ isButton: false }, <>Order by</> ],
|
||||||
[{ isButton: true }, <>Order by...</> ],
|
[{ isButton: true }, <>Order by...</> ],
|
||||||
[
|
[
|
||||||
{ isButton: true, orderField: 'foo', orderDir: 'ASC' as OrderDir },
|
{ isButton: true, order: { field: 'foo', dir: 'ASC' as OrderDir } },
|
||||||
'Order by: "Foo" - "ASC"',
|
'Order by: "Foo" - "ASC"',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ isButton: true, orderField: 'baz', orderDir: 'DESC' as OrderDir },
|
{ isButton: true, order: { field: 'baz', dir: 'DESC' as OrderDir } },
|
||||||
'Order by: "Hello World" - "DESC"',
|
'Order by: "Hello World" - "DESC"',
|
||||||
],
|
],
|
||||||
[{ isButton: true, orderField: 'baz' }, 'Order by: "Hello World" - "DESC"' ],
|
[{ isButton: true, order: { field: 'baz' } }, 'Order by: "Hello World" - "DESC"' ],
|
||||||
])('displays expected text in toggle', (props, expectedText) => {
|
])('displays expected text in toggle', (props, expectedText) => {
|
||||||
const wrapper = createWrapper(props);
|
const wrapper = createWrapper(props);
|
||||||
const toggle = wrapper.find(DropdownToggle);
|
const toggle = wrapper.find(DropdownToggle);
|
||||||
|
|
Loading…
Reference in a new issue