Updated ShortUrlForm to ensure it does not render empty cards

This commit is contained in:
Alejandro Celaya 2021-03-27 18:39:55 +01:00
parent 6628a4059e
commit 56aab349db
4 changed files with 118 additions and 78 deletions

View file

@ -3,25 +3,27 @@ import { InputType } from 'reactstrap/lib/Input';
import { Button, FormGroup, Input, Row } from 'reactstrap'; import { Button, FormGroup, Input, Row } from 'reactstrap';
import { isEmpty, pipe, replace, trim } from 'ramda'; import { isEmpty, pipe, replace, trim } from 'ramda';
import m from 'moment'; import m from 'moment';
import classNames from 'classnames';
import DateInput, { DateInputProps } from '../utils/DateInput'; import DateInput, { DateInputProps } from '../utils/DateInput';
import { import {
supportsListingDomains, supportsListingDomains,
supportsSettingShortCodeLength, supportsSettingShortCodeLength,
supportsShortUrlTitle, supportsShortUrlTitle,
supportsValidateUrl,
} from '../utils/helpers/features'; } from '../utils/helpers/features';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import { handleEventPreventingDefault, hasValue } from '../utils/utils'; import { handleEventPreventingDefault, hasValue } from '../utils/utils';
import Checkbox from '../utils/Checkbox'; import Checkbox from '../utils/Checkbox';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { Versions } from '../utils/helpers/version';
import { DomainSelectorProps } from '../domains/DomainSelector'; import { DomainSelectorProps } from '../domains/DomainSelector';
import { formatIsoDate } from '../utils/helpers/date'; import { formatIsoDate } from '../utils/helpers/date';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
import { ShortUrlData } from './data'; import { ShortUrlData } from './data';
import './ShortUrlForm.scss'; import './ShortUrlForm.scss';
type Mode = 'create' | 'create-basic' | 'edit'; export type Mode = 'create' | 'create-basic' | 'edit';
type DateFields = 'validSince' | 'validUntil'; type DateFields = 'validSince' | 'validUntil';
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits' | 'title'; type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits' | 'title';
@ -37,7 +39,6 @@ const normalizeTag = pipe(trim, replace(/ /g, '-'));
export const ShortUrlForm = ( export const ShortUrlForm = (
TagsSelector: FC<TagsSelectorProps>, TagsSelector: FC<TagsSelectorProps>,
ForServerVersion: FC<Versions>,
DomainSelector: FC<DomainSelectorProps>, DomainSelector: FC<DomainSelectorProps>,
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => { // eslint-disable-line complexity ): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => { // eslint-disable-line complexity
const [ shortUrlData, setShortUrlData ] = useState(initialState); const [ shortUrlData, setShortUrlData ] = useState(initialState);
@ -101,6 +102,13 @@ export const ShortUrlForm = (
const showDomainSelector = supportsListingDomains(selectedServer); const showDomainSelector = supportsListingDomains(selectedServer);
const disableShortCodeLength = !supportsSettingShortCodeLength(selectedServer); const disableShortCodeLength = !supportsSettingShortCodeLength(selectedServer);
const supportsTitle = supportsShortUrlTitle(selectedServer); const supportsTitle = supportsShortUrlTitle(selectedServer);
const showCustomizeCard = supportsTitle || !isEdit;
const limitAccessCardClasses = classNames('mb-3', {
'col-sm-6': showCustomizeCard,
'col-sm-12': !showCustomizeCard,
});
const showValidateUrl = supportsValidateUrl(selectedServer);
const showExtraValidationsCard = showValidateUrl || !isEdit;
return ( return (
<form className="short-url-form" onSubmit={submit}> <form className="short-url-form" onSubmit={submit}>
@ -112,42 +120,44 @@ export const ShortUrlForm = (
</SimpleCard> </SimpleCard>
<Row> <Row>
<div className="col-sm-6 mb-3"> {showCustomizeCard && (
<SimpleCard title="Customize the short URL"> <div className="col-sm-6 mb-3">
{supportsTitle && renderOptionalInput('title', 'Title')} <SimpleCard title="Customize the short URL">
{!isEdit && ( {supportsTitle && renderOptionalInput('title', 'Title')}
<> {!isEdit && (
<Row> <>
<div className="col-lg-6"> <Row>
{renderOptionalInput('customSlug', 'Custom slug', 'text', { <div className="col-lg-6">
disabled: hasValue(shortUrlData.shortCodeLength), {renderOptionalInput('customSlug', 'Custom slug', 'text', {
})} disabled: hasValue(shortUrlData.shortCodeLength),
</div> })}
<div className="col-lg-6"> </div>
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', { <div className="col-lg-6">
min: 4, {renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
disabled: disableShortCodeLength || hasValue(shortUrlData.customSlug), min: 4,
...disableShortCodeLength && { disabled: disableShortCodeLength || hasValue(shortUrlData.customSlug),
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length', ...disableShortCodeLength && {
}, title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
})} },
</div> })}
</Row> </div>
{!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text')} </Row>
{showDomainSelector && ( {!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text')}
<FormGroup> {showDomainSelector && (
<DomainSelector <FormGroup>
value={shortUrlData.domain} <DomainSelector
onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })} value={shortUrlData.domain}
/> onChange={(domain?: string) => setShortUrlData({ ...shortUrlData, domain })}
</FormGroup> />
)} </FormGroup>
</> )}
)} </>
</SimpleCard> )}
</div> </SimpleCard>
</div>
)}
<div className="col-sm-6 mb-3"> <div className={limitAccessCardClasses}>
<SimpleCard title="Limit access to the short URL"> <SimpleCard title="Limit access to the short URL">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })} {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? m(shortUrlData.validUntil) : undefined })} {renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? m(shortUrlData.validUntil) : undefined })}
@ -156,38 +166,40 @@ export const ShortUrlForm = (
</div> </div>
</Row> </Row>
<SimpleCard title="Extra validations" className="mb-3"> {showExtraValidationsCard && (
{!isEdit && ( <SimpleCard title="Extra validations" className="mb-3">
<p> {!isEdit && (
Make sure the long URL is valid, or ensure an existing short URL is returned if it matches all <p>
provided data. Make sure the long URL is valid, or ensure an existing short URL is returned if it matches all
</p> provided data.
)} </p>
<ForServerVersion minVersion="2.4.0"> )}
<p> {showValidateUrl && (
<Checkbox <p>
inline <Checkbox
checked={shortUrlData.validateUrl} inline
onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })} checked={shortUrlData.validateUrl}
> onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })}
Validate URL >
</Checkbox> Validate URL
</p> </Checkbox>
</ForServerVersion> </p>
{!isEdit && ( )}
<p> {!isEdit && (
<Checkbox <p>
inline <Checkbox
className="mr-2" inline
checked={shortUrlData.findIfExists} className="mr-2"
onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })} checked={shortUrlData.findIfExists}
> onChange={(findIfExists) => setShortUrlData({ ...shortUrlData, findIfExists })}
Use existing URL if found >
</Checkbox> Use existing URL if found
<UseExistingIfFoundInfoIcon /> </Checkbox>
</p> <UseExistingIfFoundInfoIcon />
)} </p>
</SimpleCard> )}
</SimpleCard>
)}
</> </>
)} )}

View file

@ -34,7 +34,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout'); bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout');
bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'QrCodeModal'); bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'QrCodeModal');
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout'); bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
bottle.serviceFactory('ShortUrlForm', ShortUrlForm, 'TagsSelector', 'ForServerVersion', 'DomainSelector'); bottle.serviceFactory('ShortUrlForm', ShortUrlForm, 'TagsSelector', 'DomainSelector');
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult'); bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'ShortUrlForm', 'CreateShortUrlResult');
bottle.decorator( bottle.decorator(

View file

@ -12,6 +12,8 @@ export const supportsListingDomains = serverMatchesVersions({ minVersion: '2.4.0
export const supportsQrCodeSvgFormat = supportsListingDomains; export const supportsQrCodeSvgFormat = supportsListingDomains;
export const supportsValidateUrl = supportsListingDomains;
export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' }); export const supportsQrCodeSizeInQuery = serverMatchesVersions({ minVersion: '2.5.0' });
export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' }); export const supportsShortUrlTitle = serverMatchesVersions({ minVersion: '2.6.0' });

View file

@ -3,32 +3,37 @@ 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 { Input } from 'reactstrap';
import { ShortUrlForm as createShortUrlForm } from '../../src/short-urls/ShortUrlForm'; import { ShortUrlForm as createShortUrlForm, Mode } from '../../src/short-urls/ShortUrlForm';
import DateInput from '../../src/utils/DateInput'; import DateInput from '../../src/utils/DateInput';
import { ShortUrlData } from '../../src/short-urls/data'; import { ShortUrlData } from '../../src/short-urls/data';
import { ReachableServer, SelectedServer } from '../../src/servers/data';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<ShortUrlForm />', () => { describe('<ShortUrlForm />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const TagsSelector = () => null; const TagsSelector = () => null;
const createShortUrl = jest.fn(); const createShortUrl = jest.fn();
const createWrapper = (selectedServer: SelectedServer = null, mode: Mode = 'create') => {
beforeEach(() => { const ShortUrlForm = createShortUrlForm(TagsSelector, () => null);
const ShortUrlForm = createShortUrlForm(TagsSelector, () => null, () => null);
wrapper = shallow( wrapper = shallow(
<ShortUrlForm <ShortUrlForm
selectedServer={null} selectedServer={selectedServer}
mode="create" mode={mode}
saving={false} saving={false}
initialState={Mock.of<ShortUrlData>({ validateUrl: true, findIfExists: false })} initialState={Mock.of<ShortUrlData>({ validateUrl: true, findIfExists: false })}
onSave={createShortUrl} onSave={createShortUrl}
/>, />,
); );
});
return wrapper;
};
afterEach(() => wrapper.unmount()); afterEach(() => wrapper.unmount());
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
it('saves short URL with data set in form controls', () => { it('saves short URL with data set in form controls', () => {
const wrapper = createWrapper();
const validSince = moment('2017-01-01'); const validSince = moment('2017-01-01');
const validUntil = moment('2017-01-06'); const validUntil = moment('2017-01-06');
@ -56,4 +61,25 @@ describe('<ShortUrlForm />', () => {
validateUrl: true, validateUrl: true,
}); });
}); });
it.each([
[ null, 'create' as Mode, 4 ],
[ null, 'create-basic' as Mode, 0 ],
[ Mock.of<ReachableServer>({ version: '2.6.0' }), 'create' as Mode, 4 ],
[ Mock.of<ReachableServer>({ version: '2.5.0' }), 'create' as Mode, 4 ],
[ Mock.of<ReachableServer>({ version: '2.4.0' }), 'create' as Mode, 4 ],
[ Mock.of<ReachableServer>({ version: '2.3.0' }), 'create' as Mode, 4 ],
[ Mock.of<ReachableServer>({ version: '2.6.0' }), 'edit' as Mode, 4 ],
[ Mock.of<ReachableServer>({ version: '2.5.0' }), 'edit' as Mode, 3 ],
[ Mock.of<ReachableServer>({ version: '2.4.0' }), 'edit' as Mode, 3 ],
[ Mock.of<ReachableServer>({ version: '2.3.0' }), 'edit' as Mode, 2 ],
])(
'renders expected amount of cards based on server capabilities and mode',
(selectedServer, mode, expectedAmountOfCards) => {
const wrapper = createWrapper(selectedServer, mode);
const cards = wrapper.find(SimpleCard);
expect(cards).toHaveLength(expectedAmountOfCards);
},
);
}); });