Added pagination to tags table

This commit is contained in:
Alejandro Celaya 2021-09-25 09:34:38 +02:00
parent 1da7119c5c
commit 12f6a132bd
7 changed files with 77 additions and 33 deletions

View file

@ -5,6 +5,7 @@
@import './common/react-tag-autocomplete.scss'; @import './common/react-tag-autocomplete.scss';
@import './theme/theme'; @import './theme/theme';
@import './utils/table/ResponsiveTable'; @import './utils/table/ResponsiveTable';
@import './utils/StickyCardPaginator';
* { * {
outline: none !important; outline: none !important;

View file

@ -2,7 +2,6 @@ import { Link } from 'react-router-dom';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap'; import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import { pageIsEllipsis, keyForPage, progressivePagination, prettifyPageNumber } from '../utils/helpers/pagination'; import { pageIsEllipsis, keyForPage, progressivePagination, prettifyPageNumber } from '../utils/helpers/pagination';
import { ShlinkPaginator } from '../api/types'; import { ShlinkPaginator } from '../api/types';
import './Paginator.scss';
interface PaginatorProps { interface PaginatorProps {
paginator?: ShlinkPaginator; paginator?: ShlinkPaginator;
@ -33,7 +32,7 @@ const Paginator = ({ paginator, serverId }: PaginatorProps) => {
)); ));
return ( return (
<Pagination className="short-urls-paginator" listClassName="flex-wrap justify-content-center mb-0"> <Pagination className="sticky-card-paginator" listClassName="flex-wrap justify-content-center mb-0">
<PaginationItem disabled={currentPage === 1}> <PaginationItem disabled={currentPage === 1}>
<PaginationLink <PaginationLink
previous previous

View file

@ -13,10 +13,10 @@ interface TagsModeDropdownProps {
export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange, renderTitle }) => ( export const TagsModeDropdown: FC<TagsModeDropdownProps> = ({ mode, onChange, renderTitle }) => (
<DropdownBtn text={renderTitle?.(mode) ?? `Display mode: ${mode}`}> <DropdownBtn text={renderTitle?.(mode) ?? `Display mode: ${mode}`}>
<DropdownItem outline active={mode === 'cards'} onClick={() => onChange('cards')}> <DropdownItem active={mode === 'cards'} onClick={() => onChange('cards')}>
<FontAwesomeIcon icon={cardsIcon} fixedWidth className="mr-1" /> Cards <FontAwesomeIcon icon={cardsIcon} fixedWidth className="mr-1" /> Cards
</DropdownItem> </DropdownItem>
<DropdownItem outline active={mode === 'list'} onClick={() => onChange('list')}> <DropdownItem active={mode === 'list'} onClick={() => onChange('list')}>
<FontAwesomeIcon icon={listIcon} fixedWidth className="mr-1" /> List <FontAwesomeIcon icon={listIcon} fixedWidth className="mr-1" /> List
</DropdownItem> </DropdownItem>
</DropdownBtn> </DropdownBtn>

View file

@ -1,34 +1,60 @@
import { FC } from 'react'; import { FC, useEffect } from 'react';
import { splitEvery } from 'ramda';
import { RouteChildrenProps } from 'react-router';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import ColorGenerator from '../utils/services/ColorGenerator'; import ColorGenerator from '../utils/services/ColorGenerator';
import SimplePaginator from '../common/SimplePaginator';
import { useQueryState } from '../utils/helpers/hooks';
import { parseQuery } from '../utils/helpers/query';
import { TagsListChildrenProps } from './data/TagsListChildrenProps'; import { TagsListChildrenProps } from './data/TagsListChildrenProps';
import { TagsTableRowProps } from './TagsTableRow'; import { TagsTableRowProps } from './TagsTableRow';
const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings
export const TagsTable = (colorGenerator: ColorGenerator, TagsTableRow: FC<TagsTableRowProps>) => ( export const TagsTable = (colorGenerator: ColorGenerator, TagsTableRow: FC<TagsTableRowProps>) => (
{ tagsList, selectedServer }: TagsListChildrenProps, { tagsList, selectedServer, location }: TagsListChildrenProps & RouteChildrenProps,
) => ( ) => {
<SimpleCard> const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(location.search);
<table className="table table-hover mb-0"> const [ page, setPage ] = useQueryState<number>('page', Number(pageFromQuery));
<thead className="responsive-table__header"> const sortedTags = tagsList.filteredTags;
<tr> const pages = splitEvery(TAGS_PER_PAGE, sortedTags);
<th>Tag</th> const showPaginator = pages.length > 1;
<th className="text-lg-right">Short URLs</th> const currentPage = pages[page - 1] ?? [];
<th className="text-lg-right">Visits</th>
<th /> useEffect(() => {
</tr> setPage(1);
</thead> }, [ tagsList.filteredTags ]);
<tbody>
{tagsList.filteredTags.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>} return (
{tagsList.filteredTags.map((tag) => ( <SimpleCard key={page} bodyClassName={showPaginator ? 'pb-1' : ''}>
<TagsTableRow <table className="table table-hover mb-0">
key={tag} <thead className="responsive-table__header">
tag={tag} <tr>
tagStats={tagsList.stats[tag]} <th>Tag</th>
selectedServer={selectedServer} <th className="text-lg-right">Short URLs</th>
colorGenerator={colorGenerator} <th className="text-lg-right">Visits</th>
/> <th />
))} </tr>
</tbody> </thead>
</table> <tbody>
</SimpleCard> {currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
); {currentPage.map((tag) => (
<TagsTableRow
key={tag}
tag={tag}
tagStats={tagsList.stats[tag]}
selectedServer={selectedServer}
colorGenerator={colorGenerator}
/>
))}
</tbody>
</table>
{showPaginator && (
<div className="sticky-card-paginator">
<SimplePaginator pagesCount={pages.length} currentPage={page} setCurrentPage={setPage} />
</div>
)}
</SimpleCard>
);
};

View file

@ -1,4 +1,5 @@
import Bottle, { IContainer } from 'bottlejs'; import Bottle, { IContainer } from 'bottlejs';
import { withRouter } from 'react-router-dom';
import TagsSelector from '../helpers/TagsSelector'; import TagsSelector from '../helpers/TagsSelector';
import TagCard from '../TagCard'; import TagCard from '../TagCard';
import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal'; import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal';
@ -34,7 +35,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('TagsCards', TagsCards, 'TagCard'); bottle.serviceFactory('TagsCards', TagsCards, 'TagCard');
bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal'); bottle.serviceFactory('TagsTableRow', TagsTableRow, 'DeleteTagConfirmModal', 'EditTagModal');
bottle.serviceFactory('TagsTable', TagsTable, 'ColorGenerator', 'TagsTableRow'); bottle.serviceFactory('TagsTable', TagsTable, 'ColorGenerator', 'TagsTableRow');
bottle.decorator('TagsTable', withRouter);
bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable'); bottle.serviceFactory('TagsList', TagsList, 'TagsCards', 'TagsTable');
bottle.decorator('TagsList', connect( bottle.decorator('TagsList', connect(

View file

@ -1,4 +1,4 @@
.short-urls-paginator { .sticky-card-paginator {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
background-color: var(--primary-color-alfa); background-color: var(--primary-color-alfa);

View file

@ -1,5 +1,6 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useSwipeable as useReactSwipeable } from 'react-swipeable'; import { useSwipeable as useReactSwipeable } from 'react-swipeable';
import { parseQuery, stringifyQuery } from './query';
const DEFAULT_DELAY = 2000; const DEFAULT_DELAY = 2000;
@ -51,3 +52,17 @@ export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) =
onSwipedRight: swipeMenuIfNoModalExists(showSidebar), onSwipedRight: swipeMenuIfNoModalExists(showSidebar),
}); });
}; };
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
const [ value, setValue ] = useState(initialState);
const setValueWithLocation = (value: T) => {
const { location, history } = window;
const query = parseQuery<any>(location.search);
query[paramName] = value;
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
setValue(value);
};
return [ value, setValueWithLocation ];
};