mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-22 17:10:26 +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-closing-bracket-location": ["error", "tag-aligned"],
|
||||
"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)
|
||||
[![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)
|
||||
[![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).
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"qs": "^6.5.2",
|
||||
"ramda": "^0.25.0",
|
||||
"react": "^16.3.2",
|
||||
"react-autosuggest": "^9.4.0",
|
||||
"react-chartjs-2": "^2.7.4",
|
||||
"react-color": "^2.14.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 React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import colorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
||||
import TagBullet from '../utils/TagBullet';
|
||||
import './TagCard.scss';
|
||||
import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal';
|
||||
import EditTagModal from './helpers/EditTagModal';
|
||||
|
@ -14,16 +14,12 @@ export default class TagCard extends React.Component {
|
|||
static propTypes = {
|
||||
tag: PropTypes.string,
|
||||
currentServerId: PropTypes.string,
|
||||
colorGenerator: colorGeneratorType,
|
||||
};
|
||||
static defaultProps = {
|
||||
colorGenerator,
|
||||
};
|
||||
|
||||
state = { isDeleteModalOpen: false, isEditModalOpen: false };
|
||||
|
||||
render() {
|
||||
const { tag, colorGenerator, currentServerId } = this.props;
|
||||
const { tag, currentServerId } = this.props;
|
||||
const toggleDelete = () =>
|
||||
this.setState(({ isDeleteModalOpen }) => ({ isDeleteModalOpen: !isDeleteModalOpen }));
|
||||
const toggleEdit = () =>
|
||||
|
@ -45,10 +41,7 @@ export default class TagCard extends React.Component {
|
|||
<FontAwesomeIcon icon={editIcon} />
|
||||
</button>
|
||||
<h5 className="tag-card__tag-title">
|
||||
<div
|
||||
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||
className="tag-card__tag-bullet"
|
||||
/>
|
||||
<TagBullet tag={tag} />
|
||||
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>
|
||||
{tag}
|
||||
</Link>
|
||||
|
|
|
@ -16,17 +16,6 @@
|
|||
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 {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -59,9 +59,7 @@ export default function reducer(state = defaultState, action) {
|
|||
case FILTER_TAGS:
|
||||
return {
|
||||
...state,
|
||||
filteredTags: state.tags.filter(
|
||||
(tag) => tag.toLowerCase().match(action.searchTerm),
|
||||
),
|
||||
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(action.searchTerm)),
|
||||
};
|
||||
default:
|
||||
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 { connect } from 'react-redux';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
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 './TagsSelector.scss';
|
||||
import TagBullet from './TagBullet';
|
||||
|
||||
const defaultProps = {
|
||||
colorGenerator,
|
||||
placeholder: 'Add tags to the URL',
|
||||
};
|
||||
const propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
colorGenerator: colorGeneratorType,
|
||||
};
|
||||
export class TagsSelectorComponent extends React.Component {
|
||||
static propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
colorGenerator: colorGeneratorType,
|
||||
tagsList: PropTypes.shape({
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
};
|
||||
static defaultProps = {
|
||||
colorGenerator,
|
||||
placeholder: 'Add tags to the URL',
|
||||
};
|
||||
|
||||
export default function TagsSelector({ tags, onChange, placeholder, colorGenerator }) {
|
||||
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>
|
||||
);
|
||||
componentDidMount() {
|
||||
const { listTags } = this.props;
|
||||
|
||||
return (
|
||||
<TagsInput
|
||||
value={tags}
|
||||
inputProps={{ placeholder }}
|
||||
onlyUnique
|
||||
renderTag={renderTag}
|
||||
listTags();
|
||||
}
|
||||
|
||||
// FIXME Workaround to be able to add tags on Android
|
||||
addOnBlur
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
render() {
|
||||
const { tags, onChange, placeholder, colorGenerator, tagsList } = this.props;
|
||||
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;
|
||||
TagsSelector.propTypes = propTypes;
|
||||
const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent);
|
||||
|
||||
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"
|
||||
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:
|
||||
version "0.1.0"
|
||||
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"
|
||||
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:
|
||||
version "2.7.4"
|
||||
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"
|
||||
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:
|
||||
version "2.4.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
|
|
Loading…
Reference in a new issue