mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Added extra info and new label to highlight disabled short URLs
This commit is contained in:
parent
90837546ab
commit
187fee46f4
10 changed files with 117 additions and 39 deletions
21
src/short-urls/helpers/DisabledLabel.tsx
Normal file
21
src/short-urls/helpers/DisabledLabel.tsx
Normal file
|
@ -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<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -10,3 +10,7 @@
|
||||||
.short-url-visits-count__amount--big {
|
.short-url-visits-count__amount--big {
|
||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.short-url-visits-count__tooltip-list-item:not(:last-child) {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
|
@ -7,8 +7,9 @@ import { prettify } from '../../utils/helpers/numbers';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
||||||
import './ShortUrlVisitsCount.scss';
|
|
||||||
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
import { mutableRefToElementRef } from '../../utils/helpers/components';
|
||||||
|
import { formatHumanFriendly, parseISO } from '../../utils/helpers/date';
|
||||||
|
import './ShortUrlVisitsCount.scss';
|
||||||
|
|
||||||
interface ShortUrlVisitsCountProps {
|
interface ShortUrlVisitsCountProps {
|
||||||
shortUrl?: ShortUrl | null;
|
shortUrl?: ShortUrl | null;
|
||||||
|
@ -20,7 +21,8 @@ interface ShortUrlVisitsCountProps {
|
||||||
export const ShortUrlVisitsCount = (
|
export const ShortUrlVisitsCount = (
|
||||||
{ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps,
|
{ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps,
|
||||||
) => {
|
) => {
|
||||||
const maxVisits = shortUrl?.meta?.maxVisits;
|
const { maxVisits, validSince, validUntil } = shortUrl?.meta ?? {};
|
||||||
|
const hasLimit = !!maxVisits || !!validSince || !!validUntil;
|
||||||
const visitsLink = (
|
const visitsLink = (
|
||||||
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||||
<strong
|
<strong
|
||||||
|
@ -31,29 +33,43 @@ export const ShortUrlVisitsCount = (
|
||||||
</ShortUrlDetailLink>
|
</ShortUrlDetailLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!maxVisits) {
|
if (!hasLimit) {
|
||||||
return visitsLink;
|
return visitsLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prettifiedMaxVisits = prettify(maxVisits);
|
|
||||||
const tooltipRef = useRef<HTMLElement | undefined>();
|
const tooltipRef = useRef<HTMLElement | undefined>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="indivisible">
|
<span className="indivisible">
|
||||||
{visitsLink}
|
{visitsLink}
|
||||||
<small
|
<small className="short-urls-visits-count__max-visits-control" ref={mutableRefToElementRef(tooltipRef)}>
|
||||||
className="short-urls-visits-count__max-visits-control"
|
{maxVisits && <> / {prettify(maxVisits)}</>}
|
||||||
ref={mutableRefToElementRef(tooltipRef)}
|
<sup className="ms-1">
|
||||||
>
|
|
||||||
{' '}/ {prettifiedMaxVisits}{' '}
|
|
||||||
<sup>
|
|
||||||
<FontAwesomeIcon icon={infoIcon} />
|
<FontAwesomeIcon icon={infoIcon} />
|
||||||
</sup>
|
</sup>
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="bottom">
|
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="bottom">
|
||||||
This short URL will not accept more than <b>{prettifiedMaxVisits}</b> visits.
|
<ul className="list-unstyled mb-0">
|
||||||
|
{maxVisits && (
|
||||||
|
<li className="short-url-visits-count__tooltip-list-item">
|
||||||
|
This short URL will not accept more than <b>{prettify(maxVisits)}</b> visit{maxVisits === 1 ? '' : 's'}.
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{validSince && (
|
||||||
|
<li className="short-url-visits-count__tooltip-list-item">
|
||||||
|
This short URL will not accept visits
|
||||||
|
before <b className="indivisible">{formatHumanFriendly(parseISO(validSince))}</b>.
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{validUntil && (
|
||||||
|
<li className="short-url-visits-count__tooltip-list-item">
|
||||||
|
This short URL will not accept visits
|
||||||
|
after <b className="indivisible">{formatHumanFriendly(parseISO(validUntil))}</b>.
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,10 +10,6 @@
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.short-urls-row__cell--relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.short-urls-row__cell--indivisible {
|
.short-urls-row__cell--indivisible {
|
||||||
@media (min-width: $lgMin) {
|
@media (min-width: $lgMin) {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { FC, useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { isEmpty } from 'ramda';
|
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import { TimeoutToggle } from '../../utils/helpers/hooks';
|
import { TimeoutToggle } from '../../utils/helpers/hooks';
|
||||||
import { Tag } from '../../tags/helpers/Tag';
|
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
import { Time } from '../../utils/dates/Time';
|
import { Time } from '../../utils/dates/Time';
|
||||||
import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
|
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';
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
interface ShortUrlsRowProps {
|
interface ShortUrlsRowProps {
|
||||||
|
@ -19,28 +20,14 @@ interface ShortUrlsRowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShortUrlsRow = (
|
export const ShortUrlsRow = (
|
||||||
ShortUrlsRowMenu: FC<ShortUrlsRowMenuProps>,
|
ShortUrlsRowMenu: ShortUrlsRowMenuType,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
useTimeoutToggle: TimeoutToggle,
|
useTimeoutToggle: TimeoutToggle,
|
||||||
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
||||||
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);
|
||||||
const renderTags = (tags: string[]) => {
|
|
||||||
if (isEmpty(tags)) {
|
|
||||||
return <i className="indivisible"><small>No tags</small></i>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags.map((tag) => (
|
|
||||||
<Tag
|
|
||||||
colorGenerator={colorGenerator}
|
|
||||||
key={tag}
|
|
||||||
text={tag}
|
|
||||||
onClick={() => onTagClick?.(tag)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!isFirstRun.current && setActive();
|
!isFirstRun.current && setActive();
|
||||||
|
@ -53,7 +40,7 @@ export const ShortUrlsRow = (
|
||||||
<Time date={shortUrl.dateCreated} />
|
<Time date={shortUrl.dateCreated} />
|
||||||
</td>
|
</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="short-urls-row__cell--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">
|
||||||
<ExternalLink href={shortUrl.shortUrl} />
|
<ExternalLink href={shortUrl.shortUrl} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -67,6 +54,11 @@ 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 && (
|
||||||
|
@ -74,7 +66,9 @@ export const ShortUrlsRow = (
|
||||||
<ExternalLink href={shortUrl.longUrl} />
|
<ExternalLink href={shortUrl.longUrl} />
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className="responsive-table__cell short-urls-row__cell" data-th="Tags">{renderTags(shortUrl.tags)}</td>
|
<td className="responsive-table__cell short-urls-row__cell" data-th="Tags">
|
||||||
|
<Tags tags={shortUrl.tags} colorGenerator={colorGenerator} onTagClick={onTagClick} disabled={isDisabled} />
|
||||||
|
</td>
|
||||||
<td className="responsive-table__cell short-urls-row__cell text-lg-end" data-th="Visits">
|
<td className="responsive-table__cell short-urls-row__cell text-lg-end" data-th="Visits">
|
||||||
<ShortUrlVisitsCount
|
<ShortUrlVisitsCount
|
||||||
visitsCount={shortUrl.visitsCount}
|
visitsCount={shortUrl.visitsCount}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { SelectedServer } from '../../servers/data';
|
||||||
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
||||||
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
||||||
|
|
||||||
export interface ShortUrlsRowMenuProps {
|
interface ShortUrlsRowMenuProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
}
|
}
|
||||||
|
@ -51,3 +51,5 @@ export const ShortUrlsRowMenu = (
|
||||||
</DropdownBtnMenu>
|
</DropdownBtnMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShortUrlsRowMenuType = ReturnType<typeof ShortUrlsRowMenu>;
|
||||||
|
|
30
src/short-urls/helpers/Tags.tsx
Normal file
30
src/short-urls/helpers/Tags.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { isEmpty } from 'ramda';
|
||||||
|
import { Tag } from '../../tags/helpers/Tag';
|
||||||
|
import { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
|
|
||||||
|
interface TagsProps {
|
||||||
|
tags: string[];
|
||||||
|
onTagClick?: (tag: string) => void;
|
||||||
|
colorGenerator: ColorGenerator;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tags: FC<TagsProps> = ({ tags, onTagClick, colorGenerator, disabled = false }) => {
|
||||||
|
if (isEmpty(tags)) {
|
||||||
|
return disabled ? null : <i className="indivisible"><small>No tags</small></i>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={tag}
|
||||||
|
text={tag}
|
||||||
|
colorGenerator={colorGenerator}
|
||||||
|
onClick={() => onTagClick?.(tag)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,6 +3,8 @@ 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)) {
|
||||||
|
@ -20,6 +22,14 @@ 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;
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,8 @@ export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date,
|
||||||
|
|
||||||
export const formatInternational = formatDate();
|
export const formatInternational = formatDate();
|
||||||
|
|
||||||
|
export const formatHumanFriendly = formatDate(STANDARD_DATE_AND_TIME_FORMAT);
|
||||||
|
|
||||||
export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, now());
|
export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, now());
|
||||||
|
|
||||||
export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
|
export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
|
||||||
|
|
|
@ -15,10 +15,13 @@
|
||||||
.responsive-table__row {
|
.responsive-table__row {
|
||||||
@media (max-width: $responsiveTableBreakpoint) {
|
@media (max-width: $responsiveTableBreakpoint) {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
border-top: 2px solid var(--border-color);
|
border-top: 2px solid var(--border-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue