Added form to create short URLs to overview page

This commit is contained in:
Alejandro Celaya
2020-12-07 20:37:03 +01:00
parent 9b30a82a79
commit 17d5c4327b
5 changed files with 109 additions and 88 deletions

View File

@@ -7,6 +7,7 @@ import { prettify } from '../utils/helpers/numbers';
import { TagsList } from '../tags/reducers/tagsList'; import { TagsList } from '../tags/reducers/tagsList';
import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable'; import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
import { VisitsOverview } from '../visits/reducers/visitsOverview'; import { VisitsOverview } from '../visits/reducers/visitsOverview';
import { isServerWithId, SelectedServer } from './data'; import { isServerWithId, SelectedServer } from './data';
import './Overview.scss'; import './Overview.scss';
@@ -21,7 +22,10 @@ interface OverviewConnectProps {
loadVisitsOverview: Function; loadVisitsOverview: Function;
} }
export const Overview = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub(({ export const Overview = (
ShortUrlsTable: FC<ShortUrlsTableProps>,
CreateShortUrl: FC<CreateShortUrlProps>,
) => boundToMercureHub(({
shortUrlsList, shortUrlsList,
listShortUrls, listShortUrls,
listTags, listTags,
@@ -67,10 +71,12 @@ export const Overview = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMerc
</div> </div>
<Card className="mb-4"> <Card className="mb-4">
<CardHeader> <CardHeader>
Create short URL Create a short URL
<Link className="float-right" to={`/server/${serverId}/create-short-url`}>Advanced options &raquo;</Link> <Link className="float-right" to={`/server/${serverId}/create-short-url`}>Advanced options &raquo;</Link>
</CardHeader> </CardHeader>
<CardBody>Create</CardBody> <CardBody>
<CreateShortUrl basicMode />
</CardBody>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>

View File

@@ -44,7 +44,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton'); bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ])); bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable'); bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
bottle.decorator('Overview', connect( bottle.decorator('Overview', connect(
[ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview' ], [ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview' ],
[ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview' ], [ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview' ],

View File

@@ -17,15 +17,19 @@ import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult'; import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
import './CreateShortUrl.scss'; import './CreateShortUrl.scss';
const normalizeTag = pipe(trim, replace(/ /g, '-')); export interface CreateShortUrlProps {
basicMode?: boolean;
}
interface CreateShortUrlProps { interface CreateShortUrlConnectProps extends CreateShortUrlProps {
shortUrlCreationResult: ShortUrlCreation; shortUrlCreationResult: ShortUrlCreation;
selectedServer: SelectedServer; selectedServer: SelectedServer;
createShortUrl: Function; createShortUrl: (data: ShortUrlData) => Promise<void>;
resetCreateShortUrl: () => void; resetCreateShortUrl: () => void;
} }
export const normalizeTag = pipe(trim, replace(/ /g, '-'));
const initialState: ShortUrlData = { const initialState: ShortUrlData = {
longUrl: '', longUrl: '',
tags: [], tags: [],
@@ -47,7 +51,13 @@ const CreateShortUrl = (
CreateShortUrlResult: FC<CreateShortUrlResultProps>, CreateShortUrlResult: FC<CreateShortUrlResultProps>,
ForServerVersion: FC<Versions>, ForServerVersion: FC<Versions>,
DomainSelector: FC<DomainSelectorProps>, DomainSelector: FC<DomainSelectorProps>,
) => ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }: CreateShortUrlProps) => { ) => ({
createShortUrl,
shortUrlCreationResult,
resetCreateShortUrl,
selectedServer,
basicMode = false,
}: CreateShortUrlConnectProps) => {
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState); const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) }); const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
@@ -55,8 +65,8 @@ const CreateShortUrl = (
const save = handleEventPreventingDefault(() => { const save = handleEventPreventingDefault(() => {
const shortUrlData = { const shortUrlData = {
...shortUrlCreation, ...shortUrlCreation,
validSince: formatIsoDate(shortUrlCreation.validSince), validSince: formatIsoDate(shortUrlCreation.validSince) ?? undefined,
validUntil: formatIsoDate(shortUrlCreation.validUntil), validUntil: formatIsoDate(shortUrlCreation.validUntil) ?? undefined,
}; };
createShortUrl(shortUrlData).then(reset).catch(() => {}); createShortUrl(shortUrlData).then(reset).catch(() => {});
@@ -92,90 +102,94 @@ const CreateShortUrl = (
return ( return (
<form onSubmit={save}> <form onSubmit={save}>
<div className="form-group"> <FormGroup>
<input <Input
className="form-control form-control-lg" bsSize="lg"
type="url" type="url"
placeholder="Insert the URL to be shortened" placeholder="Insert the URL to be shortened"
required required
value={shortUrlCreation.longUrl} value={shortUrlCreation.longUrl}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, longUrl: e.target.value })} onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, longUrl: e.target.value })}
/> />
</div> </FormGroup>
<div className="form-group"> <FormGroup>
<TagsSelector tags={shortUrlCreation.tags ?? []} onChange={changeTags} /> <TagsSelector tags={shortUrlCreation.tags ?? []} onChange={changeTags} />
</div> </FormGroup>
<div className="row"> {!basicMode && (
<div className="col-sm-4"> <>
{renderOptionalInput('customSlug', 'Custom slug', 'text', { <div className="row">
disabled: hasValue(shortUrlCreation.shortCodeLength), <div className="col-sm-4">
})} {renderOptionalInput('customSlug', 'Custom slug', 'text', {
</div> disabled: hasValue(shortUrlCreation.shortCodeLength),
<div className="col-sm-4"> })}
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', { </div>
min: 4, <div className="col-sm-4">
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug), {renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
...disableShortCodeLength && { min: 4,
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length', disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
}, ...disableShortCodeLength && {
})} title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
</div> },
<div className="col-sm-4"> })}
{!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text', { </div>
disabled: disableDomain, <div className="col-sm-4">
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' }, {!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text', {
})} disabled: disableDomain,
{showDomainSelector && ( ...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
<FormGroup> })}
<DomainSelector {showDomainSelector && (
value={shortUrlCreation.domain} <FormGroup>
onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })} <DomainSelector
/> value={shortUrlCreation.domain}
</FormGroup> onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })}
)} />
</div> </FormGroup>
</div> )}
</div>
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-4">
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })}
</div>
<div className="col-sm-4">
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
</div>
</div>
<ForServerVersion minVersion="1.16.0">
<div className="mb-4 row">
<div className="col-sm-6 text-center text-sm-left mb-2 mb-sm-0">
<ForServerVersion minVersion="2.4.0">
<Checkbox
inline
checked={shortUrlCreation.validateUrl}
onChange={(validateUrl) => setShortUrlCreation({ ...shortUrlCreation, validateUrl })}
>
Validate URL
</Checkbox>
</ForServerVersion>
</div> </div>
<div className="col-sm-6 text-center text-sm-right">
<Checkbox <div className="row">
inline <div className="col-sm-4">
className="mr-2" {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
checked={shortUrlCreation.findIfExists} </div>
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })} <div className="col-sm-4">
> {renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })}
Use existing URL if found </div>
</Checkbox> <div className="col-sm-4">
<UseExistingIfFoundInfoIcon /> {renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
</div>
</div> </div>
</div>
</ForServerVersion> <ForServerVersion minVersion="1.16.0">
<div className="mb-4 row">
<div className="col-sm-6 text-center text-sm-left mb-2 mb-sm-0">
<ForServerVersion minVersion="2.4.0">
<Checkbox
inline
checked={shortUrlCreation.validateUrl}
onChange={(validateUrl) => setShortUrlCreation({ ...shortUrlCreation, validateUrl })}
>
Validate URL
</Checkbox>
</ForServerVersion>
</div>
<div className="col-sm-6 text-center text-sm-right">
<Checkbox
inline
className="mr-2"
checked={shortUrlCreation.findIfExists}
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</div>
</ForServerVersion>
</>
)}
<div className="text-right"> <div className="text-right">
<Button <Button

View File

@@ -25,10 +25,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList');
bottle.decorator('ShortUrls', connect([ 'shortUrlsList' ])); bottle.decorator('ShortUrls', connect([ 'shortUrlsList' ]));
// Services
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator', 'ForServerVersion');
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable'); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable');
bottle.decorator('ShortUrlsList', connect( bottle.decorator('ShortUrlsList', connect(
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo' ], [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo' ],
@@ -73,6 +69,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal); bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal);
bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ])); bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ]));
// Services
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator', 'ForServerVersion');
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
// Actions // Actions
bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient');
bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags);

View File

@@ -2,6 +2,7 @@ import { shallow, ShallowWrapper } from 'enzyme';
import moment from 'moment'; import moment from 'moment';
import { identity } from 'ramda'; import { identity } from 'ramda';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { Input } from 'reactstrap';
import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl'; import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl';
import DateInput from '../../src/utils/DateInput'; import DateInput from '../../src/utils/DateInput';
import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation'; import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
@@ -31,7 +32,7 @@ describe('<CreateShortUrl />', () => {
const validSince = moment('2017-01-01'); const validSince = moment('2017-01-01');
const validUntil = moment('2017-01-06'); const validUntil = moment('2017-01-06');
wrapper.find('.form-control-lg').simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } }); wrapper.find(Input).first().simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } });
wrapper.find('TagsSelector').simulate('change', [ 'tag_foo', 'tag_bar' ]); wrapper.find('TagsSelector').simulate('change', [ 'tag_foo', 'tag_bar' ]);
wrapper.find('#customSlug').simulate('change', { target: { value: 'my-slug' } }); wrapper.find('#customSlug').simulate('change', { target: { value: 'my-slug' } });
wrapper.find('#domain').simulate('change', { target: { value: 'example.com' } }); wrapper.find('#domain').simulate('change', { target: { value: 'example.com' } });