Merge pull request #502 from acelaya-forks/feature/forward-query

Feature/forward query
This commit is contained in:
Alejandro Celaya 2021-10-13 23:17:52 +02:00 committed by GitHub
commit f4908cacc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 48 deletions

View file

@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased] ## [Unreleased]
### Added ### Added
* [#496](https://github.com/shlinkio/shlink-web-client/issues/496) Allowed to select "all visits" as the default interval for visits. * [#496](https://github.com/shlinkio/shlink-web-client/issues/496) Allowed to select "all visits" as the default interval for visits.
* [#500](https://github.com/shlinkio/shlink-web-client/issues/500) Allowed to set the `forwardQuery` flag when creating/editing short URLs on a Shlink v2.9.0 server.
### Changed ### Changed
* *Nothing* * *Nothing*

View file

@ -14,8 +14,8 @@ const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): s
tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input'; tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input';
const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode => const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode =>
tagFilteringMode === 'includes' tagFilteringMode === 'includes'
? <>The list of suggested tags will contain existing ones <b>including</b> provided input.</> ? <>The list of suggested tags will contain those <b>including</b> provided input.</>
: <>The list of suggested tags will contain existing ones <b>starting with</b> provided input.</>; : <>The list of suggested tags will contain those <b>starting with</b> provided input.</>;
export const ShortUrlCreation: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => { export const ShortUrlCreation: FC<ShortUrlCreationProps> = ({ settings, setShortUrlCreationSettings }) => {
const shortUrlCreation: ShortUrlCreationSettings = settings.shortUrlCreation ?? { validateUrls: false }; const shortUrlCreation: ShortUrlCreationSettings = settings.shortUrlCreation ?? { validateUrls: false };
@ -24,19 +24,31 @@ export const ShortUrlCreation: FC<ShortUrlCreationProps> = ({ settings, setShort
); );
return ( return (
<SimpleCard title="Short URLs creation" className="h-100"> <SimpleCard title="Short URLs form" className="h-100">
<FormGroup> <FormGroup>
<ToggleSwitch <ToggleSwitch
checked={shortUrlCreation.validateUrls ?? false} checked={shortUrlCreation.validateUrls ?? false}
onChange={(validateUrls) => setShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })} onChange={(validateUrls) => setShortUrlCreationSettings({ ...shortUrlCreation, validateUrls })}
> >
By default, request validation on long URLs when creating new short URLs. Request validation on long URLs when creating new short URLs.
<small className="form-text text-muted"> <small className="form-text text-muted">
The initial state of the <b>Validate URL</b> checkbox will The initial state of the <b>Validate URL</b> checkbox will
be <b>{shortUrlCreation.validateUrls ? 'checked' : 'unchecked'}</b>. be <b>{shortUrlCreation.validateUrls ? 'checked' : 'unchecked'}</b>.
</small> </small>
</ToggleSwitch> </ToggleSwitch>
</FormGroup> </FormGroup>
<FormGroup>
<ToggleSwitch
checked={shortUrlCreation.forwardQuery ?? true}
onChange={(forwardQuery) => setShortUrlCreationSettings({ ...shortUrlCreation, forwardQuery })}
>
Make all new short URLs forward their query params to the long URL.
<small className="form-text text-muted">
The initial state of the <b>Forward query params on redirect</b> checkbox will
be <b>{shortUrlCreation.forwardQuery ?? true ? 'checked' : 'unchecked'}</b>.
</small>
</ToggleSwitch>
</FormGroup>
<FormGroup className="mb-0"> <FormGroup className="mb-0">
<label>Tag suggestions search mode:</label> <label>Tag suggestions search mode:</label>
<DropdownBtn text={tagFilteringModeText(shortUrlCreation.tagFilteringMode)}> <DropdownBtn text={tagFilteringModeText(shortUrlCreation.tagFilteringMode)}>

View file

@ -22,6 +22,7 @@ export type TagFilteringMode = 'startsWith' | 'includes';
export interface ShortUrlCreationSettings { export interface ShortUrlCreationSettings {
validateUrls: boolean; validateUrls: boolean;
tagFilteringMode?: TagFilteringMode; tagFilteringMode?: TagFilteringMode;
forwardQuery?: boolean;
} }
export type TagsMode = 'cards' | 'list'; export type TagsMode = 'cards' | 'list';

View file

@ -30,6 +30,7 @@ const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => (
maxVisits: undefined, maxVisits: undefined,
findIfExists: false, findIfExists: false,
validateUrl: settings?.validateUrls ?? false, validateUrl: settings?.validateUrls ?? false,
forwardQuery: settings?.forwardQuery ?? true,
}); });
const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResult: FC<CreateShortUrlResultProps>) => ({ const CreateShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>, CreateShortUrlResult: FC<CreateShortUrlResultProps>) => ({

View file

@ -42,6 +42,7 @@ const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSetting
validUntil: shortUrl.meta.validUntil ?? undefined, validUntil: shortUrl.meta.validUntil ?? undefined,
maxVisits: shortUrl.meta.maxVisits ?? undefined, maxVisits: shortUrl.meta.maxVisits ?? undefined,
crawlable: shortUrl.crawlable, crawlable: shortUrl.crawlable,
forwardQuery: shortUrl.forwardQuery,
validateUrl, validateUrl,
}; };
}; };

View file

@ -5,7 +5,7 @@ import { isEmpty, pipe, replace, trim } 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, supportsForwardQuery, supportsShortUrlTitle } 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';
@ -33,6 +33,7 @@ export interface ShortUrlFormProps {
const normalizeTag = pipe(trim, replace(/ /g, '-')); const normalizeTag = pipe(trim, replace(/ /g, '-'));
const toDate = (date?: string | Date): Date | undefined => typeof date === 'string' ? parseISO(date) : date; const toDate = (date?: string | Date): Date | undefined => typeof date === 'string' ? parseISO(date) : date;
const dynamicColClasses = (flag: boolean) => ({ 'col-sm-6': flag, 'col-sm-12': !flag });
export const ShortUrlForm = ( export const ShortUrlForm = (
TagsSelector: FC<TagsSelectorProps>, TagsSelector: FC<TagsSelectorProps>,
@ -98,11 +99,11 @@ export const ShortUrlForm = (
const supportsTitle = supportsShortUrlTitle(selectedServer); const supportsTitle = supportsShortUrlTitle(selectedServer);
const showCustomizeCard = supportsTitle || !isEdit; const showCustomizeCard = supportsTitle || !isEdit;
const limitAccessCardClasses = classNames('mb-3', { const limitAccessCardClasses = classNames('mb-3', dynamicColClasses(showCustomizeCard));
'col-sm-6': showCustomizeCard,
'col-sm-12': !showCustomizeCard,
});
const showCrawlableControl = supportsCrawlableVisits(selectedServer); const showCrawlableControl = supportsCrawlableVisits(selectedServer);
const showForwardQueryControl = supportsForwardQuery(selectedServer);
const showBehaviorCard = showCrawlableControl || showForwardQueryControl;
const extraChecksCardClasses = classNames('mb-3', dynamicColClasses(showBehaviorCard));
return ( return (
<form className="short-url-form" onSubmit={submit}> <form className="short-url-form" onSubmit={submit}>
@ -154,7 +155,9 @@ export const ShortUrlForm = (
</div> </div>
</Row> </Row>
<SimpleCard title="Extra checks" className="mb-3"> <Row>
<div className={extraChecksCardClasses}>
<SimpleCard title="Extra checks">
<ShortUrlFormCheckboxGroup <ShortUrlFormCheckboxGroup
infoTooltip="If checked, Shlink will try to reach the long URL, failing in case it's not publicly accessible." infoTooltip="If checked, Shlink will try to reach the long URL, failing in case it's not publicly accessible."
checked={shortUrlData.validateUrl} checked={shortUrlData.validateUrl}
@ -162,15 +165,6 @@ export const ShortUrlForm = (
> >
Validate URL Validate URL
</ShortUrlFormCheckboxGroup> </ShortUrlFormCheckboxGroup>
{showCrawlableControl && (
<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>
)}
{!isEdit && ( {!isEdit && (
<p> <p>
<Checkbox <Checkbox
@ -185,6 +179,32 @@ export const ShortUrlForm = (
</p> </p>
)} )}
</SimpleCard> </SimpleCard>
</div>
{showBehaviorCard && (
<div className="col-sm-6 mb-3">
<SimpleCard title="Configure behavior">
{showCrawlableControl && (
<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>
</> </>
)} )}

View file

@ -9,6 +9,7 @@ export interface EditShortUrlData {
maxVisits?: number | null; maxVisits?: number | null;
validateUrl?: boolean; validateUrl?: boolean;
crawlable?: boolean; crawlable?: boolean;
forwardQuery?: boolean;
} }
export interface ShortUrlData extends EditShortUrlData { export interface ShortUrlData extends EditShortUrlData {
@ -30,6 +31,7 @@ export interface ShortUrl {
domain: string | null; domain: string | null;
title?: string | null; title?: string | null;
crawlable?: boolean; crawlable?: boolean;
forwardQuery?: boolean;
} }
export interface ShortUrlMeta { export interface ShortUrlMeta {

View file

@ -21,3 +21,5 @@ export const supportsCrawlableVisits = supportsBotVisits;
export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' }); export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' });
export const supportsDomainRedirects = supportsQrErrorCorrection; export const supportsDomainRedirects = supportsQrErrorCorrection;
export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' });

View file

@ -29,20 +29,42 @@ describe('<ShortUrlCreation />', () => {
[ undefined, false ], [ undefined, false ],
])('URL validation switch is toggled if option is true', (shortUrlCreation, expectedChecked) => { ])('URL validation switch is toggled if option is true', (shortUrlCreation, expectedChecked) => {
const wrapper = createWrapper(shortUrlCreation); const wrapper = createWrapper(shortUrlCreation);
const toggle = wrapper.find(ToggleSwitch); const urlValidationToggle = wrapper.find(ToggleSwitch).first();
expect(toggle.prop('checked')).toEqual(expectedChecked); expect(urlValidationToggle.prop('checked')).toEqual(expectedChecked);
}); });
it.each([ it.each([
[{ validateUrls: true }, 'checkbox will be checked' ], [{ forwardQuery: true }, true ],
[{ validateUrls: false }, 'checkbox will be unchecked' ], [{ forwardQuery: false }, false ],
[ undefined, 'checkbox will be unchecked' ], [{}, true ],
])('forward query switch is toggled if option is true', (shortUrlCreation, expectedChecked) => {
const wrapper = createWrapper({ validateUrls: true, ...shortUrlCreation });
const forwardQueryToggle = wrapper.find(ToggleSwitch).last();
expect(forwardQueryToggle.prop('checked')).toEqual(expectedChecked);
});
it.each([
[{ validateUrls: true }, 'Validate URL checkbox will be checked' ],
[{ validateUrls: false }, 'Validate URL checkbox will be unchecked' ],
[ undefined, 'Validate URL checkbox will be unchecked' ],
])('shows expected helper text for URL validation', (shortUrlCreation, expectedText) => { ])('shows expected helper text for URL validation', (shortUrlCreation, expectedText) => {
const wrapper = createWrapper(shortUrlCreation); const wrapper = createWrapper(shortUrlCreation);
const text = wrapper.find('.form-text').first(); const validateUrlText = wrapper.find('.form-text').first();
expect(text.text()).toContain(expectedText); expect(validateUrlText.text()).toContain(expectedText);
});
it.each([
[{ forwardQuery: true }, 'Forward query params on redirect checkbox will be checked' ],
[{ forwardQuery: false }, 'Forward query params on redirect checkbox will be unchecked' ],
[{}, 'Forward query params on redirect checkbox will be checked' ],
])('shows expected helper text for query forwarding', (shortUrlCreation, expectedText) => {
const wrapper = createWrapper({ validateUrls: true, ...shortUrlCreation });
const forwardQueryText = wrapper.find('.form-text').at(1);
expect(forwardQueryText.text()).toContain(expectedText);
}); });
it.each([ it.each([
@ -62,15 +84,24 @@ describe('<ShortUrlCreation />', () => {
expect(hintText.text()).toContain(expectedHint); expect(hintText.text()).toContain(expectedHint);
}); });
it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when toggle value changes', (validateUrls) => { it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when URL validation toggle value changes', (validateUrls) => {
const wrapper = createWrapper(); const wrapper = createWrapper();
const toggle = wrapper.find(ToggleSwitch); const urlValidationToggle = wrapper.find(ToggleSwitch).first();
expect(setShortUrlCreationSettings).not.toHaveBeenCalled(); expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
toggle.simulate('change', validateUrls); urlValidationToggle.simulate('change', validateUrls);
expect(setShortUrlCreationSettings).toHaveBeenCalledWith({ validateUrls }); expect(setShortUrlCreationSettings).toHaveBeenCalledWith({ validateUrls });
}); });
it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when forward query toggle value changes', (forwardQuery) => {
const wrapper = createWrapper();
const urlValidationToggle = wrapper.find(ToggleSwitch).last();
expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
urlValidationToggle.simulate('change', forwardQuery);
expect(setShortUrlCreationSettings).toHaveBeenCalledWith(expect.objectContaining({ forwardQuery }));
});
it('invokes setShortUrlCreationSettings when dropdown value changes', () => { it('invokes setShortUrlCreationSettings when dropdown value changes', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
const firstDropdownItem = wrapper.find(DropdownItem).first(); const firstDropdownItem = wrapper.find(DropdownItem).first();