shlink-web-client/src/short-urls/ShortUrlForm.tsx

231 lines
9.1 KiB
TypeScript
Raw Normal View History

2023-02-18 13:11:01 +03:00
import { parseISO } from 'date-fns';
import { cond, isEmpty, pipe, replace, T, trim } from 'ramda';
2023-02-18 12:40:37 +03:00
import type { FC } from 'react';
import { useEffect, useState } from 'react';
2021-03-20 13:18:00 +03:00
import { Button, FormGroup, Input, Row } from 'reactstrap';
2023-02-18 13:11:01 +03:00
import type { InputType } from 'reactstrap/types/lib/Input';
import type { DomainSelectorProps } from '../domains/DomainSelector';
import type { SelectedServer } from '../servers/data';
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { Checkbox } from '../utils/Checkbox';
2023-02-18 12:40:37 +03:00
import type { DateTimeInputProps } from '../utils/dates/DateTimeInput';
import { DateTimeInput } from '../utils/dates/DateTimeInput';
2023-02-18 13:11:01 +03:00
import { formatIsoDate } from '../utils/helpers/date';
import { supportsForwardQuery } from '../utils/helpers/features';
import { SimpleCard } from '../utils/SimpleCard';
2023-02-18 12:40:37 +03:00
import type { OptionalString } from '../utils/utils';
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
import type { ShortUrlData } from './data';
import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
2023-02-18 13:11:01 +03:00
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
import './ShortUrlForm.scss';
export type Mode = 'create' | 'create-basic' | 'edit';
type DateFields = 'validSince' | 'validUntil';
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits' | 'title';
export interface ShortUrlFormProps {
mode: Mode;
saving: boolean;
initialState: ShortUrlData;
onSave: (shortUrlData: ShortUrlData) => Promise<unknown>;
selectedServer: SelectedServer;
}
2021-03-20 18:32:12 +03:00
const normalizeTag = pipe(trim, replace(/ /g, '-'));
2022-03-26 14:17:42 +03:00
const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date);
2021-03-20 18:32:12 +03:00
export const ShortUrlForm = (
TagsSelector: FC<TagsSelectorProps>,
DomainSelector: FC<DomainSelectorProps>,
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
2022-03-26 14:17:42 +03:00
const [shortUrlData, setShortUrlData] = useState(initialState);
2021-03-20 18:32:12 +03:00
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>([
2022-03-26 14:17:42 +03:00
[() => !hasNewTitle && !hadTitleOriginally, () => undefined],
[() => !hasNewTitle && hadTitleOriginally, () => null],
[T, () => shortUrlData.title],
]);
return matcher();
};
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(),
2021-03-20 18:32:12 +03:00
}).then(() => !isEdit && reset()).catch(() => {}));
useEffect(() => {
setShortUrlData(initialState);
2022-03-26 14:17:42 +03:00
}, [initialState]);
const renderOptionalInput = (
id: NonDateFields,
placeholder: string,
type: InputType = 'text',
props = {},
fromGroupProps = {},
) => (
<FormGroup {...fromGroupProps}>
<Input
id={id}
type={type}
placeholder={placeholder}
2021-03-20 18:32:12 +03:00
value={shortUrlData[id] ?? ''}
onChange={(e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value })}
{...props}
/>
</FormGroup>
);
2022-10-18 23:02:09 +03:00
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateTimeInputProps> = {}) => (
<DateTimeInput
selected={shortUrlData[id] ? toDate(shortUrlData[id] as string | Date) : null}
placeholderText={placeholder}
isClearable
onChange={(date) => setShortUrlData({ ...shortUrlData, [id]: date })}
{...props}
/>
);
const basicComponents = (
<>
<FormGroup>
<Input
bsSize="lg"
type="url"
placeholder="URL to be shortened"
required
value={shortUrlData.longUrl}
onChange={(e) => setShortUrlData({ ...shortUrlData, longUrl: e.target.value })}
/>
</FormGroup>
<Row>
{isBasicMode && renderOptionalInput('customSlug', 'Custom slug', 'text', { bsSize: 'lg' }, { className: 'col-lg-6' })}
2022-03-24 22:23:46 +03:00
<div className={isBasicMode ? 'col-lg-6 mb-3' : 'col-12'}>
<TagsSelector selectedTags={shortUrlData.tags ?? []} onChange={changeTags} />
</div>
</Row>
</>
);
const showForwardQueryControl = supportsForwardQuery(selectedServer);
return (
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
{isBasicMode && basicComponents}
{!isBasicMode && (
<>
<SimpleCard title="Main options" className="mb-3">
{basicComponents}
</SimpleCard>
2021-03-20 13:18:00 +03:00
<Row>
2022-05-01 12:00:46 +03:00
<div className="col-sm-6 mb-3">
<SimpleCard title="Customize the short URL">
{renderOptionalInput('title', 'Title')}
{!isEdit && (
<>
<Row>
<div className="col-lg-6">
{renderOptionalInput('customSlug', 'Custom slug', 'text', {
disabled: hasValue(shortUrlData.shortCodeLength),
})}
</div>
<div className="col-lg-6">
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
min: 4,
disabled: hasValue(shortUrlData.customSlug),
})}
</div>
</Row>
<DomainSelector
value={shortUrlData.domain}
onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })}
/>
</>
)}
</SimpleCard>
</div>
2022-05-01 12:00:46 +03:00
<div className="col-sm-6 mb-3">
<SimpleCard title="Limit access to the short URL">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
<div className="mb-3">
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? toDate(shortUrlData.validUntil) : undefined })}
</div>
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlData.validSince ? toDate(shortUrlData.validSince) : undefined })}
</SimpleCard>
</div>
2021-03-20 13:18:00 +03:00
</Row>
<Row>
2022-05-01 12:00:46 +03:00
<div className="col-sm-6 mb-3">
<SimpleCard title="Extra checks">
<ShortUrlFormCheckboxGroup
infoTooltip="If checked, Shlink will try to reach the long URL, failing in case it's not publicly accessible."
checked={shortUrlData.validateUrl}
onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })}
>
Validate URL
</ShortUrlFormCheckboxGroup>
{!isEdit && (
<p>
<Checkbox
inline
className="me-2"
checked={shortUrlData.findIfExists}
onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</p>
)}
</SimpleCard>
</div>
<div className="col-sm-6 mb-3">
<SimpleCard title="Configure behavior">
<ShortUrlFormCheckboxGroup
infoTooltip="This short URL will be included in the robots.txt for your Shlink instance, allowing web crawlers (like Google) to index it."
checked={shortUrlData.crawlable}
onChange={(crawlable) => setShortUrlData({ ...shortUrlData, crawlable })}
>
Make it crawlable
</ShortUrlFormCheckboxGroup>
{showForwardQueryControl && (
<ShortUrlFormCheckboxGroup
infoTooltip="When this short URL is visited, any query params appended to it will be forwarded to the long URL."
checked={shortUrlData.forwardQuery}
onChange={(forwardQuery) => setShortUrlData({ ...shortUrlData, forwardQuery })}
>
Forward query params on redirect
</ShortUrlFormCheckboxGroup>
)}
</SimpleCard>
</div>
</Row>
</>
)}
<div className="text-center">
<Button
outline
color="primary"
disabled={saving || isEmpty(shortUrlData.longUrl)}
className="btn-xs-block"
>
2021-03-20 18:32:12 +03:00
{saving ? 'Saving...' : 'Save'}
</Button>
</div>
</form>
);
};