Skip to content

Commit ec991c1

Browse files
authored
Merge pull request #714 from acelaya-forks/feature/tags-auto-complete
Migrate from react-tag-autocomplete to TagsAutocomplete component
2 parents c5f09df + 012fc52 commit ec991c1

14 files changed

Lines changed: 46 additions & 352 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
66

7+
## [Unreleased]
8+
### Added
9+
* *Nothing*
10+
11+
### Changed
12+
* Replace `react-tag-autocomplete` with `TagsAutocomplete` from shlink-frontend-kit.
13+
14+
### Deprecated
15+
* *Nothing*
16+
17+
### Removed
18+
* *Nothing*
19+
20+
### Fixed
21+
* *Nothing*
22+
23+
724
## [0.14.2] - 2025-06-11
825
### Added
926
* *Nothing*

package-lock.json

Lines changed: 4 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"@fortawesome/free-solid-svg-icons": "^6.7.2",
4747
"@fortawesome/react-fontawesome": "^0.2.2",
4848
"@reduxjs/toolkit": "^2.5.0",
49-
"@shlinkio/shlink-frontend-kit": "^0.9.10",
49+
"@shlinkio/shlink-frontend-kit": "^0.9.11",
5050
"@shlinkio/shlink-js-sdk": "^2.0.0",
5151
"react": "^18.3 || ^19.0",
5252
"react-dom": "^18.3 || ^19.0",
@@ -73,7 +73,6 @@
7373
"react-external-link": "^2.5.0",
7474
"react-leaflet": "^4.2.1 || ^5.0",
7575
"react-swipeable": "^7.0.2",
76-
"react-tag-autocomplete": "^7.5.0",
7776
"recharts": "^2.15.3"
7877
},
7978
"devDependencies": {

src/index.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
@use '../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
2-
@use './tags/react-tag-autocomplete';
32
@use 'leaflet/dist/leaflet.css';
43

54
dialog .leaflet-top .leaflet-control {

src/short-urls/ShortUrlsFilteringBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const ShortUrlsFilteringBar: FCWithDeps<ShortUrlsFilteringConnectProps, ShortUrl
9090
tags={tagsList.tags}
9191
selectedTags={tags}
9292
onChange={changeTagSelection}
93+
containerClassName={clsx(tags.length > 1 && 'tw:[&]:rounded-r-none')}
9394
/>
9495
</div>
9596
{tags.length > 1 && (

src/tags/helpers/TagsSelector.tsx

Lines changed: 11 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,31 @@
1-
import { clsx } from 'clsx';
2-
import type { RefObject } from 'react';
3-
import { useRef } from 'react';
4-
import type { OptionRendererProps, ReactTagsAPI, TagRendererProps, TagSuggestion } from 'react-tag-autocomplete';
5-
import { ReactTags } from 'react-tag-autocomplete';
1+
import type { TagsAutocompleteProps } from '@shlinkio/shlink-frontend-kit/tailwind';
2+
import { TagsAutocomplete } from '@shlinkio/shlink-frontend-kit/tailwind';
63
import type { FCWithDeps } from '../../container/utils';
74
import { componentFactory, useDependencies } from '../../container/utils';
85
import { useSetting } from '../../settings';
96
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
10-
import { normalizeTag } from './index';
11-
import { Tag } from './Tag';
12-
import { TagBullet } from './TagBullet';
13-
14-
let tagId = 1;
15-
16-
const NOT_FOUND_TAG = 'Tag not found';
17-
const NEW_TAG = 'Add tag';
18-
const isSelectableOption = (tag: string) => tag !== NOT_FOUND_TAG;
19-
const isNewOption = (tag: string) => tag === NEW_TAG;
20-
const toTagObject = (tag: string): TagSuggestion => {
21-
// react-tag-autocomplete uses the value to determine if a tag is already added, removing it in that case.
22-
// Using a unique value ensures all tags are always considered new, together with a `Set` to ignore duplicates later.
23-
tagId += 1;
24-
return { label: tag, value: `${tag}${tagId}` };
25-
};
26-
27-
const buildTagRenderer = (colorGenerator: ColorGenerator) => ({ tag, onClick: deleteTag }: TagRendererProps) => (
28-
<Tag colorGenerator={colorGenerator} text={tag.label} className="react-tags__tag" onClose={deleteTag} />
29-
);
30-
const buildOptionRenderer = (colorGenerator: ColorGenerator, api: RefObject<ReactTagsAPI | null>) => (
31-
{ option, classNames: classes, ...rest }: OptionRendererProps,
32-
) => {
33-
const isSelectable = isSelectableOption(option.label);
34-
const isNew = isNewOption(option.label);
35-
36-
return (
37-
<div
38-
className={clsx(classes.option, {
39-
[classes.optionIsActive]: isSelectable && option.active,
40-
'react-tags__listbox-option--not-selectable': !isSelectable,
41-
})}
42-
{...rest}
43-
>
44-
{!isSelectable ? <i>{option.label}</i> : (
45-
<>
46-
{!isNew && <TagBullet tag={`${option.label}`} colorGenerator={colorGenerator} />}
47-
{!isNew ? option.label : <i>Add &quot;{normalizeTag(api.current?.input.value ?? '')}&quot;</i>}
48-
</>
49-
)}
50-
</div>
51-
);
52-
};
537

548
export type TagsSelectorProps = {
55-
tags: string[];
56-
selectedTags: string[];
579
onChange: (tags: string[]) => void;
58-
placeholder?: string;
59-
/** If true, it won't allow adding new tags */
60-
immutable?: boolean;
61-
};
10+
} & Pick<TagsAutocompleteProps, 'tags' | 'selectedTags' | 'placeholder' | 'immutable' | 'containerClassName'>;
6211

6312
type TagsSelectorDeps = {
6413
ColorGenerator: ColorGenerator;
6514
};
6615

67-
const TagsSelector: FCWithDeps<TagsSelectorProps, TagsSelectorDeps> = (
68-
{ selectedTags, onChange, placeholder, tags, immutable = false },
69-
) => {
16+
const TagsSelector: FCWithDeps<TagsSelectorProps, TagsSelectorDeps> = ({ onChange, placeholder, ...rest }) => {
7017
const { ColorGenerator: colorGenerator } = useDependencies(TagsSelector);
7118
const shortUrlCreation = useSetting('shortUrlCreation');
7219
const searchMode = shortUrlCreation?.tagFilteringMode ?? 'startsWith';
73-
const apiRef = useRef<ReactTagsAPI>(null);
7420

7521
return (
76-
<ReactTags
77-
ref={apiRef}
78-
selected={selectedTags.map(toTagObject)}
79-
suggestions={tags.filter((tag) => !selectedTags.includes(tag)).map(toTagObject)}
80-
renderTag={buildTagRenderer(colorGenerator)}
81-
// eslint-disable-next-line react-compiler/react-compiler
82-
renderOption={buildOptionRenderer(colorGenerator, apiRef)}
83-
activateFirstOption
84-
allowNew={!immutable}
85-
newOptionText={NEW_TAG}
86-
noOptionsText={NOT_FOUND_TAG}
87-
placeholderText={placeholder ?? 'Add tags to the URL'}
88-
delimiterKeys={['Enter', ',']}
89-
suggestionsTransform={
90-
(query, suggestions) => {
91-
const searchTerm = query.toLowerCase().trim();
92-
return searchTerm.length < 1 ? [] : [...suggestions.filter(
93-
({ label }) => (searchMode === 'includes' ? label.includes(searchTerm) : label.startsWith(searchTerm)),
94-
)].slice(0, 5);
95-
}
96-
}
97-
onDelete={(removedTagIndex) => {
98-
const tagsCopy = [...selectedTags];
99-
tagsCopy.splice(removedTagIndex, 1);
100-
onChange(tagsCopy);
101-
}}
102-
onAdd={({ label: newTag }) => onChange(
103-
// Use a Set to ignore duplicated tags.
104-
// Split any of the new tags by comma, allowing to paste multiple comma-separated tags at once.
105-
[...new Set([...selectedTags, ...newTag.split(',').map(normalizeTag)])],
106-
)}
22+
<TagsAutocomplete
23+
{...rest}
24+
onTagsChange={onChange}
25+
getColorForTag={(tag) => colorGenerator.getColorForKey(tag)}
26+
size="lg"
27+
placeholder={placeholder ?? 'Add tags to the URL'}
28+
searchMode={searchMode}
10729
/>
10830
);
10931
};

src/tags/react-tag-autocomplete.scss

Lines changed: 0 additions & 142 deletions
This file was deleted.

src/utils/components/ColorPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import { isLightColor } from '@shlinkio/shlink-frontend-kit';
34
import { Input } from '@shlinkio/shlink-frontend-kit/tailwind';
45
import { clsx } from 'clsx';
56
import { forwardRef } from 'react';
6-
import { isLightColor } from '../helpers/color';
77

88
export type ColorPickerProps = {
99
name: string;

src/utils/helpers/color.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,7 @@ import { rangeOf } from './index';
22

33
const HEX_COLOR_LENGTH = 6;
44
const HEX_DIGITS = '0123456789ABCDEF';
5-
const LIGHTNESS_BREAKPOINT = 128;
65

76
export function buildRandomColor(): string {
87
return `#${rangeOf(HEX_COLOR_LENGTH, () => HEX_DIGITS[Math.floor(Math.random() * HEX_DIGITS.length)]).join('')}`;
98
}
10-
11-
function perceivedLightness (r: number, g: number, b: number): number {
12-
return Math.round(Math.sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2));
13-
}
14-
15-
/** @deprecated. Use same symbol from @shlinkio/shlink-frontend-kit */
16-
export function isLightColor(colorHex: string): boolean {
17-
const [r, g, b] = (colorHex.match(/../g) ?? []).map((hex) => parseInt(hex, 16) || 0);
18-
return perceivedLightness(r, g, b) >= LIGHTNESS_BREAKPOINT;
19-
}

0 commit comments

Comments
 (0)