mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Improved tags filtering for short URLs, allowing to select from any existing tag
This commit is contained in:
parent
e387706a7b
commit
4b97abaf72
6 changed files with 39 additions and 42 deletions
|
@ -1,5 +1,11 @@
|
||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
|
|
||||||
|
.input-group > .react-tags {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 1%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.react-tags {
|
.react-tags {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px 0 0 6px;
|
padding: 5px 0 0 6px;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
.short-urls-filtering-bar__tags-icon {
|
.short-urls-filtering-bar__tags-icon {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { isEmpty, pipe } from 'ramda';
|
import { isEmpty, pipe } from 'ramda';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { Row } from 'reactstrap';
|
import { Button, InputGroup, Row, UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faTag, faTags } from '@fortawesome/free-solid-svg-icons';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import Tag from '../tags/helpers/Tag';
|
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
|
||||||
import { DateRange } from '../utils/dates/types';
|
import { DateRange } from '../utils/dates/types';
|
||||||
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
|
|
||||||
import { OrderDir } from '../utils/helpers/ordering';
|
import { OrderDir } from '../utils/helpers/ordering';
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||||
import { useShortUrlsQuery } from './helpers/hooks';
|
import { useShortUrlsQuery } from './helpers/hooks';
|
||||||
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||||
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
||||||
|
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
import './ShortUrlsFilteringBar.scss';
|
import './ShortUrlsFilteringBar.scss';
|
||||||
|
|
||||||
export interface ShortUrlsFilteringProps {
|
export interface ShortUrlsFilteringProps {
|
||||||
|
@ -32,8 +30,8 @@ export interface ShortUrlsFilteringProps {
|
||||||
const dateOrNull = (date?: string) => (date ? parseISO(date) : null);
|
const dateOrNull = (date?: string) => (date ? parseISO(date) : null);
|
||||||
|
|
||||||
const ShortUrlsFilteringBar = (
|
const ShortUrlsFilteringBar = (
|
||||||
colorGenerator: ColorGenerator,
|
|
||||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||||
|
TagsSelector: FC<TagsSelectorProps>,
|
||||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
||||||
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
||||||
const setDates = pipe(
|
const setDates = pipe(
|
||||||
|
@ -47,10 +45,7 @@ const ShortUrlsFilteringBar = (
|
||||||
(searchTerm: string) => (isEmpty(searchTerm) ? undefined : searchTerm),
|
(searchTerm: string) => (isEmpty(searchTerm) ? undefined : searchTerm),
|
||||||
(searchTerm) => toFirstPage({ search: searchTerm }),
|
(searchTerm) => toFirstPage({ search: searchTerm }),
|
||||||
);
|
);
|
||||||
const removeTag = pipe(
|
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
||||||
(tag: string) => tags.filter((selectedTag) => selectedTag !== tag),
|
|
||||||
(updateTags) => toFirstPage({ tags: updateTags }),
|
|
||||||
);
|
|
||||||
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
||||||
const toggleTagsMode = pipe(
|
const toggleTagsMode = pipe(
|
||||||
() => (tagsMode === 'any' ? 'all' : 'any'),
|
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||||
|
@ -61,13 +56,21 @@ const ShortUrlsFilteringBar = (
|
||||||
<div className={classNames('short-urls-filtering-bar-container', className)}>
|
<div className={classNames('short-urls-filtering-bar-container', className)}>
|
||||||
<SearchField initialValue={search} onChange={setSearch} />
|
<SearchField initialValue={search} onChange={setSearch} />
|
||||||
|
|
||||||
<Row className="flex-column-reverse flex-lg-row">
|
<InputGroup className="mt-3">
|
||||||
<div className="col-lg-4 col-xl-6 mt-3">
|
<TagsSelector allowNew={false} placeholder="With tags..." selectedTags={tags} onChange={changeTagSelection} />
|
||||||
<ExportShortUrlsBtn amount={shortUrlsAmount} />
|
{canChangeTagsMode && tags.length > 1 && (
|
||||||
</div>
|
<>
|
||||||
<div className="col-12 d-block d-lg-none mt-3">
|
<Button outline color="secondary" onClick={toggleTagsMode} id="tagsModeBtn">
|
||||||
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={order} onChange={handleOrderBy} />
|
<FontAwesomeIcon className="short-urls-filtering-bar__tags-icon" icon={tagsMode === 'all' ? faTags : faTag} />
|
||||||
</div>
|
</Button>
|
||||||
|
<UncontrolledTooltip target="tagsModeBtn" placement="left">
|
||||||
|
{tagsMode === 'all' ? 'With all the tags.' : 'With any of the tags.'}
|
||||||
|
</UncontrolledTooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<Row className="flex-lg-row-reverse">
|
||||||
<div className="col-lg-8 col-xl-6 mt-3">
|
<div className="col-lg-8 col-xl-6 mt-3">
|
||||||
<DateRangeSelector
|
<DateRangeSelector
|
||||||
defaultText="All short URLs"
|
defaultText="All short URLs"
|
||||||
|
@ -78,26 +81,13 @@ const ShortUrlsFilteringBar = (
|
||||||
onDatesChange={setDates}
|
onDatesChange={setDates}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-6 col-lg-4 col-xl-6 mt-3">
|
||||||
|
<ExportShortUrlsBtn amount={shortUrlsAmount} />
|
||||||
|
</div>
|
||||||
|
<div className="col-6 d-lg-none mt-3">
|
||||||
|
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={order} onChange={handleOrderBy} />
|
||||||
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{tags.length > 0 && (
|
|
||||||
<h4 className="mt-3">
|
|
||||||
{canChangeTagsMode && tags.length > 1 && (
|
|
||||||
<div className="float-end ms-2 mt-1">
|
|
||||||
<TooltipToggleSwitch
|
|
||||||
checked={tagsMode === 'all'}
|
|
||||||
tooltip={{ placement: 'left' }}
|
|
||||||
onChange={toggleTagsMode}
|
|
||||||
>
|
|
||||||
{tagsMode === 'all' ? 'Short URLs including all tags.' : 'Short URLs including any tag.'}
|
|
||||||
</TooltipToggleSwitch>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<FontAwesomeIcon icon={tagsIcon} className="short-urls-filtering-bar__tags-icon me-1" />
|
|
||||||
{tags.map((tag) =>
|
|
||||||
<Tag colorGenerator={colorGenerator} key={tag} text={tag} clearable onClose={() => removeTag(tag)} />)}
|
|
||||||
</h4>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,7 +50,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
||||||
bottle.decorator('QrCodeModal', connect(['selectedServer']));
|
bottle.decorator('QrCodeModal', connect(['selectedServer']));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator', 'ExportShortUrlsBtn');
|
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
||||||
|
|
||||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
||||||
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface TagsSelectorProps {
|
||||||
selectedTags: string[];
|
selectedTags: string[];
|
||||||
onChange: (tags: string[]) => void;
|
onChange: (tags: string[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
allowNew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TagsSelectorConnectProps extends TagsSelectorProps {
|
interface TagsSelectorConnectProps extends TagsSelectorProps {
|
||||||
|
@ -21,7 +22,7 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
|
||||||
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
|
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
|
||||||
|
|
||||||
const TagsSelector = (colorGenerator: ColorGenerator) => (
|
const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
{ selectedTags, onChange, placeholder, listTags, tagsList, settings }: TagsSelectorConnectProps,
|
{ selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps,
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listTags();
|
listTags();
|
||||||
|
@ -43,7 +44,7 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
tagComponent={ReactTagsTag}
|
tagComponent={ReactTagsTag}
|
||||||
suggestions={tagsList.tags.filter((tag) => !selectedTags.includes(tag)).map(toComponentTag)}
|
suggestions={tagsList.tags.filter((tag) => !selectedTags.includes(tag)).map(toComponentTag)}
|
||||||
suggestionComponent={ReactTagsSuggestion}
|
suggestionComponent={ReactTagsSuggestion}
|
||||||
allowNew
|
allowNew={allowNew}
|
||||||
addOnBlur
|
addOnBlur
|
||||||
placeholderText={placeholder ?? 'Add tags to the URL'}
|
placeholderText={placeholder ?? 'Add tags to the URL'}
|
||||||
minQueryLength={1}
|
minQueryLength={1}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import filteringBarCreator from '../../src/short-urls/ShortUrlsFilteringBar';
|
||||||
import SearchField from '../../src/utils/SearchField';
|
import SearchField from '../../src/utils/SearchField';
|
||||||
import Tag from '../../src/tags/helpers/Tag';
|
import Tag from '../../src/tags/helpers/Tag';
|
||||||
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
|
||||||
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
|
||||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||||
import { TooltipToggleSwitch } from '../../src/utils/TooltipToggleSwitch';
|
import { TooltipToggleSwitch } from '../../src/utils/TooltipToggleSwitch';
|
||||||
import { OrderingDropdown } from '../../src/utils/OrderingDropdown';
|
import { OrderingDropdown } from '../../src/utils/OrderingDropdown';
|
||||||
|
@ -21,7 +20,7 @@ jest.mock('react-router-dom', () => ({
|
||||||
describe('<ShortUrlsFilteringBar />', () => {
|
describe('<ShortUrlsFilteringBar />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const ExportShortUrlsBtn = () => null;
|
const ExportShortUrlsBtn = () => null;
|
||||||
const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>(), ExportShortUrlsBtn);
|
const ShortUrlsFilteringBar = filteringBarCreator(ExportShortUrlsBtn, () => null);
|
||||||
const navigate = jest.fn();
|
const navigate = jest.fn();
|
||||||
const handleOrderBy = jest.fn();
|
const handleOrderBy = jest.fn();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
Loading…
Reference in a new issue