diff --git a/.eslintrc b/.eslintrc index 8839bdef..7207fac3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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" } } diff --git a/README.md b/README.md index 88727204..3b2b1940 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/package.json b/package.json index 9280a206..55c84580 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/tags/TagCard.js b/src/tags/TagCard.js index b4e65959..3222a37d 100644 --- a/src/tags/TagCard.js +++ b/src/tags/TagCard.js @@ -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 {
-
+ {tag} diff --git a/src/tags/TagCard.scss b/src/tags/TagCard.scss index 6e2b56d7..c30300c3 100644 --- a/src/tags/TagCard.scss +++ b/src/tags/TagCard.scss @@ -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; } diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index 5cd229a0..cb415902 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -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; diff --git a/src/utils/TagBullet.js b/src/utils/TagBullet.js new file mode 100644 index 00000000..ccdc4801 --- /dev/null +++ b/src/utils/TagBullet.js @@ -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 ( +
+ ); +} + +TagBullet.propTypes = propTypes; +TagBullet.defaultProps = defaultProps; diff --git a/src/utils/TagBullet.scss b/src/utils/TagBullet.scss new file mode 100644 index 00000000..ad795a9d --- /dev/null +++ b/src/utils/TagBullet.scss @@ -0,0 +1,10 @@ +.tag-bullet { + $width: 20px; + + border-radius: 50%; + width: $width; + height: $width; + display: inline-block; + vertical-align: -4px; + margin-right: 7px; +} diff --git a/src/utils/TagsSelector.js b/src/utils/TagsSelector.js index 0630912d..5b8eeb00 100644 --- a/src/utils/TagsSelector.js +++ b/src/utils/TagsSelector.js @@ -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 }) => ( - - {getTagDisplayValue(tag)} - {!disabled && onRemove(key)} />} - - ); + componentDidMount() { + const { listTags } = this.props; - return ( - - ); + render() { + const { tags, onChange, placeholder, colorGenerator, tagsList } = this.props; + const renderTag = ({ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }) => ( + + {getTagDisplayValue(tag)} + {!disabled && onRemove(key)} />} + + ); + 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 ( + value && value.trim().length > 0} + getSuggestionValue={(suggestion) => { + console.log(suggestion); + + return suggestion; + }} + renderSuggestion={(suggestion) => ( + + + {suggestion} + + )} + onSuggestionSelected={(e, { suggestion }) => { + addTag(suggestion); + }} + onSuggestionsClearRequested={identity} + onSuggestionsFetchRequested={identity} + /> + ); + }; + + return ( + + ); + } } -TagsSelector.defaultProps = defaultProps; -TagsSelector.propTypes = propTypes; +const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent); + +export default TagsSelector; diff --git a/src/utils/TagsSelector.scss b/src/utils/TagsSelector.scss new file mode 100644 index 00000000..24a68c8e --- /dev/null +++ b/src/utils/TagsSelector.scss @@ -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; +} diff --git a/yarn.lock b/yarn.lock index f3deac78..a2b0dc21 100644 --- a/yarn.lock +++ b/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"