Added not-enabled sorting on tags table

This commit is contained in:
Alejandro Celaya 2021-11-01 12:48:11 +01:00
parent 844cf51d04
commit 5241925acc
17 changed files with 94 additions and 82 deletions

View file

@ -5,7 +5,7 @@ import { FC, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { Card } from 'reactstrap'; import { Card } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown'; import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, OrderDir } from '../utils/utils'; import { determineOrderDir, OrderDir } from '../utils/helpers/ordering';
import { getServerId, SelectedServer } from '../servers/data'; import { getServerId, SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { parseQuery } from '../utils/helpers/query'; import { parseQuery } from '../utils/helpers/query';

View file

@ -1,5 +1,5 @@
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { OrderDir } from '../../utils/utils'; import { OrderDir } from '../../utils/helpers/ordering';
import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList'; import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList';
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS'; export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';

View file

@ -6,4 +6,5 @@
top: $headerHeight; top: $headerHeight;
position: sticky; position: sticky;
cursor: pointer;
} }

View file

@ -1,24 +1,37 @@
import { FC, useEffect, useRef } from 'react'; import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { splitEvery } from 'ramda'; import { splitEvery } from 'ramda';
import { RouteChildrenProps } from 'react-router'; import { RouteChildrenProps } from 'react-router';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import ColorGenerator from '../utils/services/ColorGenerator';
import SimplePaginator from '../common/SimplePaginator'; import SimplePaginator from '../common/SimplePaginator';
import { useQueryState } from '../utils/helpers/hooks'; import { useQueryState } from '../utils/helpers/hooks';
import { parseQuery } from '../utils/helpers/query'; import { parseQuery } from '../utils/helpers/query';
import { Order, sortList } from '../utils/helpers/ordering';
import { TagsListChildrenProps } from './data/TagsListChildrenProps'; import { TagsListChildrenProps } from './data/TagsListChildrenProps';
import { TagsTableRowProps } from './TagsTableRow'; import { TagsTableRowProps } from './TagsTableRow';
import { NormalizedTag } from './data';
import './TagsTable.scss'; import './TagsTable.scss';
const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings
export const TagsTable = (colorGenerator: ColorGenerator, TagsTableRow: FC<TagsTableRowProps>) => ( type OrderableFields = 'tag' | 'shortUrls' | 'visits';
type TagsOrder = Order<OrderableFields>;
export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
{ tagsList, selectedServer, location }: TagsListChildrenProps & RouteChildrenProps, { tagsList, selectedServer, location }: TagsListChildrenProps & RouteChildrenProps,
) => { ) => {
const isFirstLoad = useRef(true); const isFirstLoad = useRef(true);
const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(location.search); const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(location.search);
const [ page, setPage ] = useQueryState<number>('page', Number(pageFromQuery)); const [ page, setPage ] = useQueryState<number>('page', Number(pageFromQuery));
const sortedTags = tagsList.filteredTags; // TODO Support sorting tags const [ order ] = useState<TagsOrder>({});
const normalizedTags = useMemo(
() => tagsList.filteredTags.map((tag): NormalizedTag => ({
tag,
shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 0,
visits: tagsList.stats[tag]?.visitsCount ?? 0,
})),
[ tagsList.filteredTags ],
);
const sortedTags = sortList<NormalizedTag>(normalizedTags, order);
const pages = splitEvery(TAGS_PER_PAGE, sortedTags); const pages = splitEvery(TAGS_PER_PAGE, sortedTags);
const showPaginator = pages.length > 1; const showPaginator = pages.length > 1;
const currentPage = pages[page - 1] ?? []; const currentPage = pages[page - 1] ?? [];
@ -45,15 +58,7 @@ export const TagsTable = (colorGenerator: ColorGenerator, TagsTableRow: FC<TagsT
</thead> </thead>
<tbody> <tbody>
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>} {currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
{currentPage.map((tag) => ( {currentPage.map((tag) => <TagsTableRow key={tag.tag} tag={tag} selectedServer={selectedServer} />)}
<TagsTableRow
key={tag}
tag={tag}
tagStats={tagsList.stats[tag]}
selectedServer={selectedServer}
colorGenerator={colorGenerator}
/>
))}
</tbody> </tbody>
</table> </table>

View file

@ -9,18 +9,18 @@ import { prettify } from '../utils/helpers/numbers';
import { useToggle } from '../utils/helpers/hooks'; import { useToggle } from '../utils/helpers/hooks';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu'; import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
import TagBullet from './helpers/TagBullet'; import TagBullet from './helpers/TagBullet';
import { TagModalProps, TagStats } from './data'; import { NormalizedTag, TagModalProps } from './data';
export interface TagsTableRowProps { export interface TagsTableRowProps {
tag: string; tag: NormalizedTag;
tagStats?: TagStats;
selectedServer: SelectedServer; selectedServer: SelectedServer;
colorGenerator: ColorGenerator;
} }
export const TagsTableRow = (DeleteTagConfirmModal: FC<TagModalProps>, EditTagModal: FC<TagModalProps>) => ( export const TagsTableRow = (
{ tag, tagStats, colorGenerator, selectedServer }: TagsTableRowProps, DeleteTagConfirmModal: FC<TagModalProps>,
) => { EditTagModal: FC<TagModalProps>,
colorGenerator: ColorGenerator,
) => ({ tag, selectedServer }: TagsTableRowProps) => {
const [ isDeleteModalOpen, toggleDelete ] = useToggle(); const [ isDeleteModalOpen, toggleDelete ] = useToggle();
const [ isEditModalOpen, toggleEdit ] = useToggle(); const [ isEditModalOpen, toggleEdit ] = useToggle();
const [ isDropdownOpen, toggleDropdown ] = useToggle(); const [ isDropdownOpen, toggleDropdown ] = useToggle();
@ -29,16 +29,16 @@ export const TagsTableRow = (DeleteTagConfirmModal: FC<TagModalProps>, EditTagMo
return ( return (
<tr className="responsive-table__row"> <tr className="responsive-table__row">
<th className="responsive-table__cell" data-th="Tag"> <th className="responsive-table__cell" data-th="Tag">
<TagBullet tag={tag} colorGenerator={colorGenerator} /> {tag} <TagBullet tag={tag.tag} colorGenerator={colorGenerator} /> {tag.tag}
</th> </th>
<td className="responsive-table__cell text-lg-right" data-th="Short URLs"> <td className="responsive-table__cell text-lg-right" data-th="Short URLs">
<Link to={`/server/${serverId}/list-short-urls/1?tag=${encodeURIComponent(tag)}`}> <Link to={`/server/${serverId}/list-short-urls/1?tag=${encodeURIComponent(tag.tag)}`}>
{prettify(tagStats?.shortUrlsCount ?? 0)} {prettify(tag.shortUrls)}
</Link> </Link>
</td> </td>
<td className="responsive-table__cell text-lg-right" data-th="Visits"> <td className="responsive-table__cell text-lg-right" data-th="Visits">
<Link to={`/server/${serverId}/tag/${tag}/visits`}> <Link to={`/server/${serverId}/tag/${tag.tag}/visits`}>
{prettify(tagStats?.visitsCount ?? 0)} {prettify(tag.visits)}
</Link> </Link>
</td> </td>
<td className="responsive-table__cell text-lg-right"> <td className="responsive-table__cell text-lg-right">
@ -52,8 +52,8 @@ export const TagsTableRow = (DeleteTagConfirmModal: FC<TagModalProps>, EditTagMo
</DropdownBtnMenu> </DropdownBtnMenu>
</td> </td>
<EditTagModal tag={tag} toggle={toggleEdit} isOpen={isEditModalOpen} /> <EditTagModal tag={tag.tag} toggle={toggleEdit} isOpen={isEditModalOpen} />
<DeleteTagConfirmModal tag={tag} toggle={toggleDelete} isOpen={isDeleteModalOpen} /> <DeleteTagConfirmModal tag={tag.tag} toggle={toggleDelete} isOpen={isDeleteModalOpen} />
</tr> </tr>
); );
}; };

View file

@ -8,3 +8,9 @@ export interface TagModalProps {
isOpen: boolean; isOpen: boolean;
toggle: () => void; toggle: () => void;
} }
export interface NormalizedTag {
tag: string;
shortUrls: number;
visits: number;
}

View file

@ -27,9 +27,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ])); bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ]));
bottle.serviceFactory('TagsCards', TagsCards, 'TagCard'); bottle.serviceFactory('TagsCards', TagsCards, 'TagCard');
bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal'); bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
bottle.serviceFactory('TagsTable', TagsTable, 'ColorGenerator', 'TagsTableRow'); bottle.serviceFactory('TagsTable', TagsTable, 'TagsTableRow');
bottle.decorator('TagsTable', withRouter); bottle.decorator('TagsTable', withRouter);
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable'); bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');

