diff --git a/src/api/types/index.ts b/src/api/types/index.ts index b49e4338..b16111b9 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -25,12 +25,12 @@ interface ShlinkTagsStats { export interface ShlinkTags { tags: string[]; - stats?: ShlinkTagsStats[]; // Is only optional in Shlink older than v2.2 + stats: ShlinkTagsStats[]; } export interface ShlinkTagsResponse { data: string[]; - stats?: ShlinkTagsStats[]; // Is only optional in Shlink older than v2.2 + stats: ShlinkTagsStats[]; } export interface ShlinkPaginator { @@ -90,7 +90,6 @@ export interface ProblemDetailsError { detail: string; title: string; status: number; - [extraProps: string]: any; } diff --git a/src/settings/RealTimeUpdates.tsx b/src/settings/RealTimeUpdates.tsx index 8284eeaf..243a7d0c 100644 --- a/src/settings/RealTimeUpdates.tsx +++ b/src/settings/RealTimeUpdates.tsx @@ -18,7 +18,7 @@ const RealTimeUpdates = ( - Enable or disable real-time updates, when using Shlink v2.2.0 or newer. + Enable or disable real-time updates. Real-time updates are currently being {realTimeUpdates.enabled ? 'processed' : 'ignored'}. diff --git a/src/tags/TagCard.tsx b/src/tags/TagCard.tsx index e3cd220e..df0fd26a 100644 --- a/src/tags/TagCard.tsx +++ b/src/tags/TagCard.tsx @@ -8,12 +8,11 @@ import { useToggle } from '../utils/helpers/hooks'; import ColorGenerator from '../utils/services/ColorGenerator'; import { isServerWithId, SelectedServer } from '../servers/data'; import TagBullet from './helpers/TagBullet'; -import { TagModalProps, TagStats } from './data'; +import { NormalizedTag, TagModalProps } from './data'; import './TagCard.scss'; export interface TagCardProps { - tag: string; - tagStats?: TagStats; + tag: NormalizedTag; selectedServer: SelectedServer; displayed: boolean; toggle: () => void; @@ -25,7 +24,7 @@ const TagCard = ( DeleteTagConfirmModal: FC, EditTagModal: FC, colorGenerator: ColorGenerator, -) => ({ tag, tagStats, selectedServer, displayed, toggle }: TagCardProps) => { +) => ({ tag, selectedServer, displayed, toggle }: TagCardProps) => { const [ isDeleteModalOpen, toggleDelete ] = useToggle(); const [ isEditModalOpen, toggleEdit ] = useToggle(); const [ hasTitle,, displayTitle ] = useToggle(); @@ -49,39 +48,37 @@ const TagCard = (
{ titleRef.current = el ?? undefined; }} > - - {tag} + + {tag.tag}
- {tagStats && ( - - - - Short URLs - {prettify(tagStats.shortUrlsCount)} - - - Visits - {prettify(tagStats.visitsCount)} - - - - )} + + + + Short URLs + {prettify(tag.shortUrls)} + + + Visits + {prettify(tag.visits)} + + + - - + + ); }; diff --git a/src/tags/TagsCards.tsx b/src/tags/TagsCards.tsx index 3c1408cb..9e6d0b54 100644 --- a/src/tags/TagsCards.tsx +++ b/src/tags/TagsCards.tsx @@ -7,10 +7,10 @@ import { TagsListChildrenProps } from './data/TagsListChildrenProps'; const { ceil } = Math; const TAGS_GROUPS_AMOUNT = 4; -export const TagsCards = (TagCard: FC): FC => ({ tagsList, selectedServer }) => { +export const TagsCards = (TagCard: FC): FC => ({ sortedTags, selectedServer }) => { const [ displayedTag, setDisplayedTag ] = useState(); - const tagsCount = tagsList.filteredTags.length; - const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags); + const tagsCount = sortedTags.length; + const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), sortedTags); return ( @@ -18,12 +18,11 @@ export const TagsCards = (TagCard: FC): FC
{group.map((tag) => ( setDisplayedTag(displayedTag !== tag ? tag : undefined)} + displayed={displayedTag === tag.tag} + toggle={() => setDisplayedTag(displayedTag !== tag.tag ? tag.tag : undefined)} /> ))}
diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index 4fdbf442..252ddfe2 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -1,5 +1,8 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { Row } from 'reactstrap'; +import { pipe } from 'ramda'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons'; import Message from '../utils/Message'; import SearchField from '../utils/SearchField'; import { SelectedServer } from '../servers/data'; @@ -8,9 +11,12 @@ import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; import { Topics } from '../mercure/helpers/Topics'; import { Settings, TagsMode } from '../settings/reducers/settings'; +import { determineOrderDir, sortList } from '../utils/helpers/ordering'; import { TagsList as TagsListState } from './reducers/tagsList'; -import { TagsListChildrenProps } from './data/TagsListChildrenProps'; +import { OrderableFields, TagsListChildrenProps, TagsOrder } from './data/TagsListChildrenProps'; import { TagsModeDropdown } from './TagsModeDropdown'; +import { NormalizedTag } from './data'; +import { TagsTableProps } from './TagsTable'; export interface TagsListProps { filterTags: (searchTerm: string) => void; @@ -20,10 +26,22 @@ export interface TagsListProps { settings: Settings; } -const TagsList = (TagsCards: FC, TagsTable: FC) => boundToMercureHub(( +const TagsList = (TagsCards: FC, TagsTable: FC) => boundToMercureHub(( { filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps, ) => { const [ mode, setMode ] = useState(settings.ui?.tagsMode ?? 'cards'); + const [ order, setOrder ] = useState({}); + const sortedTags = useMemo( + pipe( + () => tagsList.filteredTags.map((tag): NormalizedTag => ({ + tag, + shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 0, + visits: tagsList.stats[tag]?.visitsCount ?? 0, + })), + (normalizedTags) => sortList(normalizedTags, order), + ), + [ tagsList.filteredTags, order ], + ); useEffect(() => { forceListTags(); @@ -33,6 +51,10 @@ const TagsList = (TagsCards: FC, TagsTable: FC; } + const orderByColumn = (field: OrderableFields) => + () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) }); + const renderOrderIcon = (field: OrderableFields) => order.dir && order.field === field && + ; const renderContent = () => { if (tagsList.error) { return ( @@ -47,8 +69,15 @@ const TagsList = (TagsCards: FC, TagsTable: FC - : ; + ? + : ( + + ); }; return ( diff --git a/src/tags/TagsTable.tsx b/src/tags/TagsTable.tsx index e81d59c9..5ed1d861 100644 --- a/src/tags/TagsTable.tsx +++ b/src/tags/TagsTable.tsx @@ -1,54 +1,35 @@ -import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { pipe, splitEvery } from 'ramda'; +import { FC, ReactNode, useEffect, useRef } from 'react'; +import { splitEvery } from 'ramda'; 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 SimplePaginator from '../common/SimplePaginator'; import { useQueryState } from '../utils/helpers/hooks'; import { parseQuery } from '../utils/helpers/query'; -import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering'; -import { TagsListChildrenProps } from './data/TagsListChildrenProps'; +import { OrderableFields, TagsListChildrenProps } from './data/TagsListChildrenProps'; import { TagsTableRowProps } from './TagsTableRow'; -import { NormalizedTag } from './data'; import './TagsTable.scss'; +export interface TagsTableProps extends TagsListChildrenProps { + orderByColumn: (field: OrderableFields) => () => void; + renderOrderIcon: (field: OrderableFields) => ReactNode; +} + const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings -type OrderableFields = 'tag' | 'shortUrls' | 'visits'; -type TagsOrder = Order; - export const TagsTable = (TagsTableRow: FC) => ( - { tagsList, selectedServer, location }: TagsListChildrenProps & RouteChildrenProps, + { sortedTags, selectedServer, location, orderByColumn, renderOrderIcon }: TagsTableProps & RouteChildrenProps, ) => { const isFirstLoad = useRef(true); const { page: pageFromQuery = 1 } = parseQuery<{ page?: number | string }>(location.search); const [ page, setPage ] = useQueryState('page', Number(pageFromQuery)); - const [ order, setOrder ] = useState({}); - const sortedTags = useMemo( - pipe( - () => tagsList.filteredTags.map((tag): NormalizedTag => ({ - tag, - shortUrls: tagsList.stats[tag]?.shortUrlsCount ?? 0, - visits: tagsList.stats[tag]?.visitsCount ?? 0, - })), - (normalizedTags) => sortList(normalizedTags, order), - ), - [ tagsList.filteredTags, order ], - ); const pages = splitEvery(TAGS_PER_PAGE, sortedTags); const showPaginator = pages.length > 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 && - ; - useEffect(() => { !isFirstLoad.current && setPage(1); isFirstLoad.current = false; - }, [ tagsList.filteredTags ]); + }, [ sortedTags ]); useEffect(() => { scrollTo(0, 0); }, [ page ]); diff --git a/src/tags/data/TagsListChildrenProps.ts b/src/tags/data/TagsListChildrenProps.ts index 5777c290..17152d4d 100644 --- a/src/tags/data/TagsListChildrenProps.ts +++ b/src/tags/data/TagsListChildrenProps.ts @@ -1,7 +1,12 @@ -import { TagsList as TagsListState } from '../reducers/tagsList'; import { SelectedServer } from '../../servers/data'; +import { Order } from '../../utils/helpers/ordering'; +import { NormalizedTag } from './index'; + +export type OrderableFields = 'tag' | 'shortUrls' | 'visits'; + +export type TagsOrder = Order; export interface TagsListChildrenProps { - tagsList: TagsListState; + sortedTags: NormalizedTag[]; selectedServer: SelectedServer; }