mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Added option to customize initial state fo the 'Validate URL' option
This commit is contained in:
parent
872890e674
commit
4885088d59
8 changed files with 89 additions and 22 deletions
|
@ -1,9 +1,17 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
import { Row } from 'reactstrap';
|
||||||
import NoMenuLayout from '../common/NoMenuLayout';
|
import NoMenuLayout from '../common/NoMenuLayout';
|
||||||
|
|
||||||
const Settings = (RealTimeUpdates: FC) => () => (
|
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC) => () => (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
|
<Row>
|
||||||
|
<div className="col-lg-6">
|
||||||
<RealTimeUpdates />
|
<RealTimeUpdates />
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-6">
|
||||||
|
<ShortUrlCreation />
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
29
src/settings/ShortUrlCreation.tsx
Normal file
29
src/settings/ShortUrlCreation.tsx
Normal file
|
@ -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<ShortUrlCreationProps> = (
|
||||||
|
{ settings: { shortUrlCreation }, setShortUrlCreationSettings },
|
||||||
|
) => (
|
||||||
|
<SimpleCard title="Short URLs creation">
|
||||||
|
<FormGroup className="mb-0">
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={shortUrlCreation?.validateUrls ?? false}
|
||||||
|
onChange={(validateUrls) => setShortUrlCreationSettings({ validateUrls })}
|
||||||
|
>
|
||||||
|
By default, request validation on long URLs when creating new short URLs.
|
||||||
|
<small className="form-text text-muted">
|
||||||
|
The initial state of the <b>Validate URL</b> checkbox will
|
||||||
|
be <b>{shortUrlCreation?.validateUrls ? 'checked' : 'unchecked'}</b>.
|
||||||
|
</small>
|
||||||
|
</ToggleSwitch>
|
||||||
|
</FormGroup>
|
||||||
|
</SimpleCard>
|
||||||
|
);
|
|
@ -1,23 +1,36 @@
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { mergeDeepRight } from 'ramda';
|
import { dissoc, mergeDeepRight } from 'ramda';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { RecursivePartial } from '../../utils/utils';
|
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;
|
enabled: boolean;
|
||||||
interval?: number;
|
interval?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlCreationSettings {
|
||||||
|
validateUrls: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
realTimeUpdates: RealTimeUpdates;
|
realTimeUpdates: RealTimeUpdatesSettings;
|
||||||
|
shortUrlCreation?: ShortUrlCreationSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: Settings = {
|
const initialState: Settings = {
|
||||||
realTimeUpdates: {
|
realTimeUpdates: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
shortUrlCreation: {
|
||||||
|
validateUrls: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsAction = Action & Settings;
|
type SettingsAction = Action & Settings;
|
||||||
|
@ -25,15 +38,20 @@ type SettingsAction = Action & Settings;
|
||||||
type PartialSettingsAction = Action & RecursivePartial<Settings>;
|
type PartialSettingsAction = Action & RecursivePartial<Settings>;
|
||||||
|
|
||||||
export default buildReducer<Settings, SettingsAction>({
|
export default buildReducer<Settings, SettingsAction>({
|
||||||
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => mergeDeepRight(state, { realTimeUpdates }),
|
[SET_SETTINGS]: (state, action) => mergeDeepRight(state, dissoc('type', action)),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const toggleRealTimeUpdates = (enabled: boolean): PartialSettingsAction => ({
|
export const toggleRealTimeUpdates = (enabled: boolean): PartialSettingsAction => ({
|
||||||
type: SET_REAL_TIME_UPDATES,
|
type: SET_SETTINGS,
|
||||||
realTimeUpdates: { enabled },
|
realTimeUpdates: { enabled },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setRealTimeUpdatesInterval = (interval: number): PartialSettingsAction => ({
|
export const setRealTimeUpdatesInterval = (interval: number): PartialSettingsAction => ({
|
||||||
type: SET_REAL_TIME_UPDATES,
|
type: SET_SETTINGS,
|
||||||
realTimeUpdates: { interval },
|
realTimeUpdates: { interval },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setShortUrlCreationSettings = (settings: ShortUrlCreationSettings): PartialSettingsAction => ({
|
||||||
|
type: SET_SETTINGS,
|
||||||
|
shortUrlCreation: settings,
|
||||||
|
});
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import RealTimeUpdates from '../RealTimeUpdates';
|
import RealTimeUpdates from '../RealTimeUpdates';
|
||||||
import Settings from '../Settings';
|
import Settings from '../Settings';
|
||||||
import { setRealTimeUpdatesInterval, toggleRealTimeUpdates } from '../reducers/settings';
|
import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, toggleRealTimeUpdates } from '../reducers/settings';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||||
|
import { ShortUrlCreation } from '../ShortUrlCreation';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates');
|
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation');
|
||||||
bottle.decorator('Settings', withoutSelectedServer);
|
bottle.decorator('Settings', withoutSelectedServer);
|
||||||
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
|
||||||
|
|
||||||
// Services
|
|
||||||
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
|
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
|
||||||
bottle.decorator(
|
bottle.decorator(
|
||||||
'RealTimeUpdates',
|
'RealTimeUpdates',
|
||||||
connect([ 'settings' ], [ 'toggleRealTimeUpdates', 'setRealTimeUpdatesInterval' ]),
|
connect([ 'settings' ], [ 'toggleRealTimeUpdates', 'setRealTimeUpdatesInterval' ]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bottle.serviceFactory('ShortUrlCreation', () => ShortUrlCreation);
|
||||||
|
bottle.decorator('ShortUrlCreation', connect([ 'settings' ], [ 'setShortUrlCreationSettings' ]));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||||
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
||||||
|
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { isEmpty, pipe, replace, trim } from 'ramda';
|
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 { Button, FormGroup, Input } from 'reactstrap';
|
||||||
import { InputType } from 'reactstrap/lib/Input';
|
import { InputType } from 'reactstrap/lib/Input';
|
||||||
import * as m from 'moment';
|
import * as m from 'moment';
|
||||||
|
@ -12,6 +12,7 @@ import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
import { DomainSelectorProps } from '../domains/DomainSelector';
|
import { DomainSelectorProps } from '../domains/DomainSelector';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
|
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlData } from './data';
|
import { ShortUrlData } from './data';
|
||||||
import { ShortUrlCreation } from './reducers/shortUrlCreation';
|
import { ShortUrlCreation } from './reducers/shortUrlCreation';
|
||||||
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
||||||
|
@ -23,6 +24,7 @@ export interface CreateShortUrlProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateShortUrlConnectProps extends CreateShortUrlProps {
|
interface CreateShortUrlConnectProps extends CreateShortUrlProps {
|
||||||
|
settings: Settings;
|
||||||
shortUrlCreationResult: ShortUrlCreation;
|
shortUrlCreationResult: ShortUrlCreation;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
createShortUrl: (data: ShortUrlData) => Promise<void>;
|
createShortUrl: (data: ShortUrlData) => Promise<void>;
|
||||||
|
@ -31,7 +33,7 @@ interface CreateShortUrlConnectProps extends CreateShortUrlProps {
|
||||||
|
|
||||||
export const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
export const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||||
|
|
||||||
const initialState: ShortUrlData = {
|
const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => ({
|
||||||
longUrl: '',
|
longUrl: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
customSlug: '',
|
customSlug: '',
|
||||||
|
@ -41,8 +43,8 @@ const initialState: ShortUrlData = {
|
||||||
validUntil: undefined,
|
validUntil: undefined,
|
||||||
maxVisits: undefined,
|
maxVisits: undefined,
|
||||||
findIfExists: false,
|
findIfExists: false,
|
||||||
validateUrl: true,
|
validateUrl: settings?.validateUrls ?? false,
|
||||||
};
|
});
|
||||||
|
|
||||||
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits';
|
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits';
|
||||||
type DateFields = 'validSince' | 'validUntil';
|
type DateFields = 'validSince' | 'validUntil';
|
||||||
|
@ -58,9 +60,10 @@ const CreateShortUrl = (
|
||||||
resetCreateShortUrl,
|
resetCreateShortUrl,
|
||||||
selectedServer,
|
selectedServer,
|
||||||
basicMode = false,
|
basicMode = false,
|
||||||
|
settings: { shortUrlCreation: shortUrlCreationSettings },
|
||||||
}: CreateShortUrlConnectProps) => {
|
}: CreateShortUrlConnectProps) => {
|
||||||
|
const initialState = useMemo(() => getInitialState(shortUrlCreationSettings), [ shortUrlCreationSettings ]);
|
||||||
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
|
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
|
||||||
|
|
||||||
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
|
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
|
||||||
const reset = () => setShortUrlCreation(initialState);
|
const reset = () => setShortUrlCreation(initialState);
|
||||||
const save = handleEventPreventingDefault(() => {
|
const save = handleEventPreventingDefault(() => {
|
||||||
|
|
|
@ -56,7 +56,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
);
|
);
|
||||||
bottle.decorator(
|
bottle.decorator(
|
||||||
'CreateShortUrl',
|
'CreateShortUrl',
|
||||||
connect([ 'shortUrlCreationResult', 'selectedServer' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
|
connect([ 'shortUrlCreationResult', 'selectedServer', 'settings' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
|
||||||
);
|
);
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
||||||
|
|
|
@ -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', () => {
|
describe('settingsReducer', () => {
|
||||||
const realTimeUpdates = { enabled: true };
|
const realTimeUpdates = { enabled: true };
|
||||||
|
const shortUrlCreation = { validateUrls: false };
|
||||||
|
const settings = { realTimeUpdates, shortUrlCreation };
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns realTimeUpdates when action is SET_REAL_TIME_UPDATES', () => {
|
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) => {
|
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
|
||||||
const result = toggleRealTimeUpdates(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) => {
|
it.each([[ 0 ], [ 1 ], [ 2 ], [ 10 ]])('updates settings with provided value and then loads updates again', (interval) => {
|
||||||
const result = setRealTimeUpdatesInterval(interval);
|
const result = setRealTimeUpdatesInterval(interval);
|
||||||
|
|
||||||
expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { interval } });
|
expect(result).toEqual({ type: SET_SETTINGS, realTimeUpdates: { interval } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,10 +6,12 @@ import { Input } from 'reactstrap';
|
||||||
import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl';
|
import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl';
|
||||||
import DateInput from '../../src/utils/DateInput';
|
import DateInput from '../../src/utils/DateInput';
|
||||||
import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
|
import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('<CreateShortUrl />', () => {
|
describe('<CreateShortUrl />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const TagsSelector = () => null;
|
const TagsSelector = () => null;
|
||||||
|
const shortUrlCreation = { validateUrls: true };
|
||||||
const shortUrlCreationResult = Mock.all<ShortUrlCreation>();
|
const shortUrlCreationResult = Mock.all<ShortUrlCreation>();
|
||||||
const createShortUrl = jest.fn(async () => Promise.resolve());
|
const createShortUrl = jest.fn(async () => Promise.resolve());
|
||||||
|
|
||||||
|
@ -22,6 +24,7 @@ describe('<CreateShortUrl />', () => {
|
||||||
createShortUrl={createShortUrl}
|
createShortUrl={createShortUrl}
|
||||||
selectedServer={null}
|
selectedServer={null}
|
||||||
resetCreateShortUrl={() => {}}
|
resetCreateShortUrl={() => {}}
|
||||||
|
settings={Mock.of<Settings>({ shortUrlCreation })}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue