mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-31 21:38:19 +03:00
Merge pull request #813 from acelaya-forks/feature/device-long-urls
Feature/device long urls
This commit is contained in:
commit
46749044e2
17 changed files with 353 additions and 91 deletions
|
@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* *Nothing*
|
||||
* [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs.
|
||||
|
||||
### Changed
|
||||
* Update to Vite 4.1
|
||||
|
|
86
package-lock.json
generated
86
package-lock.json
generated
|
@ -10,10 +10,11 @@
|
|||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@json2csv/plainjs": "^6.1.2",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
|
@ -1937,49 +1938,66 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
|
||||
"integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz",
|
||||
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz",
|
||||
"integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
@ -13611,27 +13629,45 @@
|
|||
"dev": true
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.2.1"
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz",
|
||||
"integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg=="
|
||||
},
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "6.2.1"
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz",
|
||||
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz",
|
||||
"integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-xI0c+a8xnKItAXCN8rZgCNCJQiVAd2Y7p9e2ND6zN3J3ekneu96qrePieJ7yA7073C1JxxoM3vH1RU7rYsaj8w==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz",
|
||||
"integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
"@fortawesome/fontawesome-common-types": "6.3.0"
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
|
|
|
@ -26,10 +26,11 @@
|
|||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@json2csv/plainjs": "^6.1.2",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faAndroid, faApple } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faDesktop } from '@fortawesome/free-solid-svg-icons';
|
||||
import classNames from 'classnames';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { cond, isEmpty, pipe, replace, T, trim } from 'ramda';
|
||||
import type { FC } from 'react';
|
||||
import { isEmpty, pipe, replace, trim } from 'ramda';
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||
import type { InputType } from 'reactstrap/types/lib/Input';
|
||||
|
@ -12,10 +16,10 @@ import type { DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
|||
import { DateTimeInput } from '../utils/dates/DateTimeInput';
|
||||
import { formatIsoDate } from '../utils/helpers/date';
|
||||
import { useFeature } from '../utils/helpers/features';
|
||||
import { IconInput } from '../utils/IconInput';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import type { OptionalString } from '../utils/utils';
|
||||
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
|
||||
import type { ShortUrlData } from './data';
|
||||
import type { DeviceLongUrls, ShortUrlData } from './data';
|
||||
import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
|
||||
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
|
||||
import './ShortUrlForm.scss';
|
||||
|
@ -41,38 +45,38 @@ export const ShortUrlForm = (
|
|||
DomainSelector: FC<DomainSelectorProps>,
|
||||
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
|
||||
const [shortUrlData, setShortUrlData] = useState(initialState);
|
||||
const reset = () => setShortUrlData(initialState);
|
||||
const supportsDeviceLongUrls = useFeature('deviceLongUrls', selectedServer);
|
||||
|
||||
const isEdit = mode === 'edit';
|
||||
const isBasicMode = mode === 'create-basic';
|
||||
const hadTitleOriginally = hasValue(initialState.title);
|
||||
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
|
||||
const reset = () => setShortUrlData(initialState);
|
||||
const resolveNewTitle = (): OptionalString => {
|
||||
const hasNewTitle = hasValue(shortUrlData.title);
|
||||
const matcher = cond<never, OptionalString>([
|
||||
[() => !hasNewTitle && !hadTitleOriginally, () => undefined],
|
||||
[() => !hasNewTitle && hadTitleOriginally, () => null],
|
||||
[T, () => shortUrlData.title],
|
||||
]);
|
||||
const setResettableValue = (value: string, initialValue?: any) => {
|
||||
if (hasValue(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return matcher();
|
||||
// If an initial value was provided for this when the input is "emptied", explicitly set it to null so that the
|
||||
// value gets removed. Otherwise, set undefined so that it gets ignored.
|
||||
return hasValue(initialValue) ? null : undefined;
|
||||
};
|
||||
const submit = handleEventPreventingDefault(async () => onSave({
|
||||
...shortUrlData,
|
||||
validSince: formatIsoDate(shortUrlData.validSince) ?? null,
|
||||
validUntil: formatIsoDate(shortUrlData.validUntil) ?? null,
|
||||
maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits),
|
||||
title: resolveNewTitle(),
|
||||
}).then(() => !isEdit && reset()).catch(() => {}));
|
||||
|
||||
useEffect(() => {
|
||||
setShortUrlData(initialState);
|
||||
}, [initialState]);
|
||||
|
||||
// TODO Consider extracting these functions to local components
|
||||
const renderOptionalInput = (
|
||||
id: NonDateFields,
|
||||
placeholder: string,
|
||||
type: InputType = 'text',
|
||||
props = {},
|
||||
props: any = {},
|
||||
fromGroupProps = {},
|
||||
) => (
|
||||
<FormGroup {...fromGroupProps}>
|
||||
|
@ -81,11 +85,27 @@ export const ShortUrlForm = (
|
|||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={shortUrlData[id] ?? ''}
|
||||
onChange={(e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value })}
|
||||
onChange={props.onChange ?? ((e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value }))}
|
||||
{...props}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
const renderDeviceLongUrlInput = (id: keyof DeviceLongUrls, placeholder: string, icon: IconProp) => (
|
||||
<IconInput
|
||||
icon={icon}
|
||||
id={id}
|
||||
type="url"
|
||||
placeholder={placeholder}
|
||||
value={shortUrlData.deviceLongUrls?.[id] ?? ''}
|
||||
onChange={(e) => setShortUrlData({
|
||||
...shortUrlData,
|
||||
deviceLongUrls: {
|
||||
...(shortUrlData.deviceLongUrls ?? {}),
|
||||
[id]: setResettableValue(e.target.value, initialState.deviceLongUrls?.[id]),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateTimeInputProps> = {}) => (
|
||||
<DateTimeInput
|
||||
selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
|
||||
|
@ -123,14 +143,38 @@ export const ShortUrlForm = (
|
|||
{isBasicMode && basicComponents}
|
||||
{!isBasicMode && (
|
||||
<>
|
||||
<SimpleCard title="Main options" className="mb-3">
|
||||
{basicComponents}
|
||||
</SimpleCard>
|
||||
<Row>
|
||||
<div
|
||||
className={classNames('mb-3', { 'col-sm-6': supportsDeviceLongUrls, 'col-12': !supportsDeviceLongUrls })}
|
||||
>
|
||||
<SimpleCard title="Main options" className="mb-3">
|
||||
{basicComponents}
|
||||
</SimpleCard>
|
||||
</div>
|
||||
{supportsDeviceLongUrls && (
|
||||
<div className="col-sm-6 mb-3">
|
||||
<SimpleCard title="Device-specific long URLs">
|
||||
<FormGroup>
|
||||
{renderDeviceLongUrlInput('android', 'Android-specific redirection', faAndroid)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
{renderDeviceLongUrlInput('ios', 'iOS-specific redirection', faApple)}
|
||||
</FormGroup>
|
||||
{renderDeviceLongUrlInput('desktop', 'Desktop-specific redirection', faDesktop)}
|
||||
</SimpleCard>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<div className="col-sm-6 mb-3">
|
||||
<SimpleCard title="Customize the short URL">
|
||||
{renderOptionalInput('title', 'Title')}
|
||||
{renderOptionalInput('title', 'Title', 'text', {
|
||||
onChange: ({ target }: ChangeEvent<HTMLInputElement>) => setShortUrlData({
|
||||
...shortUrlData,
|
||||
title: setResettableValue(target.value, initialState.title),
|
||||
}),
|
||||
})}
|
||||
{!isEdit && (
|
||||
<>
|
||||
<Row>
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import type { Order } from '../../utils/helpers/ordering';
|
||||
import type { Nullable, OptionalString } from '../../utils/utils';
|
||||
|
||||
export interface DeviceLongUrls {
|
||||
android?: OptionalString;
|
||||
ios?: OptionalString;
|
||||
desktop?: OptionalString;
|
||||
}
|
||||
|
||||
export interface EditShortUrlData {
|
||||
longUrl?: string;
|
||||
deviceLongUrls?: DeviceLongUrls;
|
||||
tags?: string[];
|
||||
title?: string | null;
|
||||
validSince?: Date | string | null;
|
||||
|
@ -30,6 +37,7 @@ export interface ShortUrl {
|
|||
shortCode: string;
|
||||
shortUrl: string;
|
||||
longUrl: string;
|
||||
deviceLongUrls?: Required<DeviceLongUrls>, // Optional only before Shlink 3.5.0
|
||||
dateCreated: string;
|
||||
/** @deprecated */
|
||||
visitsCount: number; // Deprecated since Shlink 3.4.0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faClone as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faTimes as closeIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useEffect } from 'react';
|
||||
|
|
|
@ -37,6 +37,7 @@ export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUr
|
|||
maxVisits: shortUrl.meta.maxVisits ?? undefined,
|
||||
crawlable: shortUrl.crawlable,
|
||||
forwardQuery: shortUrl.forwardQuery,
|
||||
deviceLongUrls: shortUrl.deviceLongUrls,
|
||||
validateUrl,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faClone as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
|
26
src/utils/IconInput.scss
Normal file
26
src/utils/IconInput.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
@import './mixins/vertical-align';
|
||||
@import './base';
|
||||
|
||||
.icon-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-input-container__input {
|
||||
padding-right: 35px !important;
|
||||
}
|
||||
|
||||
.icon-input-container__input:not(:disabled) {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.card .icon-input-container__input:not(:disabled),
|
||||
.dropdown .icon-input-container__input:not(:disabled) {
|
||||
background-color: var(--input-color) !important;
|
||||
}
|
||||
|
||||
.icon-input-container__icon {
|
||||
@include vertical-align();
|
||||
|
||||
right: .75rem;
|
||||
cursor: pointer;
|
||||
}
|
29
src/utils/IconInput.tsx
Normal file
29
src/utils/IconInput.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import type { FC } from 'react';
|
||||
import type { InputProps } from 'reactstrap';
|
||||
import { Input } from 'reactstrap';
|
||||
import { useElementRef } from './helpers/hooks';
|
||||
import './IconInput.scss';
|
||||
|
||||
type IconInputProps = InputProps & {
|
||||
icon: IconProp;
|
||||
};
|
||||
|
||||
export const IconInput: FC<IconInputProps> = ({ icon, className, ...rest }) => {
|
||||
const ref = useElementRef<HTMLInputElement>();
|
||||
const classes = classNames('icon-input-container__input', className);
|
||||
|
||||
return (
|
||||
<div className="icon-input-container">
|
||||
<Input className={classes} innerRef={ref} {...rest} />
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
fixedWidth
|
||||
className="icon-input-container__icon"
|
||||
onClick={() => ref.current?.focus()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,30 +1,6 @@
|
|||
@import '../mixins/vertical-align';
|
||||
@import '../base';
|
||||
|
||||
.date-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.date-input-container__input {
|
||||
padding-right: 35px !important;
|
||||
}
|
||||
|
||||
.date-input-container__input:not(:disabled) {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.card .date-input-container__input:not(:disabled),
|
||||
.dropdown .date-input-container__input:not(:disabled) {
|
||||
background-color: var(--input-color) !important;
|
||||
}
|
||||
|
||||
.date-input-container__icon {
|
||||
@include vertical-align();
|
||||
|
||||
right: .75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-datepicker__close-icon.react-datepicker__close-icon {
|
||||
@include vertical-align();
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export const DateInput = (props: DateInputProps) => {
|
|||
const ref = useRef<{ input: HTMLInputElement }>();
|
||||
|
||||
return (
|
||||
<div className="date-input-container">
|
||||
<div className="icon-input-container">
|
||||
<DatePicker
|
||||
{...props}
|
||||
popperModifiers={[
|
||||
|
@ -26,14 +26,14 @@ export const DateInput = (props: DateInputProps) => {
|
|||
},
|
||||
]}
|
||||
dateFormat={dateFormat ?? STANDARD_DATE_FORMAT}
|
||||
className={classNames('date-input-container__input form-control', className)}
|
||||
className={classNames('icon-input-container__input form-control', className)}
|
||||
// @ts-expect-error The DatePicker type definition is wrong. It has a ref prop
|
||||
ref={ref}
|
||||
/>
|
||||
{showCalendarIcon && (
|
||||
<FontAwesomeIcon
|
||||
icon={calendarIcon}
|
||||
className="date-input-container__icon"
|
||||
className="icon-input-container__icon"
|
||||
onClick={() => ref.current?.input.focus()}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -30,7 +30,7 @@ exports[`<UserInterfaceSettings /> shows different icons based on theme 2`] = `
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM352 256c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zm32 0c0-70.7-57.3-128-128-128s-128 57.3-128 128s57.3 128 128 128s128-57.3 128-128z"
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -48,7 +48,7 @@ exports[`<UserInterfaceSettings /> shows different icons based on theme 3`] = `
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM352 256c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zm32 0c0-70.7-57.3-128-128-128s-128 57.3-128 128s57.3 128 128 128s128-57.3 128-128z"
|
||||
d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
|
|
@ -31,15 +31,30 @@ describe('<ShortUrlForm />', () => {
|
|||
await user.type(screen.getByPlaceholderText('Custom slug'), 'my-slug');
|
||||
},
|
||||
{ customSlug: 'my-slug' },
|
||||
null,
|
||||
],
|
||||
[
|
||||
async (user: UserEvent) => {
|
||||
await user.type(screen.getByPlaceholderText('Short code length'), '15');
|
||||
},
|
||||
{ shortCodeLength: '15' },
|
||||
null,
|
||||
],
|
||||
])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues) => {
|
||||
const { user } = setUp();
|
||||
[
|
||||
async (user: UserEvent) => {
|
||||
await user.type(screen.getByPlaceholderText('Android-specific redirection'), 'https://android.com');
|
||||
await user.type(screen.getByPlaceholderText('iOS-specific redirection'), 'https://ios.com');
|
||||
},
|
||||
{
|
||||
deviceLongUrls: {
|
||||
android: 'https://android.com',
|
||||
ios: 'https://ios.com',
|
||||
},
|
||||
},
|
||||
Mock.of<ReachableServer>({ version: '3.5.0' }),
|
||||
],
|
||||
])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues, selectedServer) => {
|
||||
const { user } = setUp(selectedServer);
|
||||
const validSince = parseDate('2017-01-01', 'yyyy-MM-dd');
|
||||
const validUntil = parseDate('2017-01-06', 'yyyy-MM-dd');
|
||||
|
||||
|
@ -81,17 +96,18 @@ describe('<ShortUrlForm />', () => {
|
|||
[null, true, 'new title'],
|
||||
[undefined, true, 'new title'],
|
||||
['', true, 'new title'],
|
||||
[null, false, undefined],
|
||||
['', false, undefined],
|
||||
['old title', true, 'new title'],
|
||||
[null, false, null],
|
||||
['', false, ''],
|
||||
[undefined, false, undefined],
|
||||
['old title', false, null],
|
||||
])('sends expected title based on original and new values', async (originalTitle, withNewTitle, expectedSentTitle) => {
|
||||
const { user } = setUp(Mock.of<ReachableServer>({ version: '2.6.0' }), 'create', originalTitle);
|
||||
|
||||
await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar');
|
||||
await user.clear(screen.getByPlaceholderText('Title'));
|
||||
if (withNewTitle) {
|
||||
await user.type(screen.getByPlaceholderText('Title'), 'new title');
|
||||
} else {
|
||||
await user.clear(screen.getByPlaceholderText('Title'));
|
||||
}
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
|
@ -99,4 +115,20 @@ describe('<ShortUrlForm />', () => {
|
|||
title: expectedSentTitle,
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[Mock.of<ReachableServer>({ version: '3.0.0' }), false],
|
||||
[Mock.of<ReachableServer>({ version: '3.4.0' }), false],
|
||||
[Mock.of<ReachableServer>({ version: '3.5.0' }), true],
|
||||
[Mock.of<ReachableServer>({ version: '3.6.0' }), true],
|
||||
])('shows device-specific long URLs only for servers supporting it', (selectedServer, fieldsExist) => {
|
||||
setUp(selectedServer);
|
||||
const placeholders = ['Android-specific redirection', 'iOS-specific redirection', 'Desktop-specific redirection'];
|
||||
|
||||
if (fieldsExist) {
|
||||
placeholders.forEach((placeholder) => expect(screen.getByPlaceholderText(placeholder)).toBeInTheDocument());
|
||||
} else {
|
||||
placeholders.forEach((placeholder) => expect(screen.queryByPlaceholderText(placeholder)).not.toBeInTheDocument());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
24
test/utils/IconInput.test.tsx
Normal file
24
test/utils/IconInput.test.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faAppleAlt, faCalendar, faTable } from '@fortawesome/free-solid-svg-icons';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { IconInput } from '../../src/utils/IconInput';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<IconInput />', () => {
|
||||
const setUp = (icon: IconProp, placeholder?: string) => renderWithEvents(
|
||||
<IconInput icon={icon} placeholder={placeholder} />,
|
||||
);
|
||||
|
||||
it.each([faCalendar, faAppleAlt, faTable])('displays provided icon', (icon) => {
|
||||
const { container } = setUp(icon);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('focuses input on icon click', async () => {
|
||||
const { user } = setUp(faCalendar, 'foo');
|
||||
|
||||
expect(screen.getByPlaceholderText('foo')).not.toHaveFocus();
|
||||
await user.click(screen.getByRole('img', { hidden: true }));
|
||||
expect(screen.getByPlaceholderText('foo')).toHaveFocus();
|
||||
});
|
||||
});
|
|
@ -4,8 +4,8 @@ exports[`<CopyToClipboardIcon /> wraps expected components 1`] = `
|
|||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-copy ms-2 copy-to-clipboard-icon"
|
||||
data-icon="copy"
|
||||
class="svg-inline--fa fa-clone ms-2 copy-to-clipboard-icon"
|
||||
data-icon="clone"
|
||||
data-prefix="far"
|
||||
focusable="false"
|
||||
role="img"
|
||||
|
@ -13,7 +13,7 @@ exports[`<CopyToClipboardIcon /> wraps expected components 1`] = `
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M502.6 70.63l-61.25-61.25C435.4 3.371 427.2 0 418.7 0H255.1c-35.35 0-64 28.66-64 64l.0195 256C192 355.4 220.7 384 256 384h192c35.2 0 64-28.8 64-64V93.25C512 84.77 508.6 76.63 502.6 70.63zM464 320c0 8.836-7.164 16-16 16H255.1c-8.838 0-16-7.164-16-16L239.1 64.13c0-8.836 7.164-16 16-16h128L384 96c0 17.67 14.33 32 32 32h47.1V320zM272 448c0 8.836-7.164 16-16 16H63.1c-8.838 0-16-7.164-16-16L47.98 192.1c0-8.836 7.164-16 16-16H160V128H63.99c-35.35 0-64 28.65-64 64l.0098 256C.002 483.3 28.66 512 64 512h192c35.2 0 64-28.8 64-64v-32h-47.1L272 448z"
|
||||
d="M64 464H288c8.8 0 16-7.2 16-16V384h48v64c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h64v48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16zM224 304H448c8.8 0 16-7.2 16-16V64c0-8.8-7.2-16-16-16H224c-8.8 0-16 7.2-16 16V288c0 8.8 7.2 16 16 16zm-64-16V64c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V288c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
|
85
test/utils/__snapshots__/IconInput.test.tsx.snap
Normal file
85
test/utils/__snapshots__/IconInput.test.tsx.snap
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<IconInput /> displays provided icon 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="icon-input-container"
|
||||
>
|
||||
<input
|
||||
class="icon-input-container__input form-control"
|
||||
type="text"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-calendar fa-fw icon-input-container__icon"
|
||||
data-icon="calendar"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M96 32V64H48C21.5 64 0 85.5 0 112v48H448V112c0-26.5-21.5-48-48-48H352V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V64H160V32c0-17.7-14.3-32-32-32S96 14.3 96 32zM448 192H0V464c0 26.5 21.5 48 48 48H400c26.5 0 48-21.5 48-48V192z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<IconInput /> displays provided icon 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="icon-input-container"
|
||||
>
|
||||
<input
|
||||
class="icon-input-container__input form-control"
|
||||
type="text"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-apple-whole fa-fw icon-input-container__icon"
|
||||
data-icon="apple-whole"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M224 112c-8.8 0-16-7.2-16-16V80c0-44.2 35.8-80 80-80h16c8.8 0 16 7.2 16 16V32c0 44.2-35.8 80-80 80H224zM0 288c0-76.3 35.7-160 112-160c27.3 0 59.7 10.3 82.7 19.3c18.8 7.3 39.9 7.3 58.7 0c22.9-8.9 55.4-19.3 82.7-19.3c76.3 0 112 83.7 112 160c0 128-80 224-160 224c-16.5 0-38.1-6.6-51.5-11.3c-8.1-2.8-16.9-2.8-25 0c-13.4 4.7-35 11.3-51.5 11.3C80 512 0 416 0 288z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<IconInput /> displays provided icon 3`] = `
|
||||
<div>
|
||||
<div
|
||||
class="icon-input-container"
|
||||
>
|
||||
<input
|
||||
class="icon-input-container__input form-control"
|
||||
type="text"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-inline--fa fa-table fa-fw icon-input-container__icon"
|
||||
data-icon="table"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
Loading…
Reference in a new issue