mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-07 21:42:16 -07:00
New: Use Fuse.js for UI header search
This commit is contained in:
parent
5d1a5ee946
commit
edeb3e44ff
5 changed files with 39 additions and 101 deletions
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Autosuggest from 'react-autosuggest';
|
import Autosuggest from 'react-autosuggest';
|
||||||
import jdu from 'jdu';
|
import Fuse from 'fuse.js';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
||||||
|
@ -10,6 +10,20 @@ import styles from './ArtistSearchInput.css';
|
||||||
|
|
||||||
const ADD_NEW_TYPE = 'addNew';
|
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 {
|
class ArtistSearchInput extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -69,16 +83,15 @@ class ArtistSearchInput extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArtistSearchResult
|
<ArtistSearchResult
|
||||||
query={query}
|
{...item.item}
|
||||||
cleanQuery={jdu.replace(query).toLowerCase()}
|
match={item.matches[0]}
|
||||||
{...item}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
goToArtist(artist) {
|
goToArtist(item) {
|
||||||
this.setState({ value: '' });
|
this.setState({ value: '' });
|
||||||
this.props.onGoToArtist(artist.foreignArtistId);
|
this.props.onGoToArtist(item.item.foreignArtistId);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
@ -140,24 +153,8 @@ class ArtistSearchInput extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuggestionsFetchRequested = ({ value }) => {
|
onSuggestionsFetchRequested = ({ value }) => {
|
||||||
const lowerCaseValue = jdu.replace(value).toLowerCase();
|
const fuse = new Fuse(this.props.artists, fuseOptions);
|
||||||
|
const suggestions = fuse.search(value).slice(0, 15);
|
||||||
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))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ suggestions });
|
this.setState({ suggestions });
|
||||||
}
|
}
|
||||||
|
@ -253,7 +250,7 @@ class ArtistSearchInput extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistSearchInput.propTypes = {
|
ArtistSearchInput.propTypes = {
|
||||||
artist: PropTypes.arrayOf(PropTypes.object).isRequired,
|
artists: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onGoToArtist: PropTypes.func.isRequired,
|
onGoToArtist: PropTypes.func.isRequired,
|
||||||
onGoToAddNewArtist: PropTypes.func.isRequired,
|
onGoToAddNewArtist: PropTypes.func.isRequired,
|
||||||
bindShortcut: PropTypes.func.isRequired
|
bindShortcut: PropTypes.func.isRequired
|
||||||
|
|
|
@ -1,35 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'connected-react-router';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import jdu from 'jdu';
|
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import ArtistSearchInput from './ArtistSearchInput';
|
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() {
|
function createCleanArtistSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createAllArtistSelector(),
|
createAllArtistSelector(),
|
||||||
createCleanTagsSelector(),
|
createTagsSelector(),
|
||||||
(allArtists, allTags) => {
|
(allArtists, allTags) => {
|
||||||
return allArtists.map((artist) => {
|
return allArtists.map((artist) => {
|
||||||
const {
|
const {
|
||||||
|
@ -46,26 +25,10 @@ function createCleanArtistSelector() {
|
||||||
sortName,
|
sortName,
|
||||||
foreignArtistId,
|
foreignArtistId,
|
||||||
images,
|
images,
|
||||||
cleanName: jdu.replace(artistName).toLowerCase(),
|
|
||||||
// alternateTitles: alternateTitles.map((alternateTitle) => {
|
|
||||||
// return {
|
|
||||||
// title: alternateTitle.title,
|
|
||||||
// cleanTitle: jdu.replace(alternateTitle.title).toLowerCase()
|
|
||||||
// };
|
|
||||||
// }),
|
|
||||||
tags: tags.map((id) => {
|
tags: tags.map((id) => {
|
||||||
return allTags.find((tag) => tag.id === 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() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createCleanArtistSelector(),
|
createCleanArtistSelector(),
|
||||||
(artist) => {
|
(artists) => {
|
||||||
return {
|
return {
|
||||||
artist
|
artists
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,38 +5,18 @@ import Label from 'Components/Label';
|
||||||
import ArtistPoster from 'Artist/ArtistPoster';
|
import ArtistPoster from 'Artist/ArtistPoster';
|
||||||
import styles from './ArtistSearchResult.css';
|
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) {
|
function ArtistSearchResult(props) {
|
||||||
const {
|
const {
|
||||||
cleanQuery,
|
match,
|
||||||
artistName,
|
artistName,
|
||||||
cleanName,
|
|
||||||
images,
|
images,
|
||||||
// alternateTitles,
|
|
||||||
tags
|
tags
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const titleContains = cleanName.contains(cleanQuery);
|
|
||||||
// let alternateTitle = null;
|
|
||||||
let tag = null;
|
let tag = null;
|
||||||
|
|
||||||
// if (!titleContains) {
|
if (match.key === 'tags.label') {
|
||||||
// alternateTitle = findMatchingAlternateTitle(alternateTitles, cleanQuery);
|
tag = tags[match.arrayIndex];
|
||||||
// }
|
|
||||||
|
|
||||||
if (!titleContains) { // && !alternateTitle) {
|
|
||||||
tag = getMatchingTag(tags, cleanQuery);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -55,14 +35,7 @@ function ArtistSearchResult(props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
// !!alternateTitle &&
|
tag ?
|
||||||
// <div className={styles.alternateTitle}>
|
|
||||||
// {alternateTitle.title}
|
|
||||||
// </div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!tag &&
|
|
||||||
<div className={styles.tagContainer}>
|
<div className={styles.tagContainer}>
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
|
@ -70,7 +43,8 @@ function ArtistSearchResult(props) {
|
||||||
>
|
>
|
||||||
{tag.label}
|
{tag.label}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,12 +52,10 @@ function ArtistSearchResult(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistSearchResult.propTypes = {
|
ArtistSearchResult.propTypes = {
|
||||||
cleanQuery: PropTypes.string.isRequired,
|
|
||||||
artistName: PropTypes.string.isRequired,
|
artistName: PropTypes.string.isRequired,
|
||||||
// alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
cleanName: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).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;
|
export default ArtistSearchResult;
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
"esprint": "0.4.0",
|
"esprint": "0.4.0",
|
||||||
"file-loader": "3.0.1",
|
"file-loader": "3.0.1",
|
||||||
"filesize": "4.1.2",
|
"filesize": "4.1.2",
|
||||||
|
"fuse.js": "3.4.2",
|
||||||
"gulp": "4.0.0",
|
"gulp": "4.0.0",
|
||||||
"gulp-cached": "1.1.1",
|
"gulp-cached": "1.1.1",
|
||||||
"gulp-concat": "2.6.1",
|
"gulp-concat": "2.6.1",
|
||||||
|
|
|
@ -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"
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
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:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue