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/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts index 9018b311..42b012af 100644 --- a/test/settings/reducers/settings.test.ts +++ b/test/settings/reducers/settings.test.ts @@ -1,11 +1,13 @@ -import reducer, { SET_REAL_TIME_UPDATES, toggleRealTimeUpdates, setRealTimeUpdatesInterval } from '../../../src/settings/reducers/settings'; +import reducer, { SET_SETTINGS, toggleRealTimeUpdates, setRealTimeUpdatesInterval } 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 }); + expect(reducer(undefined, { type: SET_SETTINGS, realTimeUpdates })).toEqual(settings); }); }); @@ -13,7 +15,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 +23,7 @@ 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 } }); }); }); }); 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 })} />, ); });