mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-11 10:47:27 +03:00
Added ShortUrlStatus concept
This commit is contained in:
parent
99485cc6d8
commit
d1a1b7426e
6 changed files with 105 additions and 40 deletions
|
@ -65,6 +65,7 @@ export const ShortUrlsTable = (ShortUrlsRow: ShortUrlsRowType) => ({
|
||||||
<th className={orderableColumnsClasses} onClick={orderByColumn?.('dateCreated')}>
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('dateCreated')}>
|
||||||
Created at {renderOrderIcon?.('dateCreated')}
|
Created at {renderOrderIcon?.('dateCreated')}
|
||||||
</th>
|
</th>
|
||||||
|
<th className="short-urls-table__header-cell" />
|
||||||
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
|
||||||
Short URL {renderOrderIcon?.('shortCode')}
|
Short URL {renderOrderIcon?.('shortCode')}
|
||||||
</th>
|
</th>
|
||||||
|
|
|
@ -31,7 +31,9 @@ export interface ShortUrl {
|
||||||
shortUrl: string;
|
shortUrl: string;
|
||||||
longUrl: string;
|
longUrl: string;
|
||||||
dateCreated: string;
|
dateCreated: string;
|
||||||
visitsCount: number;
|
/** @deprecated */
|
||||||
|
visitsCount: number; // Deprecated since Shlink 3.4.0
|
||||||
|
visitsSummary?: ShortUrlVisitsSummary; // Optional only before Shlink 3.4.0
|
||||||
meta: Required<Nullable<ShortUrlMeta>>;
|
meta: Required<Nullable<ShortUrlMeta>>;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
|
@ -46,6 +48,12 @@ export interface ShortUrlMeta {
|
||||||
maxVisits?: number;
|
maxVisits?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlVisitsSummary {
|
||||||
|
total: number;
|
||||||
|
nonBots: number;
|
||||||
|
bots: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShortUrlModalProps {
|
export interface ShortUrlModalProps {
|
||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
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<HTMLElement | undefined>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span style={{ cursor: 'help' }} ref={mutableRefToElementRef(tooltipRef)} className="badge text-bg-danger">
|
|
||||||
<FontAwesomeIcon icon={faLinkSlash} className="me-1" />
|
|
||||||
Disabled
|
|
||||||
</span>
|
|
||||||
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="left">
|
|
||||||
This short URL cannot be currently visited because of some of its limits.
|
|
||||||
</UncontrolledTooltip>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
91
src/short-urls/helpers/ShortUrlStatus.tsx
Normal file
91
src/short-urls/helpers/ShortUrlStatus.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { FC, ReactNode, useRef } from 'react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||||
|
import { faLinkSlash, faCalendarXmark, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import { isBefore } from 'date-fns';
|
||||||
|
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
||||||
|
import { ShortUrl } from '../data';
|
||||||
|
import { formatHumanFriendly, now, parseISO } from '../../utils/helpers/date';
|
||||||
|
|
||||||
|
interface ShortUrlStatusProps {
|
||||||
|
shortUrl: ShortUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusResult {
|
||||||
|
icon: IconDefinition;
|
||||||
|
className: string;
|
||||||
|
description?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveShortUrlStatus = (shortUrl: ShortUrl): StatusResult => {
|
||||||
|
const { meta, visitsCount, visitsSummary } = shortUrl;
|
||||||
|
const { maxVisits, validSince, validUntil } = meta;
|
||||||
|
const totalVisits = visitsSummary?.total ?? visitsCount;
|
||||||
|
|
||||||
|
if (maxVisits && totalVisits >= maxVisits) {
|
||||||
|
return {
|
||||||
|
icon: faLinkSlash,
|
||||||
|
className: 'text-danger',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
This short URL cannot be currently visited because it has reached the maximum
|
||||||
|
amount of <b>{maxVisits}</b> visit{maxVisits > 1 ? 's' : ''}.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validUntil && isBefore(parseISO(validUntil), now())) {
|
||||||
|
return {
|
||||||
|
icon: faCalendarXmark,
|
||||||
|
className: 'text-danger',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
This short URL cannot be visited
|
||||||
|
since <b className="indivisible">{formatHumanFriendly(parseISO(validUntil))}</b>.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validSince && isBefore(now(), parseISO(validSince))) {
|
||||||
|
return {
|
||||||
|
icon: faCalendarXmark,
|
||||||
|
className: 'text-warning',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
This short URL will start working
|
||||||
|
on <b className="indivisible">{formatHumanFriendly(parseISO(validSince))}</b>.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: faCheck,
|
||||||
|
className: 'text-primary',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShortUrlStatus: FC<ShortUrlStatusProps> = ({ shortUrl }) => {
|
||||||
|
const tooltipRef = useRef<HTMLElement | undefined>();
|
||||||
|
const { icon, className, description } = resolveShortUrlStatus(shortUrl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
style={{ cursor: !description ? undefined : 'help' }}
|
||||||
|
ref={mutableRefToElementRef(tooltipRef)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} />
|
||||||
|
</span>
|
||||||
|
{description && (
|
||||||
|
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="right">
|
||||||
|
{description}
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -9,8 +9,7 @@ import { Time } from '../../utils/dates/Time';
|
||||||
import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
|
import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
|
||||||
import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu';
|
import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu';
|
||||||
import { Tags } from './Tags';
|
import { Tags } from './Tags';
|
||||||
import { shortUrlIsDisabled } from './index';
|
import { ShortUrlStatus } from './ShortUrlStatus';
|
||||||
import { DisabledLabel } from './DisabledLabel';
|
|
||||||
import './ShortUrlsRow.scss';
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
interface ShortUrlsRowProps {
|
interface ShortUrlsRowProps {
|
||||||
|
@ -27,7 +26,6 @@ export const ShortUrlsRow = (
|
||||||
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
|
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
|
||||||
const [active, setActive] = useTimeoutToggle(false, 500);
|
const [active, setActive] = useTimeoutToggle(false, 500);
|
||||||
const isFirstRun = useRef(true);
|
const isFirstRun = useRef(true);
|
||||||
const isDisabled = shortUrlIsDisabled(shortUrl);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!isFirstRun.current && setActive();
|
!isFirstRun.current && setActive();
|
||||||
|
@ -39,6 +37,9 @@ export const ShortUrlsRow = (
|
||||||
<td className="indivisible short-urls-row__cell responsive-table__cell" data-th="Created at">
|
<td className="indivisible short-urls-row__cell responsive-table__cell" data-th="Created at">
|
||||||
<Time date={shortUrl.dateCreated} />
|
<Time date={shortUrl.dateCreated} />
|
||||||
</td>
|
</td>
|
||||||
|
<td className="responsive-table__cell short-urls-row__cell" data-th="Status">
|
||||||
|
<ShortUrlStatus shortUrl={shortUrl} />
|
||||||
|
</td>
|
||||||
<td className="responsive-table__cell short-urls-row__cell" data-th="Short URL">
|
<td className="responsive-table__cell short-urls-row__cell" data-th="Short URL">
|
||||||
<span className="position-relative short-urls-row__cell--indivisible">
|
<span className="position-relative short-urls-row__cell--indivisible">
|
||||||
<span className="short-urls-row__short-url-wrapper">
|
<span className="short-urls-row__short-url-wrapper">
|
||||||
|
@ -54,11 +55,6 @@ export const ShortUrlsRow = (
|
||||||
className="responsive-table__cell short-urls-row__cell short-urls-row__cell--break"
|
className="responsive-table__cell short-urls-row__cell short-urls-row__cell--break"
|
||||||
data-th={`${shortUrl.title ? 'Title' : 'Long URL'}`}
|
data-th={`${shortUrl.title ? 'Title' : 'Long URL'}`}
|
||||||
>
|
>
|
||||||
{isDisabled && (
|
|
||||||
<div className="float-end ms-2">
|
|
||||||
<DisabledLabel />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ExternalLink href={shortUrl.longUrl}>{shortUrl.title ?? shortUrl.longUrl}</ExternalLink>
|
<ExternalLink href={shortUrl.longUrl}>{shortUrl.title ?? shortUrl.longUrl}</ExternalLink>
|
||||||
</td>
|
</td>
|
||||||
{shortUrl.title && (
|
{shortUrl.title && (
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { ShortUrl, ShortUrlData } from '../data';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||||
import { ShortUrlCreationSettings } from '../../settings/reducers/settings';
|
import { ShortUrlCreationSettings } from '../../settings/reducers/settings';
|
||||||
import { isBefore, parseISO } from 'date-fns';
|
|
||||||
import { now } from '../../utils/helpers/date';
|
|
||||||
|
|
||||||
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
||||||
if (isNil(domain)) {
|
if (isNil(domain)) {
|
||||||
|
@ -22,14 +20,6 @@ export const domainMatches = (shortUrl: ShortUrl, domain: string): boolean => {
|
||||||
return shortUrl.domain === domain;
|
return shortUrl.domain === domain;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shortUrlIsDisabled = (shortUrl: ShortUrl): boolean => (
|
|
||||||
!!shortUrl.meta.maxVisits && shortUrl.visitsCount >= shortUrl.meta.maxVisits
|
|
||||||
) || (
|
|
||||||
!!shortUrl.meta.validSince && isBefore(now(), parseISO(shortUrl.meta.validSince))
|
|
||||||
) || (
|
|
||||||
!!shortUrl.meta.validUntil && isBefore(parseISO(shortUrl.meta.validUntil), now())
|
|
||||||
);
|
|
||||||
|
|
||||||
export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
|
export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
|
||||||
const validateUrl = settings?.validateUrls ?? false;
|
const validateUrl = settings?.validateUrls ?? false;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue