mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Added pagination to tags table
This commit is contained in:
parent
1da7119c5c
commit
12f6a132bd
7 changed files with 77 additions and 33 deletions
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
|
@ -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 ];
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue