Add name label to account search

This change involves a restructuring of the way the autocomplete queries
return. Instead of an array of strings, the query returns an array of
objects with text and optional label properties.

Bug: Issue 8448
Change-Id: Ie6e973473feb3d61b9e4e9dcac2ee981eed39745
This commit is contained in:
Kasper Nilsson
2018-10-02 14:11:44 -07:00
parent 125c34c18f
commit c363c9f89d
5 changed files with 71 additions and 84 deletions

View File

@@ -194,7 +194,7 @@
* to _getSearchSuggestions.
* @param {string} input - The full search term, in lowercase.
* @return {!Promise} This returns a promise that resolves to an array of
* strings.
* suggestion objects.
*/
_fetchSuggestions(input) {
// Split the input on colon to get a two part predicate/expression.
@@ -226,7 +226,8 @@
default:
return Promise.resolve(SEARCH_OPERATORS_WITH_NEGATIONS
.filter(operator => operator.includes(input)));
.filter(operator => operator.includes(input))
.map(operator => ({text: operator})));
}
},
@@ -234,7 +235,7 @@
* 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.
* suggestions.
*/
_getSearchSuggestions(input) {
// Allow spaces within quoted terms.
@@ -242,15 +243,15 @@
const trimmedInput = tokens[tokens.length - 1].toLowerCase();
return this._fetchSuggestions(trimmedInput)
.then(operators => {
if (!operators || !operators.length) { return []; }
return operators
.then(suggestions => {
if (!suggestions || !suggestions.length) { return []; }
return suggestions
// Prioritize results that start with the input.
.sort((a, b) => {
const aContains = a.toLowerCase().indexOf(trimmedInput);
const bContains = b.toLowerCase().indexOf(trimmedInput);
const aContains = a.text.toLowerCase().indexOf(trimmedInput);
const bContains = b.text.toLowerCase().indexOf(trimmedInput);
if (aContains === bContains) {
return a.localeCompare(b);
return a.text.localeCompare(b.text);
}
if (aContains === -1) {
return 1;
@@ -263,10 +264,11 @@
// 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(operator => {
.map(({text, label}) => {
return {
name: operator,
value: operator,
name: text,
value: text,
label,
};
});
});

View File

@@ -99,7 +99,7 @@ limitations under the License.
suite('_getSearchSuggestions', () => {
test('Autocompletes accounts', () => {
sandbox.stub(element, 'accountSuggestions', () =>
Promise.resolve(['owner:fred@goog.co'])
Promise.resolve([{text: 'owner:fred@goog.co'}])
);
return element._getSearchSuggestions('owner:fr').then(s => {
assert.equal(s[0].value, 'owner:fred@goog.co');
@@ -109,8 +109,8 @@ limitations under the License.
test('Autocompletes groups', done => {
sandbox.stub(element, 'groupSuggestions', () =>
Promise.resolve([
'ownerin:Polygerrit',
'ownerin:gerrit',
{text: 'ownerin:Polygerrit'},
{text: 'ownerin:gerrit'},
])
);
element._getSearchSuggestions('ownerin:pol').then(s => {
@@ -122,9 +122,9 @@ limitations under the License.
test('Autocompletes projects', done => {
sandbox.stub(element, 'projectSuggestions', () =>
Promise.resolve([
'project:Polygerrit',
'project:gerrit',
'project:gerrittest',
{text: 'project:Polygerrit'},
{text: 'project:gerrit'},
{text: 'project:gerrittest'},
])
);
element._getSearchSuggestions('project:pol').then(s => {

View File

@@ -84,7 +84,7 @@
.then(projects => {
if (!projects) { return []; }
const keys = Object.keys(projects);
return keys.map(key => predicate + ':' + key);
return keys.map(key => ({text: predicate + ':' + key}));
});
},
@@ -105,7 +105,7 @@
.then(groups => {
if (!groups) { return []; }
const keys = Object.keys(groups);
return keys.map(key => predicate + ':' + key);
return keys.map(key => ({text: predicate + ':' + key}));
});
},
@@ -125,20 +125,28 @@
MAX_AUTOCOMPLETE_RESULTS)
.then(accounts => {
if (!accounts) { return []; }
return accounts.map(acct => acct.email ?
`${predicate}:${acct.email}` :
`${predicate}:"${this._accountOrAnon(acct)}"`);
return this._mapAccountsHelper(accounts, predicate);
}).then(accounts => {
// When the expression supplied is a beginning substring of 'self',
// add it as an autocomplete option.
if (SELF_EXPRESSION.startsWith(expression)) {
return accounts.concat([predicate + ':' + SELF_EXPRESSION]);
return accounts.concat(
[{text: predicate + ':' + SELF_EXPRESSION}]);
} else if (ME_EXPRESSION.startsWith(expression)) {
return accounts.concat([predicate + ':' + ME_EXPRESSION]);
return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
} else {
return accounts;
}
});
},
_mapAccountsHelper(accounts, predicate) {
return accounts.map(account => ({
label: account.name || '',
text: account.email ?
`${predicate}:${account.email}` :
`${predicate}:"${this._accountOrAnon(account)}"`,
}));
},
});
})();

View File

@@ -57,11 +57,11 @@ limitations under the License.
])
);
return element._fetchAccounts('owner', 'fr').then(s => {
assert.equal(s[0], 'owner:fred@goog.co');
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
});
});
test('Inserts self as option when valid', done => {
test('Inserts self as option when valid', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
Promise.resolve([
{
@@ -71,17 +71,14 @@ limitations under the License.
])
);
element._fetchAccounts('owner', 's').then(s => {
assert.equal(s[0], 'owner:fred@goog.co');
assert.equal(s[1], 'owner:self');
}).then(() => {
element._fetchAccounts('owner', 'selfs').then(s => {
assert.notEqual(s[0], 'owner:self');
done();
});
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
assert.deepEqual(s[1], {text: 'owner:self'});
}).then(() => element._fetchAccounts('owner', 'selfs')).then(s => {
assert.notEqual(s[0], {text: 'owner:self'});
});
});
test('Inserts me as option when valid', done => {
test('Inserts me as option when valid', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
Promise.resolve([
{
@@ -90,18 +87,15 @@ limitations under the License.
},
])
);
element._fetchAccounts('owner', 'm').then(s => {
assert.equal(s[0], 'owner:fred@goog.co');
assert.equal(s[1], 'owner:me');
}).then(() => {
element._fetchAccounts('owner', 'meme').then(s => {
assert.notEqual(s[0], 'owner:me');
done();
});
return element._fetchAccounts('owner', 'm').then(s => {
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
assert.deepEqual(s[1], {text: 'owner:me'});
}).then(() => element._fetchAccounts('owner', 'meme')).then(s => {
assert.notEqual(s[0], {text: 'owner:me'});
});
});
test('Autocompletes groups', done => {
test('Autocompletes groups', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
Promise.resolve({
Polygerrit: 0,
@@ -109,25 +103,20 @@ limitations under the License.
gerrittest: 0,
})
);
element._fetchGroups('ownerin', 'pol').then(s => {
assert.equal(s[0], 'ownerin:Polygerrit');
done();
return element._fetchGroups('ownerin', 'pol').then(s => {
assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
});
});
test('Autocompletes projects', done => {
test('Autocompletes projects', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
Promise.resolve({
Polygerrit: 0,
})
);
element._fetchProjects('project', 'pol').then(s => {
assert.equal(s[0], 'project:Polygerrit');
done();
Promise.resolve({Polygerrit: 0}));
return element._fetchProjects('project', 'pol').then(s => {
assert.deepEqual(s[0], {text: 'project:Polygerrit'});
});
});
test('Autocomplete doesnt override exact matches to input', done => {
test('Autocomplete doesnt override exact matches to input', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
Promise.resolve({
Polygerrit: 0,
@@ -135,39 +124,26 @@ limitations under the License.
gerrittest: 0,
})
);
element._fetchGroups('ownerin', 'gerrit').then(s => {
assert.equal(s[0], 'ownerin:Polygerrit');
assert.equal(s[1], 'ownerin:gerrit');
assert.equal(s[2], 'ownerin:gerrittest');
done();
return element._fetchGroups('ownerin', 'gerrit').then(s => {
assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
});
});
test('Autocompletes accounts with no email', done => {
test('Autocompletes accounts with no email', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
Promise.resolve([
{
name: 'fred',
},
])
);
element._fetchAccounts('owner', 'fr').then(s => {
assert.equal(s[0], 'owner:"fred"');
done();
Promise.resolve([{name: 'fred'}]));
return element._fetchAccounts('owner', 'fr').then(s => {
assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
});
});
test('Autocompletes accounts with email', done => {
test('Autocompletes accounts with email', () => {
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
Promise.resolve([
{
email: 'fred@goog.co',
},
])
);
element._fetchAccounts('owner', 'fr').then(s => {
assert.equal(s[0], 'owner:fred@goog.co');
done();
Promise.resolve([{email: 'fred@goog.co'}]));
return element._fetchAccounts('owner', 'fr').then(s => {
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
});
});
});

View File

@@ -47,10 +47,11 @@
/**
* Query for requesting autocomplete suggestions. The function should
* accept the input as a string parameter and return a promise. The
* promise should yield an array of suggestion objects with "name" and
* promise yields an array of suggestion objects with "name", "label",
* "value" properties. The "name" property will be displayed in the
* suggestion entry. The "value" property will be emitted if that
* suggestion is selected.
* suggestion entry. The "label" property will, when specified, appear
* next to the "name" as label text. The "value" property will be emitted
* if that suggestion is selected.
*
* @type {function(string): Promise<?>}
*/