mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Added first autocomplete implementation on tags selector
This commit is contained in:
parent
1519f89318
commit
b0bce7498a
11 changed files with 184 additions and 55 deletions
|
@ -35,6 +35,7 @@
|
||||||
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
|
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
|
||||||
"react/jsx-closing-bracket-location": ["error", "tag-aligned"],
|
"react/jsx-closing-bracket-location": ["error", "tag-aligned"],
|
||||||
"react/no-array-index-key": "off",
|
"react/no-array-index-key": "off",
|
||||||
"react/no-did-update-set-state": "off"
|
"react/no-did-update-set-state": "off",
|
||||||
|
"react/display-name": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink-web-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
|
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink-web-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
|
||||||
[![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
[![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
||||||
[![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/master/LICENSE)
|
[![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/master/LICENSE)
|
||||||
|
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=alejandrocelaya%40gmail.com¤cy_code=EUR)
|
||||||
|
|
||||||
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
|
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"qs": "^6.5.2",
|
"qs": "^6.5.2",
|
||||||
"ramda": "^0.25.0",
|
"ramda": "^0.25.0",
|
||||||
"react": "^16.3.2",
|
"react": "^16.3.2",
|
||||||
|
"react-autosuggest": "^9.4.0",
|
||||||
"react-chartjs-2": "^2.7.4",
|
"react-chartjs-2": "^2.7.4",
|
||||||
"react-color": "^2.14.1",
|
"react-color": "^2.14.1",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import editIcon from '@fortawesome/fontawesome-free-solid/faPencilAlt';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import colorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
import TagBullet from '../utils/TagBullet';
|
||||||
import './TagCard.scss';
|
import './TagCard.scss';
|
||||||
import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal';
|
import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal';
|
||||||
import EditTagModal from './helpers/EditTagModal';
|
import EditTagModal from './helpers/EditTagModal';
|
||||||
|
@ -14,16 +14,12 @@ export default class TagCard extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tag: PropTypes.string,
|
tag: PropTypes.string,
|
||||||
currentServerId: PropTypes.string,
|
currentServerId: PropTypes.string,
|
||||||
colorGenerator: colorGeneratorType,
|
|
||||||
};
|
|
||||||
static defaultProps = {
|
|
||||||
colorGenerator,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = { isDeleteModalOpen: false, isEditModalOpen: false };
|
state = { isDeleteModalOpen: false, isEditModalOpen: false };
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tag, colorGenerator, currentServerId } = this.props;
|
const { tag, currentServerId } = this.props;
|
||||||
const toggleDelete = () =>
|
const toggleDelete = () =>
|
||||||
this.setState(({ isDeleteModalOpen }) => ({ isDeleteModalOpen: !isDeleteModalOpen }));
|
this.setState(({ isDeleteModalOpen }) => ({ isDeleteModalOpen: !isDeleteModalOpen }));
|
||||||
const toggleEdit = () =>
|
const toggleEdit = () =>
|
||||||
|
@ -45,10 +41,7 @@ export default class TagCard extends React.Component {
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
</button>
|
</button>
|
||||||
<h5 className="tag-card__tag-title">
|
<h5 className="tag-card__tag-title">
|
||||||
<div
|
<TagBullet tag={tag} />
|
||||||
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
|
||||||
className="tag-card__tag-bullet"
|
|
||||||
/>
|
|
||||||
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>
|
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>
|
||||||
{tag}
|
{tag}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -16,17 +16,6 @@
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-card__tag-bullet {
|
|
||||||
$width: 20px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
width: $width;
|
|
||||||
height: $width;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: -4px;
|
|
||||||
margin-right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-card__btn {
|
.tag-card__btn {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,9 +59,7 @@ export default function reducer(state = defaultState, action) {
|
||||||
case FILTER_TAGS:
|
case FILTER_TAGS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
filteredTags: state.tags.filter(
|
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(action.searchTerm)),
|
||||||
(tag) => tag.toLowerCase().match(action.searchTerm),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
24
src/utils/TagBullet.js
Normal file
24
src/utils/TagBullet.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as PropTypes from 'prop-types';
|
||||||
|
import colorGenerator, { colorGeneratorType } from './ColorGenerator';
|
||||||
|
import './TagBullet.scss';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
tag: PropTypes.string.isRequired,
|
||||||
|
colorGenerator: colorGeneratorType,
|
||||||
|
};
|
||||||
|
const defaultProps = {
|
||||||
|
colorGenerator,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TagBullet({ tag, colorGenerator }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||||
|
className="tag-bullet"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagBullet.propTypes = propTypes;
|
||||||
|
TagBullet.defaultProps = defaultProps;
|
10
src/utils/TagBullet.scss
Normal file
10
src/utils/TagBullet.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.tag-bullet {
|
||||||
|
$width: 20px;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
width: $width;
|
||||||
|
height: $width;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: -4px;
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
|
@ -1,40 +1,102 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import TagsInput from 'react-tagsinput';
|
import TagsInput from 'react-tagsinput';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Autosuggest from 'react-autosuggest';
|
||||||
|
import { pick, identity } from 'ramda';
|
||||||
|
import { listTags } from '../tags/reducers/tagsList';
|
||||||
import colorGenerator, { colorGeneratorType } from './ColorGenerator';
|
import colorGenerator, { colorGeneratorType } from './ColorGenerator';
|
||||||
|
import './TagsSelector.scss';
|
||||||
|
import TagBullet from './TagBullet';
|
||||||
|
|
||||||
const defaultProps = {
|
export class TagsSelectorComponent extends React.Component {
|
||||||
colorGenerator,
|
static propTypes = {
|
||||||
placeholder: 'Add tags to the URL',
|
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
};
|
onChange: PropTypes.func.isRequired,
|
||||||
const propTypes = {
|
placeholder: PropTypes.string,
|
||||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
colorGenerator: colorGeneratorType,
|
||||||
onChange: PropTypes.func.isRequired,
|
tagsList: PropTypes.shape({
|
||||||
placeholder: PropTypes.string,
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
colorGenerator: colorGeneratorType,
|
}),
|
||||||
};
|
};
|
||||||
|
static defaultProps = {
|
||||||
|
colorGenerator,
|
||||||
|
placeholder: 'Add tags to the URL',
|
||||||
|
};
|
||||||
|
|
||||||
export default function TagsSelector({ tags, onChange, placeholder, colorGenerator }) {
|
componentDidMount() {
|
||||||
const renderTag = ({ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }) => (
|
const { listTags } = this.props;
|
||||||
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
|
||||||
{getTagDisplayValue(tag)}
|
|
||||||
{!disabled && <span className={classNameRemove} onClick={() => onRemove(key)} />}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
listTags();
|
||||||
<TagsInput
|
}
|
||||||
value={tags}
|
|
||||||
inputProps={{ placeholder }}
|
|
||||||
onlyUnique
|
|
||||||
renderTag={renderTag}
|
|
||||||
|
|
||||||
// FIXME Workaround to be able to add tags on Android
|
render() {
|
||||||
addOnBlur
|
const { tags, onChange, placeholder, colorGenerator, tagsList } = this.props;
|
||||||
onChange={onChange}
|
const renderTag = ({ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }) => (
|
||||||
/>
|
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
||||||
);
|
{getTagDisplayValue(tag)}
|
||||||
|
{!disabled && <span className={classNameRemove} onClick={() => onRemove(key)} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
const renderAutocompleteInput = (data) => {
|
||||||
|
const { addTag, ...rest } = data;
|
||||||
|
|
||||||
|
const handleOnChange = (e, { method }) => {
|
||||||
|
if (method === 'enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
rest.onChange(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-extra-parens
|
||||||
|
const inputValue = (rest.value && rest.value.trim().toLowerCase()) || '';
|
||||||
|
const inputLength = inputValue.length;
|
||||||
|
const suggestions = tagsList.tags.filter((state) => state.toLowerCase().slice(0, inputLength) === inputValue);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autosuggest
|
||||||
|
ref={rest.ref}
|
||||||
|
suggestions={suggestions}
|
||||||
|
inputProps={{ ...rest, onChange: handleOnChange }}
|
||||||
|
highlightFirstSuggestion
|
||||||
|
shouldRenderSuggestions={(value) => value && value.trim().length > 0}
|
||||||
|
getSuggestionValue={(suggestion) => {
|
||||||
|
console.log(suggestion);
|
||||||
|
|
||||||
|
return suggestion;
|
||||||
|
}}
|
||||||
|
renderSuggestion={(suggestion) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<TagBullet tag={suggestion} />
|
||||||
|
{suggestion}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
onSuggestionSelected={(e, { suggestion }) => {
|
||||||
|
addTag(suggestion);
|
||||||
|
}}
|
||||||
|
onSuggestionsClearRequested={identity}
|
||||||
|
onSuggestionsFetchRequested={identity}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagsInput
|
||||||
|
value={tags}
|
||||||
|
inputProps={{ placeholder }}
|
||||||
|
onlyUnique
|
||||||
|
renderTag={renderTag}
|
||||||
|
renderInput={renderAutocompleteInput}
|
||||||
|
|
||||||
|
// FIXME Workaround to be able to add tags on Android
|
||||||
|
addOnBlur
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TagsSelector.defaultProps = defaultProps;
|
const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent);
|
||||||
TagsSelector.propTypes = propTypes;
|
|
||||||
|
export default TagsSelector;
|
||||||
|
|
16
src/utils/TagsSelector.scss
Normal file
16
src/utils/TagsSelector.scss
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import './base';
|
||||||
|
|
||||||
|
.react-autosuggest__suggestions-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-autosuggest__suggestion {
|
||||||
|
margin-left: -6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-autosuggest__suggestion--highlighted {
|
||||||
|
background-color: $lightGrey;
|
||||||
|
}
|
34
yarn.lock
34
yarn.lock
|
@ -5691,6 +5691,10 @@ object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
|
|
||||||
|
object-assign@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
|
||||||
|
|
||||||
object-copy@^0.1.0:
|
object-copy@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
|
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
|
||||||
|
@ -6717,6 +6721,22 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
react-autosuggest@^9.4.0:
|
||||||
|
version "9.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-9.4.0.tgz#3146bc9afa4f171bed067c542421edec5ca94294"
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.5.10"
|
||||||
|
react-autowhatever "^10.1.2"
|
||||||
|
shallow-equal "^1.0.0"
|
||||||
|
|
||||||
|
react-autowhatever@^10.1.2:
|
||||||
|
version "10.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-10.1.2.tgz#200ffc41373b2189e3f6140ac7bdb82363a79fd3"
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.5.8"
|
||||||
|
react-themeable "^1.1.0"
|
||||||
|
section-iterator "^2.0.0"
|
||||||
|
|
||||||
react-chartjs-2@^2.7.4:
|
react-chartjs-2@^2.7.4:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.7.4.tgz#e41ea4e81491dc78347111126a48e96ee57db1a6"
|
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.7.4.tgz#e41ea4e81491dc78347111126a48e96ee57db1a6"
|
||||||
|
@ -6879,6 +6899,12 @@ react-test-renderer@^16.0.0-0:
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
react-is "^16.4.2"
|
react-is "^16.4.2"
|
||||||
|
|
||||||
|
react-themeable@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e"
|
||||||
|
dependencies:
|
||||||
|
object-assign "^3.0.0"
|
||||||
|
|
||||||
react-transition-group@^2.3.1:
|
react-transition-group@^2.3.1:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
|
||||||
|
@ -7439,6 +7465,10 @@ scss-tokenizer@^0.2.3:
|
||||||
js-base64 "^2.1.8"
|
js-base64 "^2.1.8"
|
||||||
source-map "^0.4.2"
|
source-map "^0.4.2"
|
||||||
|
|
||||||
|
section-iterator@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a"
|
||||||
|
|
||||||
select-hose@^2.0.0:
|
select-hose@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||||
|
@ -7586,6 +7616,10 @@ shallow-clone@^1.0.0:
|
||||||
kind-of "^5.0.0"
|
kind-of "^5.0.0"
|
||||||
mixin-object "^2.0.1"
|
mixin-object "^2.0.1"
|
||||||
|
|
||||||
|
shallow-equal@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7"
|
||||||
|
|
||||||
shebang-command@^1.2.0:
|
shebang-command@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||||
|
|
Loading…
Reference in a new issue