Added logic to order tags list

This commit is contained in:
Alejandro Celaya 2021-11-01 13:41:16 +01:00
parent 5241925acc
commit 04571ea634
5 changed files with 50 additions and 58 deletions

View file

@ -1,3 +0,0 @@
.short-urls-list__header-icon {
margin-left: .4rem;
}

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/helpers/ordering'; import { determineOrderDir, Order, 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';
@ -14,7 +14,6 @@ import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable'; import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator'; import Paginator from './Paginator';
import './ShortUrlsList.scss';
interface RouteParams { interface RouteParams {
page: string; page: string;
@ -29,6 +28,8 @@ export interface ShortUrlsListProps extends RouteComponentProps<RouteParams> {
resetShortUrlParams: () => void; resetShortUrlParams: () => void;
} }
type ShortUrlsOrder = Order<OrderableFields>;
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub(({ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub(({
listShortUrls, listShortUrls,
resetShortUrlParams, resetShortUrlParams,
@ -39,34 +40,20 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
selectedServer, selectedServer,
}: ShortUrlsListProps) => { }: ShortUrlsListProps) => {
const { orderBy } = shortUrlsListParams; const { orderBy } = shortUrlsListParams;
const [ order, setOrder ] = useState<{ orderField?: OrderableFields; orderDir?: OrderDir }>({ const [ order, setOrder ] = useState<ShortUrlsOrder>({
orderField: orderBy && (head(keys(orderBy)) as OrderableFields), field: orderBy && (head(keys(orderBy)) as OrderableFields),
orderDir: orderBy && head(values(orderBy)), dir: orderBy && head(values(orderBy)),
}); });
const { pagination } = shortUrlsList?.shortUrls ?? {}; const { pagination } = shortUrlsList?.shortUrls ?? {};
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams }); const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
const handleOrderBy = (orderField?: OrderableFields, orderDir?: OrderDir) => { const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => {
setOrder({ orderField, orderDir }); setOrder({ field, dir });
refreshList({ orderBy: orderField ? { [orderField]: orderDir } : undefined }); refreshList({ orderBy: field ? { [field]: dir } : undefined });
}; };
const orderByColumn = (field: OrderableFields) => () => const orderByColumn = (field: OrderableFields) => () =>
handleOrderBy(field, determineOrderDir(field, order.orderField, order.orderDir)); handleOrderBy(field, determineOrderDir(field, order.field, order.dir));
const renderOrderIcon = (field: OrderableFields) => { const renderOrderIcon = (field: OrderableFields) => order.dir && order.field === field &&
if (order.orderField !== field) { <FontAwesomeIcon icon={order.dir === 'ASC' ? caretUpIcon : caretDownIcon} className="ml-1" />;
return null;
}
if (!order.orderDir) {
return null;
}
return (
<FontAwesomeIcon
icon={order.orderDir === 'ASC' ? caretUpIcon : caretDownIcon}
className="short-urls-list__header-icon"
/>
);
};
useEffect(() => { useEffect(() => {
const { tag } = parseQuery<{ tag?: string }>(location.search); const { tag } = parseQuery<{ tag?: string }>(location.search);
@ -82,8 +69,8 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercur
<div className="d-block d-lg-none mb-3"> <div className="d-block d-lg-none mb-3">
<SortingDropdown <SortingDropdown
items={SORTABLE_FIELDS} items={SORTABLE_FIELDS}
orderField={order.orderField} orderField={order.field}
orderDir={order.orderDir} orderDir={order.dir}
onChange={handleOrderBy} onChange={handleOrderBy}
/> />
</div> </div>

View file

@ -63,34 +63,29 @@ export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
<thead className="responsive-table__header short-urls-table__header"> <thead className="responsive-table__header short-urls-table__header">
<tr> <tr>
<th className={orderableColumnsClasses} onClick={orderByColumn?.('dateCreated')}> <th className={orderableColumnsClasses} onClick={orderByColumn?.('dateCreated')}>
Created at Created at {renderOrderIcon?.('dateCreated')}
{renderOrderIcon?.('dateCreated')}
</th> </th>
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}> <th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
Short URL Short URL {renderOrderIcon?.('shortCode')}
{renderOrderIcon?.('shortCode')}
</th> </th>
{!supportsTitle && ( {!supportsTitle && (
<th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}> <th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}>
Long URL Long URL {renderOrderIcon?.('longUrl')}
{renderOrderIcon?.('longUrl')}
</th> </th>
) || ( ) || (
<th className="short-urls-table__header-cell"> <th className="short-urls-table__header-cell">
<span className={actionableFieldClasses} onClick={orderByColumn?.('title')}> <span className={actionableFieldClasses} onClick={orderByColumn?.('title')}>
Title Title {renderOrderIcon?.('title')}
{renderOrderIcon?.('title')}
</span> </span>
&nbsp;&nbsp;/&nbsp;&nbsp; &nbsp;&nbsp;/&nbsp;&nbsp;
<span className={actionableFieldClasses} onClick={orderByColumn?.('longUrl')}> <span className={actionableFieldClasses} onClick={orderByColumn?.('longUrl')}>
<span className="indivisible">Long URL</span> <span className="indivisible">Long URL</span> {renderOrderIcon?.('longUrl')}
{renderOrderIcon?.('longUrl')}
</span> </span>
</th> </th>
)} )}
<th className="short-urls-table__header-cell">Tags</th> <th className="short-urls-table__header-cell">Tags</th>
<th className={orderableColumnsClasses} onClick={orderByColumn?.('visits')}> <th className={orderableColumnsClasses} onClick={orderByColumn?.('visits')}>
<span className="indivisible">Visits{renderOrderIcon?.('visits')}</span> <span className="indivisible">Visits {renderOrderIcon?.('visits')}</span>
</th> </th>
<th className="short-urls-table__header-cell">&nbsp;</th> <th className="short-urls-table__header-cell">&nbsp;</th>
</tr> </tr>

View file

@ -1,11 +1,13 @@
import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { splitEvery } from 'ramda'; import { pipe, splitEvery } from 'ramda';
import { RouteChildrenProps } from 'react-router'; import { RouteChildrenProps } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
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 { determineOrderDir, 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 { NormalizedTag } from './data';
@ -22,20 +24,27 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
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 [ order ] = useState<TagsOrder>({}); const [ order, setOrder ] = useState<TagsOrder>({});
const normalizedTags = useMemo( const sortedTags = useMemo(
pipe(
() => tagsList.filteredTags.map((tag): NormalizedTag => ({ () => tagsList.filteredTags.map((tag): NormalizedTag => ({
tag, tag,
shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 0, shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 0,
visits: tagsList.stats[tag]?.visitsCount ?? 0, visits: tagsList.stats[tag]?.visitsCount ?? 0,
})), })),
[ tagsList.filteredTags ], (normalizedTags) => sortList<NormalizedTag>(normalizedTags, order),
),
[ tagsList.filteredTags, order ],
); );
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] ?? [];
const orderByColumn = (field: OrderableFields) =>
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
const renderOrderIcon = (field: OrderableFields) => order.dir && order.field === field &&
<FontAwesomeIcon icon={order.dir === 'ASC' ? caretUpIcon : caretDownIcon} className="ml-1" />;
useEffect(() => { useEffect(() => {
!isFirstLoad.current && setPage(1); !isFirstLoad.current && setPage(1);
isFirstLoad.current = false; isFirstLoad.current = false;
@ -49,9 +58,13 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
<table className="table table-hover mb-0"> <table className="table table-hover mb-0">
<thead className="responsive-table__header"> <thead className="responsive-table__header">
<tr> <tr>
<th className="tags-table__header-cell">Tag</th> <th className="tags-table__header-cell" onClick={orderByColumn('tag')}>Tag {renderOrderIcon('tag')}</th>
<th className="tags-table__header-cell text-lg-right">Short URLs</th> <th className="tags-table__header-cell text-lg-right" onClick={orderByColumn('shortUrls')}>
<th className="tags-table__header-cell text-lg-right">Visits</th> Short URLs {renderOrderIcon('shortUrls')}
</th>
<th className="tags-table__header-cell text-lg-right" onClick={orderByColumn('visits')}>
Visits {renderOrderIcon('visits')}
</th>
<th className="tags-table__header-cell" /> <th className="tags-table__header-cell" />
</tr> </tr>
<tr><th colSpan={4} className="p-0 border-top-0" /></tr> <tr><th colSpan={4} className="p-0 border-top-0" /></tr>

View file

@ -79,13 +79,13 @@ describe('<ShortUrlsList />', () => {
const renderIcon = (field: OrderableFields) => const renderIcon = (field: OrderableFields) =>
(wrapper.find(ShortUrlsTable).prop('renderOrderIcon') as (field: OrderableFields) => ReactElement | null)(field); (wrapper.find(ShortUrlsTable).prop('renderOrderIcon') as (field: OrderableFields) => ReactElement | null)(field);
expect(renderIcon('visits')).toEqual(null); expect(renderIcon('visits')).toEqual(undefined);
wrapper.find(SortingDropdown).simulate('change', 'visits'); wrapper.find(SortingDropdown).simulate('change', 'visits');
expect(renderIcon('visits')).toEqual(null); expect(renderIcon('visits')).toEqual(undefined);
wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC'); wrapper.find(SortingDropdown).simulate('change', 'visits', 'ASC');
expect(renderIcon('visits')).not.toEqual(null); expect(renderIcon('visits')).not.toEqual(undefined);
}); });
it('handles order by through table', () => { it('handles order by through table', () => {