Added first autocomplete implementation on tags selector

This commit is contained in:
Alejandro Celaya 2018-08-31 18:00:33 +02:00
parent 1519f89318
commit b0bce7498a
11 changed files with 184 additions and 55 deletions

View file

@ -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"
} }
} }

View file

@ -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&currency_code=EUR)
A ReactJS-based progressive web application for [Shlink](https://shlink.io). A ReactJS-based progressive web application for [Shlink](https://shlink.io).

View file

@ -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",

View file

@ -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>

View file

@ -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;
} }

View file

@ -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
View 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
View 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;
}

View file

@ -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;

View 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;
}

View file

@ -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"