Merge pull request #4201 from hashicorp/f-ui-fuzzy-job-search
UI: fuzzy and tokenized job search
This commit is contained in:
commit
4774a16bcd
|
@ -25,6 +25,8 @@ export default Controller.extend(Sortable, Searchable, {
|
|||
sortDescending: true,
|
||||
|
||||
searchProps: computed(() => ['id', 'name']),
|
||||
fuzzySearchProps: computed(() => ['name']),
|
||||
fuzzySearchEnabled: true,
|
||||
|
||||
/**
|
||||
Filtered jobs are those that match the selected namespace and aren't children
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import { get, computed } from '@ember/object';
|
||||
import { reads } from '@ember/object/computed';
|
||||
import Fuse from 'npm:fuse.js';
|
||||
|
||||
/**
|
||||
Searchable mixin
|
||||
|
@ -9,6 +11,12 @@ import { get, computed } from '@ember/object';
|
|||
Properties to override:
|
||||
- searchTerm: the string to use as a query
|
||||
- searchProps: the props on each object to search
|
||||
-- exactMatchSearchProps: the props for exact search when props are different per search type
|
||||
-- regexSearchProps: the props for regex search when props are different per search type
|
||||
-- fuzzySearchProps: the props for fuzzy search when props are different per search type
|
||||
- exactMatchEnabled: (true) disable to not use the exact match search type
|
||||
- fuzzySearchEnabled: (false) enable to use the fuzzy search type
|
||||
- regexEnabled: (true) disable to disable the regex search type
|
||||
- listToSearch: the list of objects to search
|
||||
|
||||
Properties provided:
|
||||
|
@ -17,17 +25,83 @@ import { get, computed } from '@ember/object';
|
|||
export default Mixin.create({
|
||||
searchTerm: '',
|
||||
listToSearch: computed(() => []),
|
||||
searchProps: null,
|
||||
|
||||
listSearched: computed('searchTerm', 'listToSearch.[]', 'searchProps.[]', function() {
|
||||
const searchTerm = this.get('searchTerm');
|
||||
if (searchTerm && searchTerm.length) {
|
||||
return regexSearch(searchTerm, this.get('listToSearch'), this.get('searchProps'));
|
||||
}
|
||||
return this.get('listToSearch');
|
||||
searchProps: null,
|
||||
exactMatchSearchProps: reads('searchProps'),
|
||||
regexSearchProps: reads('searchProps'),
|
||||
fuzzySearchProps: reads('searchProps'),
|
||||
|
||||
// Three search modes
|
||||
exactMatchEnabled: true,
|
||||
fuzzySearchEnabled: false,
|
||||
regexEnabled: true,
|
||||
|
||||
fuse: computed('listToSearch.[]', 'fuzzySearchProps.[]', function() {
|
||||
return new Fuse(this.get('listToSearch'), {
|
||||
shouldSort: true,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
tokenize: true,
|
||||
matchAllTokens: true,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: this.get('fuzzySearchProps') || [],
|
||||
getFn(item, key) {
|
||||
return get(item, key);
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
listSearched: computed(
|
||||
'searchTerm',
|
||||
'listToSearch.[]',
|
||||
'exactMatchEnabled',
|
||||
'fuzzySearchEnabled',
|
||||
'regexEnabled',
|
||||
'exactMatchSearchProps.[]',
|
||||
'fuzzySearchProps.[]',
|
||||
'regexSearchProps.[]',
|
||||
function() {
|
||||
const searchTerm = this.get('searchTerm').trim();
|
||||
|
||||
if (!searchTerm || !searchTerm.length) {
|
||||
return this.get('listToSearch');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
if (this.get('exactMatchEnabled')) {
|
||||
results.push(
|
||||
...exactMatchSearch(
|
||||
searchTerm,
|
||||
this.get('listToSearch'),
|
||||
this.get('exactMatchSearchProps')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get('fuzzySearchEnabled')) {
|
||||
results.push(...this.get('fuse').search(searchTerm));
|
||||
}
|
||||
|
||||
if (this.get('regexEnabled')) {
|
||||
results.push(
|
||||
...regexSearch(searchTerm, this.get('listToSearch'), this.get('regexSearchProps'))
|
||||
);
|
||||
}
|
||||
|
||||
return results.uniq();
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
function exactMatchSearch(term, list, keys) {
|
||||
if (term.length) {
|
||||
return list.filter(item => keys.some(key => get(item, key) === term));
|
||||
}
|
||||
}
|
||||
|
||||
function regexSearch(term, list, keys) {
|
||||
if (term.length) {
|
||||
try {
|
||||
|
@ -38,5 +112,6 @@ function regexSearch(term, list, keys) {
|
|||
} catch (e) {
|
||||
// Swallow the error; most likely due to an eager search of an incomplete regex
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,9 @@
|
|||
<nav class="pagination">
|
||||
<div class="pagination-numbers">
|
||||
{{p.startsAt}}–{{p.endsAt}} of {{sortedJobs.length}}
|
||||
{{#if searchTerm}}
|
||||
<em>({{dec sortedJobs.length filteredJobs.length}} hidden by search term)</em>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#p.prev class="pagination-previous"}} < {{/p.prev}}
|
||||
{{#p.next class="pagination-next"}} > {{/p.next}}
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
"ember-welcome-page": "^3.0.0",
|
||||
"eslint": "^4.13.1",
|
||||
"flat": "^4.0.0",
|
||||
"fuse.js": "~3.2.0",
|
||||
"husky": "^0.14.3",
|
||||
"json-formatter-js": "^2.2.0",
|
||||
"lint-staged": "^6.0.0",
|
||||
|
|
|
@ -55,3 +55,124 @@ test('the searchable mixin only searches the declared search props', function(as
|
|||
'Only USA matched, since continent is not a search prop'
|
||||
);
|
||||
});
|
||||
|
||||
test('the fuzzy search mode is off by default', function(assert) {
|
||||
const subject = this.subject();
|
||||
subject.set('source', [
|
||||
{ id: '1', name: 'United States of America', continent: 'North America' },
|
||||
{ id: '2', name: 'Canada', continent: 'North America' },
|
||||
{ id: '3', name: 'Mexico', continent: 'North America' },
|
||||
]);
|
||||
|
||||
subject.set('searchTerm', 'Ameerica');
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[],
|
||||
'Nothing is matched since America is spelled incorrectly'
|
||||
);
|
||||
});
|
||||
|
||||
test('the fuzzy search mode can be enabled', function(assert) {
|
||||
const subject = this.subject();
|
||||
subject.set('source', [
|
||||
{ id: '1', name: 'United States of America', continent: 'North America' },
|
||||
{ id: '2', name: 'Canada', continent: 'North America' },
|
||||
{ id: '3', name: 'Mexico', continent: 'North America' },
|
||||
]);
|
||||
|
||||
subject.set('fuzzySearchEnabled', true);
|
||||
subject.set('searchTerm', 'Ameerica');
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[{ id: '1', name: 'United States of America', continent: 'North America' }],
|
||||
'America is matched due to fuzzy matching'
|
||||
);
|
||||
});
|
||||
|
||||
test('the exact match search mode can be disabled', function(assert) {
|
||||
const subject = this.subject();
|
||||
subject.set('source', [
|
||||
{ id: '1', name: 'United States of America', continent: 'North America' },
|
||||
{ id: '2', name: 'Canada', continent: 'North America' },
|
||||
{ id: '3', name: 'Mexico', continent: 'North America' },
|
||||
]);
|
||||
|
||||
subject.set('regexSearchProps', []);
|
||||
subject.set('searchTerm', 'Mexico');
|
||||
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[{ id: '3', name: 'Mexico', continent: 'North America' }],
|
||||
'Mexico is matched exactly'
|
||||
);
|
||||
|
||||
subject.set('exactMatchEnabled', false);
|
||||
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[],
|
||||
'Nothing is matched now that exactMatch is disabled'
|
||||
);
|
||||
});
|
||||
|
||||
test('the regex search mode can be disabled', function(assert) {
|
||||
const subject = this.subject();
|
||||
subject.set('source', [
|
||||
{ id: '1', name: 'United States of America', continent: 'North America' },
|
||||
{ id: '2', name: 'Canada', continent: 'North America' },
|
||||
{ id: '3', name: 'Mexico', continent: 'North America' },
|
||||
]);
|
||||
|
||||
subject.set('searchTerm', '^.{6}$');
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[
|
||||
{ id: '2', name: 'Canada', continent: 'North America' },
|
||||
{ id: '3', name: 'Mexico', continent: 'North America' },
|
||||
],
|
||||
'Canada and Mexico meet the regex criteria'
|
||||
);
|
||||
|
||||
subject.set('regexEnabled', false);
|
||||
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[],
|
||||
'Nothing is matched now that regex is disabled'
|
||||
);
|
||||
});
|
||||
|
||||
test('each search mode has independent search props', function(assert) {
|
||||
const subject = this.subject();
|
||||
subject.set('source', [
|
||||
{ id: '1', name: 'United States of America', continent: 'North America' },
|
||||
{ id: '2', name: 'Canada', continent: 'North America' },
|
||||
{ id: '3', name: 'Mexico', continent: 'North America' },
|
||||
]);
|
||||
|
||||
subject.set('fuzzySearchEnabled', true);
|
||||
subject.set('regexSearchProps', ['id']);
|
||||
subject.set('exactMatchSearchProps', ['continent']);
|
||||
subject.set('fuzzySearchProps', ['name']);
|
||||
|
||||
subject.set('searchTerm', 'Nor America');
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[],
|
||||
'Not an exact match on continent, not a matchAllTokens match on fuzzy, not a regex match on id'
|
||||
);
|
||||
|
||||
subject.set('searchTerm', 'America States');
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[{ id: '1', name: 'United States of America', continent: 'North America' }],
|
||||
'Fuzzy match on one country, but not an exact match on continent'
|
||||
);
|
||||
|
||||
subject.set('searchTerm', '^(.a){3}$');
|
||||
assert.deepEqual(
|
||||
subject.get('listSearched'),
|
||||
[],
|
||||
'Canada is not matched by the regex because only id is looked at for regex search'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -4280,6 +4280,10 @@ functional-red-black-tree@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
|
||||
fuse.js@~3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4"
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
|
|
Loading…
Reference in a new issue