From 90837546abfa190356b915980172a73aba9b86bc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Dec 2022 10:12:34 +0100 Subject: [PATCH 01/10] Exported some specific component types and improved spacing in short URLs list --- src/servers/Overview.tsx | 4 ++-- src/short-urls/Paginator.tsx | 4 ++-- src/short-urls/ShortUrlsFilteringBar.tsx | 4 +++- src/short-urls/ShortUrlsList.tsx | 12 ++++++------ src/short-urls/ShortUrlsTable.scss | 4 ++++ src/short-urls/ShortUrlsTable.tsx | 12 +++++++----- src/short-urls/helpers/ShortUrlsRow.tsx | 4 +++- test/short-urls/Paginator.test.tsx | 6 ++++-- test/short-urls/ShortUrlsList.test.tsx | 5 ++--- 9 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx index 8903709d..d751a534 100644 --- a/src/servers/Overview.tsx +++ b/src/servers/Overview.tsx @@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { ITEMS_IN_OVERVIEW_PAGE, ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList'; import { prettify } from '../utils/helpers/numbers'; import { TagsList } from '../tags/reducers/tagsList'; -import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable'; +import { ShortUrlsTableType } from '../short-urls/ShortUrlsTable'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { CreateShortUrlProps } from '../short-urls/CreateShortUrl'; import { VisitsOverview } from '../visits/reducers/visitsOverview'; @@ -25,7 +25,7 @@ interface OverviewConnectProps { } export const Overview = ( - ShortUrlsTable: FC, + ShortUrlsTable: ShortUrlsTableType, CreateShortUrl: FC, ) => boundToMercureHub(({ shortUrlsList, diff --git a/src/short-urls/Paginator.tsx b/src/short-urls/Paginator.tsx index 45c2fd10..5e7d488e 100644 --- a/src/short-urls/Paginator.tsx +++ b/src/short-urls/Paginator.tsx @@ -21,7 +21,7 @@ export const Paginator = ({ paginator, serverId, currentQueryString = '' }: Pagi `/server/${serverId}/list-short-urls/${pageNumber}${currentQueryString}`; if (pagesCount <= 1) { - return null; + return
; // Return some space } const renderPages = () => @@ -38,7 +38,7 @@ export const Paginator = ({ paginator, serverId, currentQueryString = '' }: Pagi )); return ( - + diff --git a/src/short-urls/ShortUrlsFilteringBar.tsx b/src/short-urls/ShortUrlsFilteringBar.tsx index 1a5ec29d..b49280f7 100644 --- a/src/short-urls/ShortUrlsFilteringBar.tsx +++ b/src/short-urls/ShortUrlsFilteringBar.tsx @@ -18,7 +18,7 @@ import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn'; import { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import './ShortUrlsFilteringBar.scss'; -export interface ShortUrlsFilteringProps { +interface ShortUrlsFilteringProps { selectedServer: SelectedServer; order: ShortUrlsOrder; handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void; @@ -90,3 +90,5 @@ export const ShortUrlsFilteringBar = (
); }; + +export type ShortUrlsFilteringBarType = ReturnType; diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index 6b3f7a82..c41a4f7d 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -1,5 +1,5 @@ import { pipe } from 'ramda'; -import { FC, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Card } from 'reactstrap'; import { useLocation, useParams } from 'react-router-dom'; import { determineOrderDir, OrderDir } from '../utils/helpers/ordering'; @@ -10,11 +10,11 @@ import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { ShlinkShortUrlsListParams } from '../api/types'; import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; -import { ShortUrlsTableProps } from './ShortUrlsTable'; +import { ShortUrlsTableType } from './ShortUrlsTable'; import { Paginator } from './Paginator'; import { useShortUrlsQuery } from './helpers/hooks'; import { ShortUrlsOrderableFields } from './data'; -import { ShortUrlsFilteringProps } from './ShortUrlsFilteringBar'; +import { ShortUrlsFilteringBarType } from './ShortUrlsFilteringBar'; interface ShortUrlsListProps { selectedServer: SelectedServer; @@ -24,8 +24,8 @@ interface ShortUrlsListProps { } export const ShortUrlsList = ( - ShortUrlsTable: FC, - ShortUrlsFilteringBar: FC, + ShortUrlsTable: ShortUrlsTableType, + ShortUrlsFilteringBar: ShortUrlsFilteringBarType, ) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => { const serverId = getServerId(selectedServer); const { page } = useParams(); @@ -70,7 +70,7 @@ export const ShortUrlsList = ( handleOrderBy={handleOrderBy} className="mb-3" /> - + () => void; renderOrderIcon?: (column: ShortUrlsOrderableFields) => ReactNode; shortUrlsList: ShortUrlsListState; @@ -16,7 +16,7 @@ export interface ShortUrlsTableProps { className?: string; } -export const ShortUrlsTable = (ShortUrlsRow: FC) => ({ +export const ShortUrlsTable = (ShortUrlsRow: ShortUrlsRowType) => ({ orderByColumn, renderOrderIcon, shortUrlsList, @@ -27,7 +27,7 @@ export const ShortUrlsTable = (ShortUrlsRow: FC) => ({ const { error, loading, shortUrls } = shortUrlsList; const actionableFieldClasses = classNames({ 'short-urls-table__header-cell--with-action': !!orderByColumn }); const orderableColumnsClasses = classNames('short-urls-table__header-cell', actionableFieldClasses); - const tableClasses = classNames('table table-hover responsive-table', className); + const tableClasses = classNames('table table-hover responsive-table short-urls-table', className); const renderShortUrls = () => { if (error) { @@ -90,3 +90,5 @@ export const ShortUrlsTable = (ShortUrlsRow: FC) => ({ ); }; + +export type ShortUrlsTableType = ReturnType; diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx index 1b7fafb1..159c2b24 100644 --- a/src/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -12,7 +12,7 @@ import { ShortUrlVisitsCount } from './ShortUrlVisitsCount'; import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu'; import './ShortUrlsRow.scss'; -export interface ShortUrlsRowProps { +interface ShortUrlsRowProps { onTagClick?: (tag: string) => void; selectedServer: SelectedServer; shortUrl: ShortUrl; @@ -89,3 +89,5 @@ export const ShortUrlsRow = ( ); }; + +export type ShortUrlsRowType = ReturnType; diff --git a/test/short-urls/Paginator.test.tsx b/test/short-urls/Paginator.test.tsx index a7a12ab5..364c8196 100644 --- a/test/short-urls/Paginator.test.tsx +++ b/test/short-urls/Paginator.test.tsx @@ -18,9 +18,11 @@ describe('', () => { [buildPaginator()], [buildPaginator(0)], [buildPaginator(1)], - ])('renders nothing if the number of pages is below 2', (paginator) => { + ])('renders an empty gap if the number of pages is below 2', (paginator) => { const { container } = setUp(paginator); - expect(container.firstChild).toBeNull(); + + expect(container.firstChild).toBeEmpty(); + expect(container.firstChild).toHaveClass('pb-3'); }); it.each([ diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 06b062ed..031dc98e 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -1,5 +1,4 @@ import { screen } from '@testing-library/react'; -import { FC } from 'react'; import { Mock } from 'ts-mockery'; import { MemoryRouter, useNavigate } from 'react-router-dom'; import { ShortUrlsList as createShortUrlsList } from '../../src/short-urls/ShortUrlsList'; @@ -8,7 +7,7 @@ import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import { ReachableServer } from '../../src/servers/data'; import { Settings } from '../../src/settings/reducers/settings'; -import { ShortUrlsTableProps } from '../../src/short-urls/ShortUrlsTable'; +import { ShortUrlsTableType } from '../../src/short-urls/ShortUrlsTable'; import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ @@ -18,7 +17,7 @@ jest.mock('react-router-dom', () => ({ })); describe('', () => { - const ShortUrlsTable: FC = ({ onTagClick }) => onTagClick?.('foo')}>ShortUrlsTable; + const ShortUrlsTable: ShortUrlsTableType = ({ onTagClick }) => onTagClick?.('foo')}>ShortUrlsTable; const ShortUrlsFilteringBar = () => ShortUrlsFilteringBar; const listShortUrlsMock = jest.fn(); const navigate = jest.fn(); From 187fee46f4bd2c93fcd289305a30b5ee5dc4baae Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Dec 2022 13:17:49 +0100 Subject: [PATCH 02/10] Added extra info and new label to highlight disabled short URLs --- src/short-urls/helpers/DisabledLabel.tsx | 21 ++++++++++ .../helpers/ShortUrlVisitsCount.scss | 4 ++ .../helpers/ShortUrlVisitsCount.tsx | 38 +++++++++++++------ src/short-urls/helpers/ShortUrlsRow.scss | 4 -- src/short-urls/helpers/ShortUrlsRow.tsx | 38 ++++++++----------- src/short-urls/helpers/ShortUrlsRowMenu.tsx | 4 +- src/short-urls/helpers/Tags.tsx | 30 +++++++++++++++ src/short-urls/helpers/index.ts | 10 +++++ src/utils/helpers/date.ts | 2 + src/utils/table/ResponsiveTable.scss | 5 ++- 10 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 src/short-urls/helpers/DisabledLabel.tsx create mode 100644 src/short-urls/helpers/Tags.tsx diff --git a/src/short-urls/helpers/DisabledLabel.tsx b/src/short-urls/helpers/DisabledLabel.tsx new file mode 100644 index 00000000..90f42597 --- /dev/null +++ b/src/short-urls/helpers/DisabledLabel.tsx @@ -0,0 +1,21 @@ +import { FC, useRef } from 'react'; +import { UncontrolledTooltip } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faLinkSlash } from '@fortawesome/free-solid-svg-icons'; +import { mutableRefToElementRef } from '../../utils/helpers/components'; + +export const DisabledLabel: FC = () => { + const tooltipRef = useRef(); + + return ( + <> + + + Disabled + + tooltipRef.current) as any} placement="left"> + This short URL cannot be currently visited because of some of its limits. + + + ); +}; diff --git a/src/short-urls/helpers/ShortUrlVisitsCount.scss b/src/short-urls/helpers/ShortUrlVisitsCount.scss index 2910381a..7ae9b852 100644 --- a/src/short-urls/helpers/ShortUrlVisitsCount.scss +++ b/src/short-urls/helpers/ShortUrlVisitsCount.scss @@ -10,3 +10,7 @@ .short-url-visits-count__amount--big { transform: scale(1.5); } + +.short-url-visits-count__tooltip-list-item:not(:last-child) { + margin-bottom: .5rem; +} diff --git a/src/short-urls/helpers/ShortUrlVisitsCount.tsx b/src/short-urls/helpers/ShortUrlVisitsCount.tsx index 3b76bda4..dfa0acf3 100644 --- a/src/short-urls/helpers/ShortUrlVisitsCount.tsx +++ b/src/short-urls/helpers/ShortUrlVisitsCount.tsx @@ -7,8 +7,9 @@ import { prettify } from '../../utils/helpers/numbers'; import { ShortUrl } from '../data'; import { SelectedServer } from '../../servers/data'; import { ShortUrlDetailLink } from './ShortUrlDetailLink'; -import './ShortUrlVisitsCount.scss'; import { mutableRefToElementRef } from '../../utils/helpers/components'; +import { formatHumanFriendly, parseISO } from '../../utils/helpers/date'; +import './ShortUrlVisitsCount.scss'; interface ShortUrlVisitsCountProps { shortUrl?: ShortUrl | null; @@ -20,7 +21,8 @@ interface ShortUrlVisitsCountProps { export const ShortUrlVisitsCount = ( { visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps, ) => { - const maxVisits = shortUrl?.meta?.maxVisits; + const { maxVisits, validSince, validUntil } = shortUrl?.meta ?? {}; + const hasLimit = !!maxVisits || !!validSince || !!validUntil; const visitsLink = ( ); - if (!maxVisits) { + if (!hasLimit) { return visitsLink; } - const prettifiedMaxVisits = prettify(maxVisits); const tooltipRef = useRef(); return ( <> {visitsLink} - - {' '}/ {prettifiedMaxVisits}{' '} - + + {maxVisits && <> / {prettify(maxVisits)}} + tooltipRef.current) as any} placement="bottom"> - This short URL will not accept more than {prettifiedMaxVisits} visits. +
    + {maxVisits && ( +
  • + This short URL will not accept more than {prettify(maxVisits)} visit{maxVisits === 1 ? '' : 's'}. +
  • + )} + {validSince && ( +
  • + This short URL will not accept visits + before {formatHumanFriendly(parseISO(validSince))}. +
  • + )} + {validUntil && ( +
  • + This short URL will not accept visits + after {formatHumanFriendly(parseISO(validUntil))}. +
  • + )} +
); diff --git a/src/short-urls/helpers/ShortUrlsRow.scss b/src/short-urls/helpers/ShortUrlsRow.scss index 4666be1a..64af7818 100644 --- a/src/short-urls/helpers/ShortUrlsRow.scss +++ b/src/short-urls/helpers/ShortUrlsRow.scss @@ -10,10 +10,6 @@ word-break: break-all; } -.short-urls-row__cell--relative { - position: relative; -} - .short-urls-row__cell--indivisible { @media (min-width: $lgMin) { white-space: nowrap; diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx index 159c2b24..10f3e1a4 100644 --- a/src/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -1,15 +1,16 @@ -import { FC, useEffect, useRef } from 'react'; -import { isEmpty } from 'ramda'; +import { useEffect, useRef } from 'react'; import { ExternalLink } from 'react-external-link'; import { ColorGenerator } from '../../utils/services/ColorGenerator'; import { TimeoutToggle } from '../../utils/helpers/hooks'; -import { Tag } from '../../tags/helpers/Tag'; import { SelectedServer } from '../../servers/data'; import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon'; import { ShortUrl } from '../data'; import { Time } from '../../utils/dates/Time'; import { ShortUrlVisitsCount } from './ShortUrlVisitsCount'; -import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu'; +import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu'; +import { Tags } from './Tags'; +import { shortUrlIsDisabled } from './index'; +import { DisabledLabel } from './DisabledLabel'; import './ShortUrlsRow.scss'; interface ShortUrlsRowProps { @@ -19,28 +20,14 @@ interface ShortUrlsRowProps { } export const ShortUrlsRow = ( - ShortUrlsRowMenu: FC, + ShortUrlsRowMenu: ShortUrlsRowMenuType, colorGenerator: ColorGenerator, useTimeoutToggle: TimeoutToggle, ) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => { const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle(); const [active, setActive] = useTimeoutToggle(false, 500); const isFirstRun = useRef(true); - - const renderTags = (tags: string[]) => { - if (isEmpty(tags)) { - return No tags; - } - - return tags.map((tag) => ( - onTagClick?.(tag)} - /> - )); - }; + const isDisabled = shortUrlIsDisabled(shortUrl); useEffect(() => { !isFirstRun.current && setActive(); @@ -53,7 +40,7 @@ export const ShortUrlsRow = (