View file

@ -3,7 +3,7 @@ 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 './utils'; import { determineOrderDir, OrderDir } from './helpers/ordering';
import './SortingDropdown.scss'; import './SortingDropdown.scss';
export interface SortingDropdownProps<T extends string = string> { export interface SortingDropdownProps<T extends string = string> {

View file

@ -0,0 +1,32 @@
export type OrderDir = 'ASC' | 'DESC' | undefined;
export interface Order<Fields> {
field?: Fields;
dir?: OrderDir;
}
export const determineOrderDir = <T extends string = string>(
currentField: T,
newField?: T,
currentOrderDir?: OrderDir,
): OrderDir => {
if (currentField !== newField) {
return 'ASC';
}
const newOrderMap: Record<'ASC' | 'DESC', OrderDir> = {
ASC: 'DESC',
DESC: undefined,
};
return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC';
};
export const sortList = <List>(list: List[], { field, dir }: Order<Partial<keyof List>>) => !field || !dir
? list
: list.sort((a, b) => {
const greaterThan = dir === 'ASC' ? 1 : -1;
const smallerThan = dir === 'ASC' ? -1 : 1;
return a[field] > b[field] ? greaterThan : smallerThan;
});

View file

@ -1,25 +1,6 @@
import { isEmpty, isNil, pipe, range } from 'ramda'; import { isEmpty, isNil, pipe, range } from 'ramda';
import { SyntheticEvent } from 'react'; import { SyntheticEvent } from 'react';
export type OrderDir = 'ASC' | 'DESC' | undefined;
export const determineOrderDir = <T extends string = string>(
currentField: T,
newField?: T,
currentOrderDir?: OrderDir,
): OrderDir => {
if (currentField !== newField) {
return 'ASC';
}
const newOrderMap: Record<'ASC' | 'DESC', OrderDir> = {
ASC: 'DESC',
DESC: undefined,
};
return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC';
};
export const rangeOf = <T>(size: number, mappingFn: (value: number) => T, startAt = 1): T[] => export const rangeOf = <T>(size: number, mappingFn: (value: number) => T, startAt = 1): T[] =>
range(startAt, size + 1).map(mappingFn); range(startAt, size + 1).map(mappingFn);

View file

@ -11,7 +11,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { UncontrolledTooltip } from 'reactstrap'; import { UncontrolledTooltip } from 'reactstrap';
import SimplePaginator from '../common/SimplePaginator'; import SimplePaginator from '../common/SimplePaginator';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import { determineOrderDir, OrderDir } from '../utils/utils'; import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering';
import { prettify } from '../utils/helpers/numbers'; import { prettify } from '../utils/helpers/numbers';
import { supportsBotVisits } from '../utils/helpers/features'; import { supportsBotVisits } from '../utils/helpers/features';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
@ -29,11 +29,7 @@ export interface VisitsTableProps {
} }
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot'; type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot';
type VisitsOrder = Order<OrderableFields>;
interface Order {
field?: OrderableFields;
dir?: OrderDir;
}
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const visitMatchesSearch = ({ browser, os, referer, country, city, ...rest }: NormalizedVisit, searchTerm: string) => const visitMatchesSearch = ({ browser, os, referer, country, city, ...rest }: NormalizedVisit, searchTerm: string) =>
@ -42,15 +38,8 @@ const visitMatchesSearch = ({ browser, os, referer, country, city, ...rest }: No
); );
const searchVisits = (searchTerm: string, visits: NormalizedVisit[]) => const searchVisits = (searchTerm: string, visits: NormalizedVisit[]) =>
visits.filter((visit) => visitMatchesSearch(visit, searchTerm)); visits.filter((visit) => visitMatchesSearch(visit, searchTerm));
const sortVisits = ({ field, dir }: Order, visits: NormalizedVisit[]) => !field || !dir ? visits : visits.sort( const sortVisits = (order: VisitsOrder, visits: NormalizedVisit[]) => sortList<NormalizedVisit>(visits, order as any);
(a, b) => { const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | undefined, order: VisitsOrder) => {
const greaterThan = dir === 'ASC' ? 1 : -1;
const smallerThan = dir === 'ASC' ? -1 : 1;
return (a as NormalizedOrphanVisit)[field] > (b as NormalizedOrphanVisit)[field] ? greaterThan : smallerThan;
},
);
const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | undefined, order: Order) => {
const filteredVisits = searchTerm ? searchVisits(searchTerm, allVisits) : [ ...allVisits ]; const filteredVisits = searchTerm ? searchVisits(searchTerm, allVisits) : [ ...allVisits ];
const sortedVisits = sortVisits(order, filteredVisits); const sortedVisits = sortVisits(order, filteredVisits);
const total = sortedVisits.length; const total = sortedVisits.length;
@ -72,7 +61,7 @@ const VisitsTable = ({
const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile()); const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
const [ searchTerm, setSearchTerm ] = useState<string | undefined>(undefined); const [ searchTerm, setSearchTerm ] = useState<string | undefined>(undefined);
const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined }); const [ order, setOrder ] = useState<VisitsOrder>({});
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]); const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
const isFirstLoad = useRef(true); const isFirstLoad = useRef(true);
const [ page, setPage ] = useState(1); const [ page, setPage ] = useState(1);

