mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Added logic to order tags list
This commit is contained in:
parent
5241925acc
commit
04571ea634
5 changed files with 50 additions and 58 deletions
|
@ -1,3 +0,0 @@
|
||||||
.short-urls-list__header-icon {
|
|
||||||
margin-left: .4rem;
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
/
|
/
|
||||||
<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"> </th>
|
<th className="short-urls-table__header-cell"> </th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -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(
|
||||||
() => tagsList.filteredTags.map((tag): NormalizedTag => ({
|
pipe(
|
||||||
tag,
|
() => tagsList.filteredTags.map((tag): NormalizedTag => ({
|
||||||
shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 0,
|
tag,
|
||||||
visits: tagsList.stats[tag]?.visitsCount ?? 0,
|
shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 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>
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue