From 0219e62979274c0326a6f9c8ff6680cbce81b61f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 27 Feb 2019 17:52:05 -0800 Subject: [PATCH] Use fuse.js for series searching in UI Closes #2954 --- .../Page/Header/SeriesSearchInput.js | 47 ++++++++++--------- .../Page/Header/SeriesSearchInputConnector.js | 41 +--------------- .../Page/Header/SeriesSearchResult.js | 41 +++++----------- package.json | 1 + yarn.lock | 5 ++ 5 files changed, 47 insertions(+), 88 deletions(-) diff --git a/frontend/src/Components/Page/Header/SeriesSearchInput.js b/frontend/src/Components/Page/Header/SeriesSearchInput.js index a58dfecc3..a2b70addb 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchInput.js +++ b/frontend/src/Components/Page/Header/SeriesSearchInput.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,21 @@ import styles from './SeriesSearchInput.css'; const ADD_NEW_TYPE = 'addNew'; +const fuseOptions = { + shouldSort: true, + includeMatches: true, + threshold: 0.3, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + 'title', + 'alternateTitles.title', + 'tags.label' + ] +}; + class SeriesSearchInput extends Component { // @@ -69,9 +84,8 @@ class SeriesSearchInput extends Component { return ( ); } @@ -140,25 +154,16 @@ class SeriesSearchInput extends Component { } onSuggestionsFetchRequested = ({ value }) => { - const lowerCaseValue = jdu.replace(value).toLowerCase(); - - const suggestions = this.props.series.filter((series) => { - // 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 ( - series.cleanTitle.startsWith(lowerCaseValue) || - series.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.startsWith(lowerCaseValue)) || - series.tags.some((tag) => tag.cleanLabel.startsWith(lowerCaseValue)) - ); + const fuse = new Fuse(this.props.series, fuseOptions); + const suggestions = fuse.search(value).sort((a, b) => { + if (a.item.sortTitle < b.item.sortTitle) { + return -1; + } + if (a.item.sortTitle > b.item.sortTitle) { + return 1; } - return ( - series.cleanTitle.contains(lowerCaseValue) || - series.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.contains(lowerCaseValue)) || - series.tags.some((tag) => tag.cleanLabel.contains(lowerCaseValue)) - ); + return 0; }); this.setState({ suggestions }); diff --git a/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js b/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js index 9269929e6..eef0b6706 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js +++ b/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js @@ -1,35 +1,14 @@ import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import { createSelector } from 'reselect'; -import jdu from 'jdu'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; import SeriesSearchInput from './SeriesSearchInput'; -function createCleanTagsSelector() { - return createSelector( - createTagsSelector(), - (tags) => { - return tags.map((tag) => { - const { - id, - label - } = tag; - - return { - id, - label, - cleanLabel: jdu.replace(label).toLowerCase() - }; - }); - } - ); -} - function createCleanSeriesSelector() { return createSelector( createAllSeriesSelector(), - createCleanTagsSelector(), + createTagsSelector(), (allSeries, allTags) => { return allSeries.map((series) => { const { @@ -46,27 +25,11 @@ function createCleanSeriesSelector() { titleSlug, sortTitle, images, - cleanTitle: jdu.replace(title).toLowerCase(), - alternateTitles: alternateTitles.map((alternateTitle) => { - return { - title: alternateTitle.title, - sortTitle: alternateTitle.sortTitle, - cleanTitle: jdu.replace(alternateTitle.title).toLowerCase() - }; - }), + alternateTitles, tags: tags.map((id) => { return allTags.find((tag) => tag.id === id); }) }; - }).sort((a, b) => { - if (a.sortTitle < b.sortTitle) { - return -1; - } - if (a.sortTitle > b.sortTitle) { - return 1; - } - - return 0; }); } ); diff --git a/frontend/src/Components/Page/Header/SeriesSearchResult.js b/frontend/src/Components/Page/Header/SeriesSearchResult.js index 9e2adb82c..09ad9d983 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchResult.js +++ b/frontend/src/Components/Page/Header/SeriesSearchResult.js @@ -5,38 +5,22 @@ import Label from 'Components/Label'; import SeriesPoster from 'Series/SeriesPoster'; import styles from './SeriesSearchResult.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 SeriesSearchResult(props) { const { - cleanQuery, + match, title, - cleanTitle, images, alternateTitles, tags } = props; - const titleContains = cleanTitle.contains(cleanQuery); let alternateTitle = null; let tag = null; - if (!titleContains) { - alternateTitle = findMatchingAlternateTitle(alternateTitles, cleanQuery); - } - - if (!titleContains && !alternateTitle) { - tag = getMatchingTag(tags, cleanQuery); + if (match.key === 'alternateTitles.cleanTitle') { + alternateTitle = alternateTitles[match.arrayIndex]; + } else if (match.key === 'tags.label') { + tag = tags[match.arrayIndex]; } return ( @@ -55,14 +39,15 @@ function SeriesSearchResult(props) { { - !!alternateTitle && + alternateTitle ?
{alternateTitle.title} -
+ : + null } { - !!tag && + tag ?
-
+ : + null } @@ -78,12 +64,11 @@ function SeriesSearchResult(props) { } SeriesSearchResult.propTypes = { - cleanQuery: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - cleanTitle: PropTypes.string.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, - tags: PropTypes.arrayOf(PropTypes.object).isRequired + tags: PropTypes.arrayOf(PropTypes.object).isRequired, + match: PropTypes.object.isRequired }; export default SeriesSearchResult; diff --git a/package.json b/package.json index fc38e6d77..1821bf4f1 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "extract-text-webpack-plugin": "3.0.2", "file-loader": "1.1.6", "filesize": "3.6.1", + "fuse.js": "3.4.2", "gulp": "3.9.1", "gulp-cached": "1.1.1", "gulp-clean-css": "3.10.0", diff --git a/yarn.lock b/yarn.lock index fa01bce74..2399b83ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3529,6 +3529,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"