diff --git a/CHANGELOG.md b/CHANGELOG.md
index 448a383b..4abdf179 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
### Added
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard.
+* [#385](https://github.com/shlinkio/shlink-web-client/issues/385) Added setting to determine if "validate URL" should be enabled or disabled by default.
### Changed
* *Nothing*
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index d2813af9..bbd524d9 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -1,9 +1,17 @@
import { FC } from 'react';
+import { Row } from 'reactstrap';
import NoMenuLayout from '../common/NoMenuLayout';
-const Settings = (RealTimeUpdates: FC) => () => (
+const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC) => () => (
-
+
+
+
+
+
+
+
+
);
diff --git a/src/settings/ShortUrlCreation.tsx b/src/settings/ShortUrlCreation.tsx
new file mode 100644
index 00000000..afd88c12
--- /dev/null
+++ b/src/settings/ShortUrlCreation.tsx
@@ -0,0 +1,29 @@
+import { FC } from 'react';
+import { FormGroup } from 'reactstrap';
+import { SimpleCard } from '../utils/SimpleCard';
+import ToggleSwitch from '../utils/ToggleSwitch';
+import { Settings, ShortUrlCreationSettings } from './reducers/settings';
+
+interface ShortUrlCreationProps {
+ settings: Settings;
+ setShortUrlCreationSettings: (settings: ShortUrlCreationSettings) => void;
+}
+
+export const ShortUrlCreation: FC = (
+ { settings: { shortUrlCreation }, setShortUrlCreationSettings },
+) => (
+
+
+ setShortUrlCreationSettings({ validateUrls })}
+ >
+ By default, request validation on long URLs when creating new short URLs.
+
+ The initial state of the Validate URL checkbox will
+ be {shortUrlCreation?.validateUrls ? 'checked' : 'unchecked'}.
+
+
+
+
+);
diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts
index 8a21d146..aa1bc929 100644
--- a/src/settings/reducers/settings.ts
+++ b/src/settings/reducers/settings.ts
@@ -1,23 +1,36 @@
import { Action } from 'redux';
-import { mergeDeepRight } from 'ramda';
+import { dissoc, mergeDeepRight } from 'ramda';
import { buildReducer } from '../../utils/helpers/redux';
import { RecursivePartial } from '../../utils/utils';
-export const SET_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/SET_REAL_TIME_UPDATES';
+export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
-interface RealTimeUpdates {
+/**
+ * Important! When adding new props in the main Settings interface or any of the nested props, they have to be set as
+ * optional, as old instances of the app will load partial objects from local storage until it is saved again.
+ */
+
+interface RealTimeUpdatesSettings {
enabled: boolean;
interval?: number;
}
+export interface ShortUrlCreationSettings {
+ validateUrls: boolean;
+}
+
export interface Settings {
- realTimeUpdates: RealTimeUpdates;
+ realTimeUpdates: RealTimeUpdatesSettings;
+ shortUrlCreation?: ShortUrlCreationSettings;
}
const initialState: Settings = {
realTimeUpdates: {
enabled: true,
},
+ shortUrlCreation: {
+ validateUrls: false,
+ },
};
type SettingsAction = Action & Settings;
@@ -25,15 +38,20 @@ type SettingsAction = Action & Settings;
type PartialSettingsAction = Action & RecursivePartial;
export default buildReducer({
- [SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => mergeDeepRight(state, { realTimeUpdates }),
+ [SET_SETTINGS]: (state, action) => mergeDeepRight(state, dissoc('type', action)),
}, initialState);
export const toggleRealTimeUpdates = (enabled: boolean): PartialSettingsAction => ({
- type: SET_REAL_TIME_UPDATES,
+ type: SET_SETTINGS,
realTimeUpdates: { enabled },
});
export const setRealTimeUpdatesInterval = (interval: number): PartialSettingsAction => ({
- type: SET_REAL_TIME_UPDATES,
+ type: SET_SETTINGS,
realTimeUpdates: { interval },
});
+
+export const setShortUrlCreationSettings = (settings: ShortUrlCreationSettings): PartialSettingsAction => ({
+ type: SET_SETTINGS,
+ shortUrlCreation: settings,
+});
diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts
index 5da9eca1..393ccefc 100644
--- a/src/settings/services/provideServices.ts
+++ b/src/settings/services/provideServices.ts
@@ -1,26 +1,30 @@
import Bottle from 'bottlejs';
import RealTimeUpdates from '../RealTimeUpdates';
import Settings from '../Settings';
-import { setRealTimeUpdatesInterval, toggleRealTimeUpdates } from '../reducers/settings';
+import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, toggleRealTimeUpdates } from '../reducers/settings';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
+import { ShortUrlCreation } from '../ShortUrlCreation';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
- bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates');
+ bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation');
bottle.decorator('Settings', withoutSelectedServer);
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
- // Services
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
bottle.decorator(
'RealTimeUpdates',
connect([ 'settings' ], [ 'toggleRealTimeUpdates', 'setRealTimeUpdatesInterval' ]),
);
+ bottle.serviceFactory('ShortUrlCreation', () => ShortUrlCreation);
+ bottle.decorator('ShortUrlCreation', connect([ 'settings' ], [ 'setShortUrlCreationSettings' ]));
+
// Actions
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
+ bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
};
export default provideServices;
diff --git a/src/short-urls/CreateShortUrl.tsx b/src/short-urls/CreateShortUrl.tsx
index 64c3926f..7bf2fe1f 100644
--- a/src/short-urls/CreateShortUrl.tsx
+++ b/src/short-urls/CreateShortUrl.tsx
@@ -1,5 +1,5 @@
import { isEmpty, pipe, replace, trim } from 'ramda';
-import { FC, useState } from 'react';
+import { FC, useMemo, useState } from 'react';
import { Button, FormGroup, Input } from 'reactstrap';
import { InputType } from 'reactstrap/lib/Input';
import * as m from 'moment';
@@ -12,6 +12,7 @@ import { formatIsoDate } from '../utils/helpers/date';
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { DomainSelectorProps } from '../domains/DomainSelector';
import { SimpleCard } from '../utils/SimpleCard';
+import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
import { ShortUrlData } from './data';
import { ShortUrlCreation } from './reducers/shortUrlCreation';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
@@ -23,6 +24,7 @@ export interface CreateShortUrlProps {
}
interface CreateShortUrlConnectProps extends CreateShortUrlProps {
+ settings: Settings;
shortUrlCreationResult: ShortUrlCreation;
selectedServer: SelectedServer;
createShortUrl: (data: ShortUrlData) => Promise;
@@ -31,7 +33,7 @@ interface CreateShortUrlConnectProps extends CreateShortUrlProps {
export const normalizeTag = pipe(trim, replace(/ /g, '-'));
-const initialState: ShortUrlData = {
+const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => ({
longUrl: '',
tags: [],
customSlug: '',
@@ -41,8 +43,8 @@ const initialState: ShortUrlData = {
validUntil: undefined,
maxVisits: undefined,
findIfExists: false,
- validateUrl: true,
-};
+ validateUrl: settings?.validateUrls ?? false,
+});
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits';
type DateFields = 'validSince' | 'validUntil';
@@ -58,9 +60,10 @@ const CreateShortUrl = (
resetCreateShortUrl,
selectedServer,
basicMode = false,
+ settings: { shortUrlCreation: shortUrlCreationSettings },
}: CreateShortUrlConnectProps) => {
+ const initialState = useMemo(() => getInitialState(shortUrlCreationSettings), [ shortUrlCreationSettings ]);
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
-
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
const reset = () => setShortUrlCreation(initialState);
const save = handleEventPreventingDefault(() => {
diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts
index 93d987b0..e40fd84f 100644
--- a/src/short-urls/services/provideServices.ts
+++ b/src/short-urls/services/provideServices.ts
@@ -56,7 +56,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
);
bottle.decorator(
'CreateShortUrl',
- connect([ 'shortUrlCreationResult', 'selectedServer' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
+ connect([ 'shortUrlCreationResult', 'selectedServer', 'settings' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
);
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
diff --git a/test/settings/ShortUrlCreation.test.tsx b/test/settings/ShortUrlCreation.test.tsx
new file mode 100644
index 00000000..07d0bd7b
--- /dev/null
+++ b/test/settings/ShortUrlCreation.test.tsx
@@ -0,0 +1,54 @@
+import { shallow, ShallowWrapper } from 'enzyme';
+import { Mock } from 'ts-mockery';
+import { ShortUrlCreationSettings, Settings } from '../../src/settings/reducers/settings';
+import { ShortUrlCreation } from '../../src/settings/ShortUrlCreation';
+import ToggleSwitch from '../../src/utils/ToggleSwitch';
+
+describe('', () => {
+ let wrapper: ShallowWrapper;
+ const setShortUrlCreationSettings = jest.fn();
+ const createWrapper = (shortUrlCreation?: ShortUrlCreationSettings) => {
+ wrapper = shallow(
+ ({ shortUrlCreation })}
+ setShortUrlCreationSettings={setShortUrlCreationSettings}
+ />,
+ );
+
+ return wrapper;
+ };
+
+ afterEach(() => wrapper?.unmount());
+ afterEach(jest.clearAllMocks);
+
+ it.each([
+ [{ validateUrls: true }, true ],
+ [{ validateUrls: false }, false ],
+ [ undefined, false ],
+ ])('switch is toggled if option is tru', (shortUrlCreation, expectedChecked) => {
+ const wrapper = createWrapper(shortUrlCreation);
+ const toggle = wrapper.find(ToggleSwitch);
+
+ expect(toggle.prop('checked')).toEqual(expectedChecked);
+ });
+
+ it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when toggle value changes', (validateUrls) => {
+ const wrapper = createWrapper();
+ const toggle = wrapper.find(ToggleSwitch);
+
+ expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
+ toggle.simulate('change', validateUrls);
+ expect(setShortUrlCreationSettings).toHaveBeenCalledWith({ validateUrls });
+ });
+
+ it.each([
+ [{ validateUrls: true }, 'checkbox will be checked' ],
+ [{ validateUrls: false }, 'checkbox will be unchecked' ],
+ [ undefined, 'checkbox will be unchecked' ],
+ ])('shows expected helper text', (shortUrlCreation, expectedText) => {
+ const wrapper = createWrapper(shortUrlCreation);
+ const text = wrapper.find('.form-text');
+
+ expect(text.text()).toContain(expectedText);
+ });
+});
diff --git a/test/settings/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts
index 9018b311..1bfd1701 100644
--- a/test/settings/reducers/settings.test.ts
+++ b/test/settings/reducers/settings.test.ts
@@ -1,11 +1,18 @@
-import reducer, { SET_REAL_TIME_UPDATES, toggleRealTimeUpdates, setRealTimeUpdatesInterval } from '../../../src/settings/reducers/settings';
+import reducer, {
+ SET_SETTINGS,
+ toggleRealTimeUpdates,
+ setRealTimeUpdatesInterval,
+ setShortUrlCreationSettings,
+} from '../../../src/settings/reducers/settings';
describe('settingsReducer', () => {
const realTimeUpdates = { enabled: true };
+ const shortUrlCreation = { validateUrls: false };
+ const settings = { realTimeUpdates, shortUrlCreation };
describe('reducer', () => {
- it('returns realTimeUpdates when action is SET_REAL_TIME_UPDATES', () => {
- expect(reducer(undefined, { type: SET_REAL_TIME_UPDATES, realTimeUpdates })).toEqual({ realTimeUpdates });
+ it('returns realTimeUpdates when action is SET_SETTINGS', () => {
+ expect(reducer(undefined, { type: SET_SETTINGS, realTimeUpdates })).toEqual(settings);
});
});
@@ -13,7 +20,7 @@ describe('settingsReducer', () => {
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
const result = toggleRealTimeUpdates(enabled);
- expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { enabled } });
+ expect(result).toEqual({ type: SET_SETTINGS, realTimeUpdates: { enabled } });
});
});
@@ -21,7 +28,15 @@ describe('settingsReducer', () => {
it.each([[ 0 ], [ 1 ], [ 2 ], [ 10 ]])('updates settings with provided value and then loads updates again', (interval) => {
const result = setRealTimeUpdatesInterval(interval);
- expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { interval } });
+ expect(result).toEqual({ type: SET_SETTINGS, realTimeUpdates: { interval } });
+ });
+ });
+
+ describe('setShortUrlCreationSettings', () => {
+ it('creates action to set shortUrlCreation settings', () => {
+ const result = setShortUrlCreationSettings({ validateUrls: true });
+
+ expect(result).toEqual({ type: SET_SETTINGS, shortUrlCreation: { validateUrls: true } });
});
});
});
diff --git a/test/short-urls/CreateShortUrl.test.tsx b/test/short-urls/CreateShortUrl.test.tsx
index e4d0d5ab..ca36661f 100644
--- a/test/short-urls/CreateShortUrl.test.tsx
+++ b/test/short-urls/CreateShortUrl.test.tsx
@@ -6,10 +6,12 @@ import { Input } from 'reactstrap';
import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl';
import DateInput from '../../src/utils/DateInput';
import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
+import { Settings } from '../../src/settings/reducers/settings';
describe('', () => {
let wrapper: ShallowWrapper;
const TagsSelector = () => null;
+ const shortUrlCreation = { validateUrls: true };
const shortUrlCreationResult = Mock.all();
const createShortUrl = jest.fn(async () => Promise.resolve());
@@ -22,6 +24,7 @@ describe('', () => {
createShortUrl={createShortUrl}
selectedServer={null}
resetCreateShortUrl={() => {}}
+ settings={Mock.of({ shortUrlCreation })}
/>,
);
});