Merge pull request #505 from acelaya-forks/feature/resettable-title

Ensured short URL title can be resetted after creation
This commit is contained in:
Alejandro Celaya 2021-10-17 12:39:45 +02:00 committed by GitHub
commit f19746cd58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 6 deletions

View file

@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## [3.3.2] - 2021-10-17
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#503](https://github.com/shlinkio/shlink-web-client/issues/503) Fixed short URLs title not being resettable after creation.
## [3.3.1] - 2021-09-27 ## [3.3.1] - 2021-09-27
### Added ### Added
* *Nothing* * *Nothing*

View file

@ -1,13 +1,13 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { InputType } from 'reactstrap/lib/Input'; 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 { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import DateInput, { DateInputProps } from '../utils/DateInput'; import DateInput, { DateInputProps } from '../utils/DateInput';
import { supportsCrawlableVisits, supportsShortUrlTitle } from '../utils/helpers/features'; import { supportsCrawlableVisits, supportsShortUrlTitle } from '../utils/helpers/features';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import { handleEventPreventingDefault, hasValue } from '../utils/utils'; import { handleEventPreventingDefault, hasValue, OptionalString } 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';
@ -40,14 +40,25 @@ export const ShortUrlForm = (
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => { ): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer }) => {
const [ shortUrlData, setShortUrlData ] = useState(initialState); const [ shortUrlData, setShortUrlData ] = useState(initialState);
const isEdit = mode === 'edit'; const isEdit = mode === 'edit';
const hadTitleOriginally = hasValue(initialState.title);
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) }); const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
const reset = () => setShortUrlData(initialState); 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 ],
]);
return matcher();
};
const submit = handleEventPreventingDefault(async () => onSave({ const submit = handleEventPreventingDefault(async () => onSave({
...shortUrlData, ...shortUrlData,
validSince: formatIsoDate(shortUrlData.validSince) ?? null, validSince: formatIsoDate(shortUrlData.validSince) ?? null,
validUntil: formatIsoDate(shortUrlData.validUntil) ?? null, validUntil: formatIsoDate(shortUrlData.validUntil) ?? null,
maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits), maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits),
title: !hasValue(shortUrlData.title) ? undefined : shortUrlData.title, title: resolveNewTitle(),
}).then(() => !isEdit && reset()).catch(() => {})); }).then(() => !isEdit && reset()).catch(() => {}));
useEffect(() => { useEffect(() => {

View file

@ -3,7 +3,7 @@ import { Nullable, OptionalString } from '../../utils/utils';
export interface EditShortUrlData { export interface EditShortUrlData {
longUrl?: string; longUrl?: string;
tags?: string[]; tags?: string[];
title?: string; title?: string | null;
validSince?: Date | string | null; validSince?: Date | string | null;
validUntil?: Date | string | null; validUntil?: Date | string | null;
maxVisits?: number | null; maxVisits?: number | null;

View file

@ -9,13 +9,14 @@ import { ShortUrlData } from '../../src/short-urls/data';
import { ReachableServer, SelectedServer } from '../../src/servers/data'; import { ReachableServer, SelectedServer } from '../../src/servers/data';
import { SimpleCard } from '../../src/utils/SimpleCard'; import { SimpleCard } from '../../src/utils/SimpleCard';
import { parseDate } from '../../src/utils/helpers/date'; import { parseDate } from '../../src/utils/helpers/date';
import { OptionalString } from '../../src/utils/utils';
describe('<ShortUrlForm />', () => { describe('<ShortUrlForm />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const TagsSelector = () => null; const TagsSelector = () => null;
const DomainSelector = () => null; const DomainSelector = () => null;
const createShortUrl = jest.fn(async () => Promise.resolve()); const createShortUrl = jest.fn(async () => Promise.resolve());
const createWrapper = (selectedServer: SelectedServer = null, mode: Mode = 'create') => { const createWrapper = (selectedServer: SelectedServer = null, mode: Mode = 'create', title?: OptionalString) => {
const ShortUrlForm = createShortUrlForm(TagsSelector, DomainSelector); const ShortUrlForm = createShortUrlForm(TagsSelector, DomainSelector);
wrapper = shallow( wrapper = shallow(
@ -23,7 +24,7 @@ describe('<ShortUrlForm />', () => {
selectedServer={selectedServer} selectedServer={selectedServer}
mode={mode} mode={mode}
saving={false} saving={false}
initialState={Mock.of<ShortUrlData>({ validateUrl: true, findIfExists: false })} initialState={Mock.of<ShortUrlData>({ validateUrl: true, findIfExists: false, title })}
onSave={createShortUrl} onSave={createShortUrl}
/>, />,
); );
@ -80,4 +81,26 @@ describe('<ShortUrlForm />', () => {
expect(cards).toHaveLength(expectedAmountOfCards); expect(cards).toHaveLength(expectedAmountOfCards);
}, },
); );
it.each([
[ null, 'new title', 'new title' ],
[ undefined, 'new title', 'new title' ],
[ '', 'new title', 'new title' ],
[ null, '', undefined ],
[ null, null, undefined ],
[ '', '', undefined ],
[ undefined, undefined, undefined ],
[ 'old title', null, null ],
[ 'old title', undefined, null ],
[ 'old title', '', null ],
])('sends expected title based on original and new values', (originalTitle, newTitle, expectedSentTitle) => {
const wrapper = createWrapper(Mock.of<ReachableServer>({ version: '2.6.0' }), 'create', originalTitle);
wrapper.find('#title').simulate('change', { target: { value: newTitle } });
wrapper.find('form').simulate('submit', { preventDefault: identity });
expect(createShortUrl).toHaveBeenCalledWith(expect.objectContaining({
title: expectedSentTitle,
}));
});
}); });