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';
|
||||
|
||||
.input-group > .react-tags {
|
||||
flex: 1 1 auto;
|
||||
width: 1%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.react-tags {
|
||||
position: relative;
|
||||
padding: 5px 0 0 6px;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.short-urls-filtering-bar__tags-icon {
|
||||
vertical-align: bottom;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
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 { 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 SearchField from '../utils/SearchField';
|
||||
import Tag from '../tags/helpers/Tag';
|
||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||
import { formatIsoDate } from '../utils/helpers/date';
|
||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||
import { DateRange } from '../utils/dates/types';
|
||||
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
|
||||
import { OrderDir } from '../utils/helpers/ordering';
|
||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||
import { useShortUrlsQuery } from './helpers/hooks';
|
||||
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||
import './ShortUrlsFilteringBar.scss';
|
||||
|
||||
export interface ShortUrlsFilteringProps {
|
||||
|
@ -32,8 +30,8 @@ export interface ShortUrlsFilteringProps {
|
|||
const dateOrNull = (date?: string) => (date ? parseISO(date) : null);
|
||||
|
||||
const ShortUrlsFilteringBar = (
|
||||
colorGenerator: ColorGenerator,
|
||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||
TagsSelector: FC<TagsSelectorProps>,
|
||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
||||
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage] = useShortUrlsQuery();
|
||||
const setDates = pipe(
|
||||
|
@ -47,10 +45,7 @@ const ShortUrlsFilteringBar = (
|
|||
(searchTerm: string) => (isEmpty(searchTerm) ? undefined : searchTerm),
|
||||
(searchTerm) => toFirstPage({ search: searchTerm }),
|
||||
);
|
||||
const removeTag = pipe(
|
||||
(tag: string) => tags.filter((selectedTag) => selectedTag !== tag),
|
||||
(updateTags) => toFirstPage({ tags: updateTags }),
|
||||
);
|
||||
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
||||
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
||||
const toggleTagsMode = pipe(
|
||||
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||
|
@ -61,13 +56,21 @@ const ShortUrlsFilteringBar = (
|
|||
<div className={classNames('short-urls-filtering-bar-container', className)}>
|
||||
<SearchField initialValue={search} onChange={setSearch} />
|
||||
|
||||
<Row className="flex-column-reverse flex-lg-row">
|
||||
<div className="col-lg-4 col-xl-6 mt-3">
|
||||
<ExportShortUrlsBtn amount={shortUrlsAmount} />
|
||||
</div>
|
||||
<div className="col-12 d-block d-lg-none mt-3">
|
||||
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={order} onChange={handleOrderBy} />
|
||||
</div>
|
||||
<InputGroup className="mt-3">
|
||||
<TagsSelector allowNew={false} placeholder="With tags..." selectedTags={tags} onChange={changeTagSelection} />
|
||||
{canChangeTagsMode && tags.length > 1 && (
|
||||
<>
|
||||
<Button outline color="secondary" onClick={toggleTagsMode} id="tagsModeBtn">
|
||||
<FontAwesomeIcon className="short-urls-filtering-bar__tags-icon" icon={tagsMode === 'all' ? faTags : faTag} />
|
||||
</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">
|
||||
<DateRangeSelector
|
||||
defaultText="All short URLs"
|
||||
|
@ -78,26 +81,13 @@ const ShortUrlsFilteringBar = (
|
|||
onDatesChange={setDates}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -50,7 +50,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
||||
bottle.decorator('QrCodeModal', connect(['selectedServer']));
|
||||
|
||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator', 'ExportShortUrlsBtn');
|
||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
||||
|
||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
||||
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface TagsSelectorProps {
|
|||
selectedTags: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
allowNew?: boolean;
|
||||
}
|
||||
|
||||
interface TagsSelectorConnectProps extends TagsSelectorProps {
|
||||
|
@ -21,7 +22,7 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
|
|||
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
|
||||
|
||||
const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||
{ selectedTags, onChange, placeholder, listTags, tagsList, settings }: TagsSelectorConnectProps,
|
||||
{ selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
listTags();
|
||||
|
@ -43,7 +44,7 @@ const TagsSelector = (colorGenerator: ColorGenerator) => (
|
|||
tagComponent={ReactTagsTag}
|
||||
suggestions={tagsList.tags.filter((tag) => !selectedTags.includes(tag)).map(toComponentTag)}
|
||||
suggestionComponent={ReactTagsSuggestion}
|
||||
allowNew
|
||||
allowNew={allowNew}
|
||||
addOnBlur
|
||||
placeholderText={placeholder ?? 'Add tags to the URL'}
|
||||
minQueryLength={1}
|
||||
|
|
|
@ -6,7 +6,6 @@ import filteringBarCreator from '../../src/short-urls/ShortUrlsFilteringBar';
|
|||
import SearchField from '../../src/utils/SearchField';
|
||||
import Tag from '../../src/tags/helpers/Tag';
|
||||
import { DateRangeSelector } from '../../src/utils/dates/DateRangeSelector';
|
||||
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||
import { TooltipToggleSwitch } from '../../src/utils/TooltipToggleSwitch';
|
||||
import { OrderingDropdown } from '../../src/utils/OrderingDropdown';
|
||||
|
@ -21,7 +20,7 @@ jest.mock('react-router-dom', () => ({
|
|||
describe('<ShortUrlsFilteringBar />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const ExportShortUrlsBtn = () => null;
|
||||
const ShortUrlsFilteringBar = filteringBarCreator(Mock.all<ColorGenerator>(), ExportShortUrlsBtn);
|
||||
const ShortUrlsFilteringBar = filteringBarCreator(ExportShortUrlsBtn, () => null);
|
||||
const navigate = jest.fn();
|
||||
const handleOrderBy = jest.fn();
|
||||
const now = new Date();
|
||||
|
|
Loading…
Reference in a new issue