Implement more feature-full autocomplete for the search bar
This change further modifies the search bar, allowing for autocomplete of project names, reviewer account names, and group names in specific cases of query predicates. Bug: Issue 4276 Change-Id: I065f97b59814d60d124c7c95ef5fc754db64ac40
This commit is contained in:
@@ -18,6 +18,8 @@ limitations under the License.
|
||||
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
|
||||
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
|
||||
|
||||
<dom-module id="gr-search-bar">
|
||||
<template>
|
||||
@@ -53,6 +55,7 @@ limitations under the License.
|
||||
multi
|
||||
borderless></gr-autocomplete>
|
||||
<gr-button id="searchButton">Search</gr-button>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
</form>
|
||||
</template>
|
||||
<script src="gr-search-bar.js"></script>
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
'tr',
|
||||
];
|
||||
|
||||
var MAX_AUTOCOMPLETE_RESULTS = 10;
|
||||
|
||||
var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
|
||||
|
||||
Polymer({
|
||||
is: 'gr-search-bar',
|
||||
|
||||
@@ -124,34 +128,144 @@
|
||||
page.show('/q/' + encodeURIComponent(encodeURIComponent(this._inputVal)));
|
||||
},
|
||||
|
||||
// TODO(kaspern): Flesh this out better.
|
||||
_makeSuggestion: function(str) {
|
||||
return {
|
||||
name: str,
|
||||
value: str,
|
||||
};
|
||||
/**
|
||||
* Fetch from the API the predicted accounts.
|
||||
* @param {string} predicate - The first part of the search term, e.g.
|
||||
* 'owner'
|
||||
* @param {string} expression - The second part of the search term, e.g.
|
||||
* 'kasp'
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchAccounts: function(predicate, expression) {
|
||||
if (expression.length === 0) { return Promise.resolve([]); }
|
||||
return this.$.restAPI.getSuggestedAccounts(
|
||||
expression,
|
||||
MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(function(accounts) {
|
||||
if (!accounts) { return []; }
|
||||
return accounts.map(function(acct) {
|
||||
return predicate + ':"' + acct.name + ' <' + acct.email + '>"';
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// TODO(kaspern): Expand support for more complicated autocomplete features.
|
||||
_getSearchSuggestions: function(input) {
|
||||
return Promise.resolve(SEARCH_OPERATORS).then(function(operators) {
|
||||
if (!operators) { return []; }
|
||||
var lowerCaseInput = input
|
||||
.substring(input.lastIndexOf(' ') + 1)
|
||||
.toLowerCase();
|
||||
return operators
|
||||
/**
|
||||
* Fetch from the API the predicted groups.
|
||||
* @param {string} predicate - The first part of the search term, e.g.
|
||||
* 'ownerin'
|
||||
* @param {string} expression - The second part of the search term, e.g.
|
||||
* 'polyger'
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchGroups: function(predicate, expression) {
|
||||
if (expression.length === 0) { return Promise.resolve([]); }
|
||||
return this.$.restAPI.getSuggestedGroups(
|
||||
expression,
|
||||
MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(function(groups) {
|
||||
if (!groups) { return []; }
|
||||
var keys = Object.keys(groups);
|
||||
return keys.map(function(key) { return predicate + ':' + key; });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch from the API the predicted projects.
|
||||
* @param {string} predicate - The first part of the search term, e.g.
|
||||
* 'project'
|
||||
* @param {string} expression - The second part of the search term, e.g.
|
||||
* 'gerr'
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchProjects: function(predicate, expression) {
|
||||
return this.$.restAPI.getSuggestedProjects(
|
||||
expression,
|
||||
MAX_AUTOCOMPLETE_RESULTS)
|
||||
.then(function(projects) {
|
||||
if (!projects) { return []; }
|
||||
var keys = Object.keys(projects);
|
||||
return keys.map(function(key) { return predicate + ':' + key; });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine what array of possible suggestions should be provided
|
||||
* to _getSearchSuggestions.
|
||||
* @param {string} input - The full search term, in lowercase.
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_fetchSuggestions: function(input) {
|
||||
// Split the input on colon to get a two part predicate/expression.
|
||||
var splitInput = input.split(':');
|
||||
var predicate = splitInput[0];
|
||||
var expression = splitInput[1] || '';
|
||||
// Switch on the predicate to determine what to autocomplete.
|
||||
switch (predicate) {
|
||||
case 'ownerin':
|
||||
case 'reviewerin':
|
||||
// Fetch groups.
|
||||
return this._fetchGroups(predicate, expression);
|
||||
|
||||
case 'parentproject':
|
||||
case 'project':
|
||||
// Fetch projects.
|
||||
return this._fetchProjects(predicate, expression);
|
||||
|
||||
case 'author':
|
||||
case 'commentby':
|
||||
case 'committer':
|
||||
case 'from':
|
||||
case 'owner':
|
||||
case 'reviewedby':
|
||||
case 'reviewer':
|
||||
// Fetch accounts.
|
||||
return this._fetchAccounts(predicate, expression);
|
||||
|
||||
default:
|
||||
return Promise.resolve(SEARCH_OPERATORS
|
||||
.filter(function(operator) {
|
||||
// Disallow autocomplete values that exactly match the whole str.
|
||||
var opContainsInput = operator.indexOf(lowerCaseInput) !== -1;
|
||||
var inputContainsOp = lowerCaseInput.indexOf(operator) !== -1;
|
||||
return opContainsInput && !inputContainsOp;
|
||||
return operator.indexOf(input) !== -1;
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the sorted, pruned list of suggestions for the current search query.
|
||||
* @param {string} input - The complete search query.
|
||||
* @return {!Promise} This returns a promise that resolves to an array of
|
||||
* strings.
|
||||
*/
|
||||
_getSearchSuggestions: function(input) {
|
||||
// Allow spaces within quoted terms.
|
||||
var tokens = input.match(TOKENIZE_REGEX);
|
||||
var trimmedInput = tokens[tokens.length - 1].toLowerCase();
|
||||
|
||||
return this._fetchSuggestions(trimmedInput)
|
||||
.then(function(operators) {
|
||||
if (!operators) { return []; }
|
||||
return operators
|
||||
// Disallow autocomplete values that exactly match the str.
|
||||
.filter(function(operator) {
|
||||
return input.indexOf(operator.toLowerCase()) == -1;
|
||||
})
|
||||
// Prioritize results that start with the input.
|
||||
.sort(function(operator) {
|
||||
return operator.indexOf(lowerCaseInput);
|
||||
return operator.indexOf(trimmedInput);
|
||||
})
|
||||
.map(this._makeSuggestion);
|
||||
}.bind(this));
|
||||
// Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
|
||||
.slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
|
||||
// Map to an object to play nice with gr-autocomplete.
|
||||
.map(function(operator) {
|
||||
return {
|
||||
name: operator,
|
||||
value: operator,
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_handleKey: function(e) {
|
||||
|
||||
@@ -79,17 +79,77 @@ limitations under the License.
|
||||
showStub.restore();
|
||||
});
|
||||
|
||||
test('_getSearchSuggestions returns proper set of suggestions',
|
||||
suite('_getSearchSuggestions',
|
||||
function() {
|
||||
setup(function() {
|
||||
sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() {
|
||||
return Promise.resolve([
|
||||
{
|
||||
name: 'fred',
|
||||
email: 'fred@goog.co',
|
||||
},
|
||||
]);
|
||||
});
|
||||
sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() {
|
||||
return Promise.resolve({
|
||||
Polygerrit: 0,
|
||||
});
|
||||
});
|
||||
sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() {
|
||||
return Promise.resolve({
|
||||
Polygerrit: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
element.$.restAPI.getSuggestedAccounts.restore();
|
||||
element.$.restAPI.getSuggestedGroups.restore();
|
||||
element.$.restAPI.getSuggestedProjects.restore();
|
||||
});
|
||||
|
||||
test('Autocompletes accounts',
|
||||
function(done) {
|
||||
element._getSearchSuggestions('is:o')
|
||||
return element._getSearchSuggestions('owner:fr')
|
||||
.then(function(suggestions) {
|
||||
assert.equal(suggestions[0].value, 'owner:"fred <fred@goog.co>"');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('Autocompletes groups',
|
||||
function(done) {
|
||||
return element._getSearchSuggestions('ownerin:pol')
|
||||
.then(function(suggestions) {
|
||||
assert.equal(suggestions[0].value, 'ownerin:Polygerrit');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('Autocompletes projects',
|
||||
function(done) {
|
||||
return element._getSearchSuggestions('project:pol')
|
||||
.then(function(suggestions) {
|
||||
assert.equal(suggestions[0].value, 'project:Polygerrit');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('Autocompletes simple searches',
|
||||
function(done) {
|
||||
return element._getSearchSuggestions('is:o')
|
||||
.then(function(suggestions) {
|
||||
assert.equal(suggestions[0].name, 'is:open');
|
||||
assert.equal(suggestions[0].value, 'is:open');
|
||||
assert.equal(suggestions[1].name, 'is:owner');
|
||||
assert.equal(suggestions[1].value, 'is:owner');
|
||||
})
|
||||
.then(function() {
|
||||
element._getSearchSuggestions('asdasdasdasd')
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('Does not autocomplete with no match',
|
||||
function(done) {
|
||||
return element._getSearchSuggestions('asdasdasdasd')
|
||||
.then(function(suggestions) {
|
||||
assert.equal(suggestions.length, 0);
|
||||
done();
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
|
||||
|
||||
Polymer({
|
||||
is: 'gr-autocomplete',
|
||||
|
||||
@@ -198,8 +200,10 @@
|
||||
var completed = suggestions[index].value;
|
||||
if (this.multi) {
|
||||
// Append the completed text to the end of the string.
|
||||
var shortStr = this.text.substring(0, this.text.lastIndexOf(' ') + 1);
|
||||
this.value = shortStr + completed;
|
||||
// Allow spaces within quoted terms.
|
||||
var tokens = this.text.match(TOKENIZE_REGEX);
|
||||
tokens[tokens.length - 1] = completed;
|
||||
this.value = tokens.join(' ');
|
||||
} else {
|
||||
this.value = completed;
|
||||
}
|
||||
|
||||
@@ -467,8 +467,26 @@
|
||||
});
|
||||
},
|
||||
|
||||
getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) {
|
||||
return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {p: inputVal});
|
||||
getSuggestedGroups: function(inputVal, opt_n, opt_errFn, opt_ctx) {
|
||||
return this.fetchJSON('/groups/', opt_errFn, opt_ctx, {
|
||||
s: inputVal,
|
||||
n: opt_n,
|
||||
});
|
||||
},
|
||||
|
||||
getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) {
|
||||
return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {
|
||||
p: inputVal,
|
||||
n: opt_n,
|
||||
});
|
||||
},
|
||||
|
||||
getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) {
|
||||
return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, {
|
||||
q: inputVal,
|
||||
n: opt_n,
|
||||
suggest: null,
|
||||
});
|
||||
},
|
||||
|
||||
addChangeReviewer: function(changeNum, reviewerID) {
|
||||
|
||||
Reference in New Issue
Block a user