diff --git a/CHANGELOG.md b/CHANGELOG.md
index 128b91bd..ccd3121f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added
* [#460](https://github.com/shlinkio/shlink-web-client/pull/460) Added dynamic title on hover for tags with a very long title.
* [#462](https://github.com/shlinkio/shlink-web-client/pull/462) Now it is possible to paste multiple comma-separated tags in the tags selector, making all of them to be added as individual tags.
+* [#463](https://github.com/shlinkio/shlink-web-client/pull/463) The strategy to determine which tags to suggest in the TagsSelector during short URL creation, can now be configured:
+
+ * `startsWith`: Suggests tags that start with the input. This is the default behavior for keep it as it was so far.
+ * `includes`: Suggests tags that contain the input.
### Changed
* *Nothing*
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 294115a0..fc8ea340 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -20,8 +20,8 @@ const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC,
, ], // eslint-disable-line react/jsx-key
- [ , ], // eslint-disable-line react/jsx-key
+ [ , ], // eslint-disable-line react/jsx-key
+ [ , ], // eslint-disable-line react/jsx-key
]}
/>
diff --git a/src/settings/ShortUrlCreation.tsx b/src/settings/ShortUrlCreation.tsx
index b91ed500..2caaa329 100644
--- a/src/settings/ShortUrlCreation.tsx
+++ b/src/settings/ShortUrlCreation.tsx
@@ -1,29 +1,62 @@
-import { FC } from 'react';
-import { FormGroup } from 'reactstrap';
+import { FC, ReactNode } from 'react';
+import { DropdownItem, FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
import ToggleSwitch from '../utils/ToggleSwitch';
-import { Settings, ShortUrlCreationSettings } from './reducers/settings';
+import { DropdownBtn } from '../utils/DropdownBtn';
+import { Settings, ShortUrlCreationSettings, TagFilteringMode } 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.
+const tagFilteringModeText = (tagFilteringMode: TagFilteringMode | undefined): string =>
+ tagFilteringMode === 'includes' ? 'Suggest tags including input' : 'Suggest tags starting with input';
+const tagFilteringModeHint = (tagFilteringMode: TagFilteringMode | undefined): ReactNode =>
+ tagFilteringMode === 'includes'
+ ? <>The list of suggested tags will contain existing ones including provided input.>
+ : <>The list of suggested tags will contain existing ones starting with provided input.>;
+
+export const ShortUrlCreation: FC = ({ settings, setShortUrlCreationSettings }) => {
+ const shortUrlCreation: ShortUrlCreationSettings = settings.shortUrlCreation ?? { validateUrls: false };
+ const changeTagsFilteringMode = (tagFilteringMode: TagFilteringMode) => () => setShortUrlCreationSettings(
+ { ...shortUrlCreation ?? { validateUrls: false }, tagFilteringMode },
+ );
+
+ return (
+
+
+ setShortUrlCreationSettings({ ...shortUrlCreation, 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'}.
+
+
+
+
+
+
+
+ {tagFilteringModeText('startsWith')}
+
+
+ {tagFilteringModeText('includes')}
+
+
- The initial state of the Validate URL checkbox will
- be {shortUrlCreation?.validateUrls ? 'checked' : 'unchecked'}.
+ {tagFilteringModeHint(shortUrlCreation.tagFilteringMode)}
-
-
-
-);
+
+
+ );
+};
diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts
index 0ba6f106..a798697b 100644
--- a/src/settings/reducers/settings.ts
+++ b/src/settings/reducers/settings.ts
@@ -17,8 +17,11 @@ interface RealTimeUpdatesSettings {
interval?: number;
}
+export type TagFilteringMode = 'startsWith' | 'includes';
+
export interface ShortUrlCreationSettings {
validateUrls: boolean;
+ tagFilteringMode?: TagFilteringMode;
}
export interface UiSettings {
diff --git a/src/tags/helpers/TagsSelector.tsx b/src/tags/helpers/TagsSelector.tsx
index c7041576..8b585fe2 100644
--- a/src/tags/helpers/TagsSelector.tsx
+++ b/src/tags/helpers/TagsSelector.tsx
@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import ReactTags, { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
import ColorGenerator from '../../utils/services/ColorGenerator';
+import { Settings } from '../../settings/reducers/settings';
import { TagsList } from '../reducers/tagsList';
import TagBullet from './TagBullet';
import Tag from './Tag';
@@ -14,17 +15,19 @@ export interface TagsSelectorProps {
interface TagsSelectorConnectProps extends TagsSelectorProps {
listTags: Function;
tagsList: TagsList;
+ settings: Settings;
}
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
const TagsSelector = (colorGenerator: ColorGenerator) => (
- { selectedTags, onChange, listTags, tagsList, placeholder = 'Add tags to the URL' }: TagsSelectorConnectProps,
+ { selectedTags, onChange, placeholder, listTags, tagsList, settings }: TagsSelectorConnectProps,
) => {
useEffect(() => {
listTags();
}, []);
+ const searchMode = settings.shortUrlCreation?.tagFilteringMode ?? 'startsWith';
const ReactTagsTag = ({ tag, onDelete }: TagComponentProps) =>
;
const ReactTagsSuggestion = ({ item }: SuggestionComponentProps) => (
@@ -42,9 +45,14 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
suggestionComponent={ReactTagsSuggestion}
allowNew
addOnBlur
- placeholderText={placeholder}
+ placeholderText={placeholder ?? 'Add tags to the URL'}
minQueryLength={1}
delimiters={[ 'Enter', 'Tab', ',' ]}
+ suggestionsTransform={
+ searchMode === 'includes'
+ ? (query, suggestions) => suggestions.filter(({ name }) => name.includes(query))
+ : undefined
+ }
onDelete={(removedTagIndex) => {
const tagsCopy = [ ...selectedTags ];
diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts
index 3e71c32f..ba5951c1 100644
--- a/src/tags/services/provideServices.ts
+++ b/src/tags/services/provideServices.ts
@@ -12,7 +12,7 @@ import { ConnectDecorator } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator');
- bottle.decorator('TagsSelector', connect([ 'tagsList' ], [ 'listTags' ]));
+ bottle.decorator('TagsSelector', connect([ 'tagsList', 'settings' ], [ 'listTags' ]));
bottle.serviceFactory(
'TagCard',
diff --git a/test/settings/ShortUrlCreation.test.tsx b/test/settings/ShortUrlCreation.test.tsx
index 1cb92d9a..3a61f5dd 100644
--- a/test/settings/ShortUrlCreation.test.tsx
+++ b/test/settings/ShortUrlCreation.test.tsx
@@ -1,8 +1,10 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
+import { DropdownItem } from 'reactstrap';
import { ShortUrlCreationSettings, Settings } from '../../src/settings/reducers/settings';
import { ShortUrlCreation } from '../../src/settings/ShortUrlCreation';
import ToggleSwitch from '../../src/utils/ToggleSwitch';
+import { DropdownBtn } from '../../src/utils/DropdownBtn';
describe('', () => {
let wrapper: ShallowWrapper;
@@ -25,13 +27,41 @@ describe('', () => {
[{ validateUrls: true }, true ],
[{ validateUrls: false }, false ],
[ undefined, false ],
- ])('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 toggle = wrapper.find(ToggleSwitch);
expect(toggle.prop('checked')).toEqual(expectedChecked);
});
+ it.each([
+ [{ validateUrls: true }, 'checkbox will be checked' ],
+ [{ validateUrls: false }, 'checkbox will be unchecked' ],
+ [ undefined, 'checkbox will be unchecked' ],
+ ])('shows expected helper text for URL validation', (shortUrlCreation, expectedText) => {
+ const wrapper = createWrapper(shortUrlCreation);
+ const text = wrapper.find('.form-text').first();
+
+ expect(text.text()).toContain(expectedText);
+ });
+
+ it.each([
+ [ { tagFilteringMode: 'includes' } as ShortUrlCreationSettings, 'Suggest tags including input', 'including' ],
+ [
+ { tagFilteringMode: 'startsWith' } as ShortUrlCreationSettings,
+ 'Suggest tags starting with input',
+ 'starting with',
+ ],
+ [ undefined, 'Suggest tags starting with input', 'starting with' ],
+ ])('shows expected texts for tags suggestions', (shortUrlCreation, expectedText, expectedHint) => {
+ const wrapper = createWrapper(shortUrlCreation);
+ const hintText = wrapper.find('.form-text').last();
+ const dropdown = wrapper.find(DropdownBtn);
+
+ expect(dropdown.prop('text')).toEqual(expectedText);
+ expect(hintText.text()).toContain(expectedHint);
+ });
+
it.each([[ true ], [ false ]])('invokes setShortUrlCreationSettings when toggle value changes', (validateUrls) => {
const wrapper = createWrapper();
const toggle = wrapper.find(ToggleSwitch);
@@ -41,14 +71,21 @@ describe('', () => {
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');
+ it('invokes setShortUrlCreationSettings when dropdown value changes', () => {
+ const wrapper = createWrapper();
+ const firstDropdownItem = wrapper.find(DropdownItem).first();
+ const secondDropdownItem = wrapper.find(DropdownItem).last();
- expect(text.text()).toContain(expectedText);
+ expect(setShortUrlCreationSettings).not.toHaveBeenCalled();
+
+ firstDropdownItem.simulate('click');
+ expect(setShortUrlCreationSettings).toHaveBeenCalledWith(expect.objectContaining(
+ { tagFilteringMode: 'startsWith' },
+ ));
+
+ secondDropdownItem.simulate('click');
+ expect(setShortUrlCreationSettings).toHaveBeenCalledWith(expect.objectContaining(
+ { tagFilteringMode: 'includes' },
+ ));
});
});
diff --git a/test/tags/helpers/TagsSelector.test.tsx b/test/tags/helpers/TagsSelector.test.tsx
index 3c3739cb..91934aca 100644
--- a/test/tags/helpers/TagsSelector.test.tsx
+++ b/test/tags/helpers/TagsSelector.test.tsx
@@ -3,6 +3,7 @@ import { Mock } from 'ts-mockery';
import createTagsSelector from '../../../src/tags/helpers/TagsSelector';
import ColorGenerator from '../../../src/utils/services/ColorGenerator';
import { TagsList } from '../../../src/tags/reducers/tagsList';
+import { Settings } from '../../../src/settings/reducers/settings';
describe('', () => {
const onChange = jest.fn();
@@ -14,7 +15,13 @@ describe('', () => {
beforeEach(jest.clearAllMocks);
beforeEach(() => {
wrapper = shallow(
- ,
+ ()}
+ listTags={jest.fn()}
+ onChange={onChange}
+ />,
);
});