From edeb3e44ff7764a5d33b91b5319942c9b4b38a91 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 27 Apr 2019 21:51:24 -0400 Subject: [PATCH] New: Use Fuse.js for UI header search --- .../Page/Header/ArtistSearchInput.js | 47 +++++++++---------- .../Page/Header/ArtistSearchInputConnector.js | 43 ++--------------- .../Page/Header/ArtistSearchResult.js | 44 ++++------------- package.json | 1 + yarn.lock | 5 ++ 5 files changed, 39 insertions(+), 101 deletions(-) diff --git a/frontend/src/Components/Page/Header/ArtistSearchInput.js b/frontend/src/Components/Page/Header/ArtistSearchInput.js index 55477b10b..eb22640ce 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchInput.js +++ b/frontend/src/Components/Page/Header/ArtistSearchInput.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Autosuggest from 'react-autosuggest'; -import jdu from 'jdu'; +import Fuse from 'fuse.js'; import { icons } from 'Helpers/Props'; import Icon from 'Components/Icon'; import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; @@ -10,6 +10,20 @@ import styles from './ArtistSearchInput.css'; const ADD_NEW_TYPE = 'addNew'; +const fuseOptions = { + shouldSort: true, + includeMatches: true, + threshold: 0.3, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + 'artistName', + 'tags.label' + ] +}; + class ArtistSearchInput extends Component { // @@ -69,16 +83,15 @@ class ArtistSearchInput extends Component { return ( ); } - goToArtist(artist) { + goToArtist(item) { this.setState({ value: '' }); - this.props.onGoToArtist(artist.foreignArtistId); + this.props.onGoToArtist(item.item.foreignArtistId); } reset() { @@ -140,24 +153,8 @@ class ArtistSearchInput extends Component { } onSuggestionsFetchRequested = ({ value }) => { - const lowerCaseValue = jdu.replace(value).toLowerCase(); - - const suggestions = this.props.artist.filter((artist) => { - // Check the title first and if there isn't a match fallback to - // the alternate titles and finally the tags. - - if (value.length === 1) { - return ( - artist.cleanName.startsWith(lowerCaseValue) || - artist.tags.some((tag) => tag.cleanLabel.startsWith(lowerCaseValue)) - ); - } - - return ( - artist.cleanName.contains(lowerCaseValue) || - artist.tags.some((tag) => tag.cleanLabel.contains(lowerCaseValue)) - ); - }); + const fuse = new Fuse(this.props.artists, fuseOptions); + const suggestions = fuse.search(value).slice(0, 15); this.setState({ suggestions }); } @@ -253,7 +250,7 @@ class ArtistSearchInput extends Component { } ArtistSearchInput.propTypes = { - artist: PropTypes.arrayOf(PropTypes.object).isRequired, + artists: PropTypes.arrayOf(PropTypes.object).isRequired, onGoToArtist: PropTypes.func.isRequired, onGoToAddNewArtist: PropTypes.func.isRequired, bindShortcut: PropTypes.func.isRequired diff --git a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js index 1bb5acba3..e42131faa 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js +++ b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js @@ -1,35 +1,14 @@ import { connect } from 'react-redux'; import { push } from 'connected-react-router'; import { createSelector } from 'reselect'; -import jdu from 'jdu'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; import ArtistSearchInput from './ArtistSearchInput'; -function createCleanTagsSelector() { - return createSelector( - createTagsSelector(), - (tags) => { - return tags.map((tag) => { - const { - id, - label - } = tag; - - return { - id, - label, - cleanLabel: jdu.replace(label).toLowerCase() - }; - }); - } - ); -} - function createCleanArtistSelector() { return createSelector( createAllArtistSelector(), - createCleanTagsSelector(), + createTagsSelector(), (allArtists, allTags) => { return allArtists.map((artist) => { const { @@ -46,26 +25,10 @@ function createCleanArtistSelector() { sortName, foreignArtistId, images, - cleanName: jdu.replace(artistName).toLowerCase(), - // alternateTitles: alternateTitles.map((alternateTitle) => { - // return { - // title: alternateTitle.title, - // cleanTitle: jdu.replace(alternateTitle.title).toLowerCase() - // }; - // }), tags: tags.map((id) => { return allTags.find((tag) => tag.id === id); }) }; - }).sort((a, b) => { - if (a.sortName < b.sortName) { - return -1; - } - if (a.sortName > b.sortName) { - return 1; - } - - return 0; }); } ); @@ -74,9 +37,9 @@ function createCleanArtistSelector() { function createMapStateToProps() { return createSelector( createCleanArtistSelector(), - (artist) => { + (artists) => { return { - artist + artists }; } ); diff --git a/frontend/src/Components/Page/Header/ArtistSearchResult.js b/frontend/src/Components/Page/Header/ArtistSearchResult.js index 3112f7b4e..9e8511918 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchResult.js +++ b/frontend/src/Components/Page/Header/ArtistSearchResult.js @@ -5,38 +5,18 @@ import Label from 'Components/Label'; import ArtistPoster from 'Artist/ArtistPoster'; import styles from './ArtistSearchResult.css'; -// function findMatchingAlternateTitle(alternateTitles, cleanQuery) { -// return alternateTitles.find((alternateTitle) => { -// return alternateTitle.cleanTitle.contains(cleanQuery); -// }); -// } - -function getMatchingTag(tags, cleanQuery) { - return tags.find((tag) => { - return tag.cleanLabel.contains(cleanQuery); - }); -} - function ArtistSearchResult(props) { const { - cleanQuery, + match, artistName, - cleanName, images, - // alternateTitles, tags } = props; - const titleContains = cleanName.contains(cleanQuery); - // let alternateTitle = null; let tag = null; - // if (!titleContains) { - // alternateTitle = findMatchingAlternateTitle(alternateTitles, cleanQuery); - // } - - if (!titleContains) { // && !alternateTitle) { - tag = getMatchingTag(tags, cleanQuery); + if (match.key === 'tags.label') { + tag = tags[match.arrayIndex]; } return ( @@ -55,14 +35,7 @@ function ArtistSearchResult(props) { { - // !!alternateTitle && - //
- // {alternateTitle.title} - //
- } - - { - !!tag && + tag ?
-
+ : + null } @@ -78,12 +52,10 @@ function ArtistSearchResult(props) { } ArtistSearchResult.propTypes = { - cleanQuery: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired, - // alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, - cleanName: PropTypes.string.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, - tags: PropTypes.arrayOf(PropTypes.object).isRequired + tags: PropTypes.arrayOf(PropTypes.object).isRequired, + match: PropTypes.object.isRequired }; export default ArtistSearchResult; diff --git a/package.json b/package.json index 654eef82f..10122ca4f 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "esprint": "0.4.0", "file-loader": "3.0.1", "filesize": "4.1.2", + "fuse.js": "3.4.2", "gulp": "4.0.0", "gulp-cached": "1.1.1", "gulp-concat": "2.6.1", diff --git a/yarn.lock b/yarn.lock index 0f6b4073b..c155749ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3695,6 +3695,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +fuse.js@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.2.tgz#d7a638c436ecd7b9c4c0051478c09594eb956212" + integrity sha512-WVbrm+cAxPtyMqdtL7cYhR7aZJPhtOfjNClPya8GKMVukKDYs7pEnPINeRVX1C9WmWgU8MdYGYbUPAP2AJXdoQ== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"