View file

@ -1,6 +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 { OrderDir, rangeOf } from '../../utils/utils'; import { rangeOf } from '../../utils/utils';
import { OrderDir } 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';

View file

@ -2,7 +2,6 @@ import { Mock } from 'ts-mockery';
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { match } from 'react-router'; import { match } from 'react-router';
import { Location, History } from 'history'; import { Location, History } from 'history';
import ColorGenerator from '../../src/utils/services/ColorGenerator';
import { TagsTable as createTagsTable } from '../../src/tags/TagsTable'; import { TagsTable as createTagsTable } from '../../src/tags/TagsTable';
import { SelectedServer } from '../../src/servers/data'; import { SelectedServer } from '../../src/servers/data';
import { TagsList } from '../../src/tags/reducers/tagsList'; import { TagsList } from '../../src/tags/reducers/tagsList';
@ -10,9 +9,8 @@ import { rangeOf } from '../../src/utils/utils';
import SimplePaginator from '../../src/common/SimplePaginator'; import SimplePaginator from '../../src/common/SimplePaginator';
describe('<TagsTable />', () => { describe('<TagsTable />', () => {
const colorGenerator = Mock.all<ColorGenerator>();
const TagsTableRow = () => null; const TagsTableRow = () => null;
const TagsTable = createTagsTable(colorGenerator, TagsTableRow); const TagsTable = createTagsTable(TagsTableRow);
const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`); const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`);
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (filteredTags: string[] = [], search = '') => { const createWrapper = (filteredTags: string[] = [], search = '') => {
@ -86,7 +84,7 @@ describe('<TagsTable />', () => {
expect(tagRows).toHaveLength(expectedRows); expect(tagRows).toHaveLength(expectedRows);
tagRows.forEach((row, index) => { tagRows.forEach((row, index) => {
expect(row.prop('tag')).toEqual(`tag_${index + offset + 1}`); expect(row.prop('tag')).toEqual(expect.objectContaining({ tag: `tag_${index + offset + 1}` }));
}); });
}); });

View file

@ -5,21 +5,18 @@ import { DropdownItem } from 'reactstrap';
import { TagsTableRow as createTagsTableRow } from '../../src/tags/TagsTableRow'; import { TagsTableRow as createTagsTableRow } from '../../src/tags/TagsTableRow';
import { ReachableServer } from '../../src/servers/data'; import { ReachableServer } from '../../src/servers/data';
import ColorGenerator from '../../src/utils/services/ColorGenerator'; import ColorGenerator from '../../src/utils/services/ColorGenerator';
import { TagStats } from '../../src/tags/data';
import { DropdownBtnMenu } from '../../src/utils/DropdownBtnMenu'; import { DropdownBtnMenu } from '../../src/utils/DropdownBtnMenu';
describe('<TagsTableRow />', () => { describe('<TagsTableRow />', () => {
const DeleteTagConfirmModal = () => null; const DeleteTagConfirmModal = () => null;
const EditTagModal = () => null; const EditTagModal = () => null;
const TagsTableRow = createTagsTableRow(DeleteTagConfirmModal, EditTagModal); const TagsTableRow = createTagsTableRow(DeleteTagConfirmModal, EditTagModal, Mock.all<ColorGenerator>());
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (tagStats?: TagStats) => { const createWrapper = (tagStats?: { visits?: number; shortUrls?: number }) => {
wrapper = shallow( wrapper = shallow(
<TagsTableRow <TagsTableRow
tag="foo&bar" tag={{ tag: 'foo&bar', visits: tagStats?.visits ?? 0, shortUrls: tagStats?.shortUrls ?? 0 }}
tagStats={tagStats}
selectedServer={Mock.of<ReachableServer>({ id: 'abc123' })} selectedServer={Mock.of<ReachableServer>({ id: 'abc123' })}
colorGenerator={Mock.all<ColorGenerator>()}
/>, />,
); );
@ -30,7 +27,7 @@ describe('<TagsTableRow />', () => {
it.each([ it.each([
[ undefined, '0', '0' ], [ undefined, '0', '0' ],
[ Mock.of<TagStats>({ shortUrlsCount: 10, visitsCount: 3480 }), '10', '3,480' ], [{ shortUrls: 10, visits: 3480 }, '10', '3,480' ],
])('shows expected tag stats', (stats, expectedShortUrls, expectedVisits) => { ])('shows expected tag stats', (stats, expectedShortUrls, expectedVisits) => {
const wrapper = createWrapper(stats); const wrapper = createWrapper(stats);
const links = wrapper.find(Link); const links = wrapper.find(Link);

View file

@ -4,7 +4,7 @@ import { identity, values } from 'ramda';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSortAmountDown as caretDownIcon } from '@fortawesome/free-solid-svg-icons'; import { faSortAmountDown as caretDownIcon } from '@fortawesome/free-solid-svg-icons';
import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown'; import SortingDropdown, { SortingDropdownProps } from '../../src/utils/SortingDropdown';
import { OrderDir } from '../../src/utils/utils'; import { OrderDir } from '../../src/utils/helpers/ordering';
describe('<SortingDropdown />', () => { describe('<SortingDropdown />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;

View file

@ -1,4 +1,5 @@
import { capitalize, determineOrderDir, nonEmptyValueOrNull, rangeOf } from '../../src/utils/utils'; import { capitalize, nonEmptyValueOrNull, rangeOf } from '../../src/utils/utils';
import { determineOrderDir } from '../../src/utils/helpers/ordering';
describe('utils', () => { describe('utils', () => {
describe('determineOrderDir', () => { describe('determineOrderDir', () => {

View file

@ -2,7 +2,8 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { range } from 'ramda'; import { range } from 'ramda';
import SortingDropdown from '../../../src/utils/SortingDropdown'; import SortingDropdown from '../../../src/utils/SortingDropdown';
import PaginationDropdown from '../../../src/utils/PaginationDropdown'; import PaginationDropdown from '../../../src/utils/PaginationDropdown';
import { OrderDir, rangeOf } from '../../../src/utils/utils'; import { rangeOf } from '../../../src/utils/utils';
import { OrderDir } from '../../../src/utils/helpers/ordering';
import { Stats } from '../../../src/visits/types'; import { Stats } from '../../../src/visits/types';
import { SortableBarChartCard } from '../../../src/visits/charts/SortableBarChartCard'; import { SortableBarChartCard } from '../../../src/visits/charts/SortableBarChartCard';
import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart'; import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart';