Merge "Add gr-smart-search to handle rest API calls and navigation"
This commit is contained in:
commit
acdc81bb81
@ -22,7 +22,7 @@ limitations under the License.
|
|||||||
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
|
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
|
||||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||||
<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
|
<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
|
||||||
<link rel="import" href="../gr-search-bar/gr-search-bar.html">
|
<link rel="import" href="../gr-smart-search/gr-smart-search.html">
|
||||||
|
|
||||||
<dom-module id="gr-main-header">
|
<dom-module id="gr-main-header">
|
||||||
<template>
|
<template>
|
||||||
@ -86,7 +86,7 @@ limitations under the License.
|
|||||||
.rightItems gr-endpoint-decorator:not(:empty) {
|
.rightItems gr-endpoint-decorator:not(:empty) {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
gr-search-bar {
|
gr-smart-search {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-left: .5em;
|
margin-left: .5em;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
@ -129,7 +129,7 @@ limitations under the License.
|
|||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
font-family: var(--font-family-bold);
|
font-family: var(--font-family-bold);
|
||||||
}
|
}
|
||||||
gr-search-bar,
|
gr-smart-search,
|
||||||
.browse,
|
.browse,
|
||||||
.rightItems .hideOnMobile,
|
.rightItems .hideOnMobile,
|
||||||
.links > li.hideOnMobile {
|
.links > li.hideOnMobile {
|
||||||
@ -171,7 +171,7 @@ limitations under the License.
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="rightItems">
|
<div class="rightItems">
|
||||||
<gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
|
<gr-smart-search id="search" value="{{searchQuery}}"></gr-smart-search>
|
||||||
<gr-endpoint-decorator
|
<gr-endpoint-decorator
|
||||||
class="hideOnMobile"
|
class="hideOnMobile"
|
||||||
name="header-browse-source"></gr-endpoint-decorator>
|
name="header-browse-source"></gr-endpoint-decorator>
|
||||||
|
@ -237,6 +237,15 @@ limitations under the License.
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a search query
|
||||||
|
* @param {string} query
|
||||||
|
* @param {number=} opt_offset
|
||||||
|
*/
|
||||||
|
navigateToSearchQuery(query, opt_offset) {
|
||||||
|
return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {!Object} change The change object.
|
* @param {!Object} change The change object.
|
||||||
* @param {number=} opt_patchNum
|
* @param {number=} opt_patchNum
|
||||||
|
@ -15,20 +15,15 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
|
|
||||||
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
|
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
|
||||||
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
|
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
|
||||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||||
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
|
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
|
||||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
|
||||||
<link rel="import" href="../../../styles/shared-styles.html">
|
<link rel="import" href="../../../styles/shared-styles.html">
|
||||||
|
|
||||||
<dom-module id="gr-search-bar">
|
<dom-module id="gr-search-bar">
|
||||||
<template>
|
<template>
|
||||||
<style include="shared-styles">
|
<style include="shared-styles">
|
||||||
:host {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@ -55,7 +50,6 @@ limitations under the License.
|
|||||||
threshold="[[_threshold]]"
|
threshold="[[_threshold]]"
|
||||||
tab-complete
|
tab-complete
|
||||||
vertical-offset="30"></gr-autocomplete>
|
vertical-offset="30"></gr-autocomplete>
|
||||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<script src="gr-search-bar.js"></script>
|
<script src="gr-search-bar.js"></script>
|
||||||
|
@ -92,9 +92,6 @@
|
|||||||
const SEARCH_OPERATORS_WITH_NEGATIONS =
|
const SEARCH_OPERATORS_WITH_NEGATIONS =
|
||||||
SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`));
|
SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`));
|
||||||
|
|
||||||
const SELF_EXPRESSION = 'self';
|
|
||||||
const ME_EXPRESSION = 'me';
|
|
||||||
|
|
||||||
const MAX_AUTOCOMPLETE_RESULTS = 10;
|
const MAX_AUTOCOMPLETE_RESULTS = 10;
|
||||||
|
|
||||||
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
|
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
|
||||||
@ -102,8 +99,13 @@
|
|||||||
Polymer({
|
Polymer({
|
||||||
is: 'gr-search-bar',
|
is: 'gr-search-bar',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when a search is committed
|
||||||
|
*
|
||||||
|
* @event handle-search
|
||||||
|
*/
|
||||||
|
|
||||||
behaviors: [
|
behaviors: [
|
||||||
Gerrit.AnonymousNameBehavior,
|
|
||||||
Gerrit.KeyboardShortcutBehavior,
|
Gerrit.KeyboardShortcutBehavior,
|
||||||
Gerrit.URLEncodingBehavior,
|
Gerrit.URLEncodingBehavior,
|
||||||
],
|
],
|
||||||
@ -129,18 +131,29 @@
|
|||||||
return this._getSearchSuggestions.bind(this);
|
return this._getSearchSuggestions.bind(this);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
projectSuggestions: {
|
||||||
|
type: Function,
|
||||||
|
value() {
|
||||||
|
return () => Promise.resolve([]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupSuggestions: {
|
||||||
|
type: Function,
|
||||||
|
value() {
|
||||||
|
return () => Promise.resolve([]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accountSuggestions: {
|
||||||
|
type: Function,
|
||||||
|
value() {
|
||||||
|
return () => Promise.resolve([]);
|
||||||
|
},
|
||||||
|
},
|
||||||
_inputVal: String,
|
_inputVal: String,
|
||||||
_threshold: {
|
_threshold: {
|
||||||
type: Number,
|
type: Number,
|
||||||
value: 1,
|
value: 1,
|
||||||
},
|
},
|
||||||
_config: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
attached() {
|
|
||||||
this.$.restAPI.getConfig().then(cfg => {
|
|
||||||
this._config = cfg;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_valueChanged(value) {
|
_valueChanged(value) {
|
||||||
@ -170,87 +183,12 @@
|
|||||||
target.blur();
|
target.blur();
|
||||||
}
|
}
|
||||||
if (this._inputVal) {
|
if (this._inputVal) {
|
||||||
page.show('/q/' + this.encodeURL(this._inputVal, false));
|
this.dispatchEvent(new CustomEvent('handle-search', {
|
||||||
|
detail: {inputVal: this._inputVal},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_accountOrAnon(name) {
|
|
||||||
return this.getUserName(this._config, name, false);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(predicate, expression) {
|
|
||||||
if (expression.length === 0) { return Promise.resolve([]); }
|
|
||||||
return this.$.restAPI.getSuggestedAccounts(
|
|
||||||
expression,
|
|
||||||
MAX_AUTOCOMPLETE_RESULTS)
|
|
||||||
.then(accounts => {
|
|
||||||
if (!accounts) { return []; }
|
|
||||||
return accounts.map(acct => acct.email ?
|
|
||||||
`${predicate}:${acct.email}` :
|
|
||||||
`${predicate}:"${this._accountOrAnon(acct)}"`);
|
|
||||||
}).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]);
|
|
||||||
} else if (ME_EXPRESSION.startsWith(expression)) {
|
|
||||||
return accounts.concat([predicate + ':' + ME_EXPRESSION]);
|
|
||||||
} else {
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(predicate, expression) {
|
|
||||||
if (expression.length === 0) { return Promise.resolve([]); }
|
|
||||||
return this.$.restAPI.getSuggestedGroups(
|
|
||||||
expression,
|
|
||||||
MAX_AUTOCOMPLETE_RESULTS)
|
|
||||||
.then(groups => {
|
|
||||||
if (!groups) { return []; }
|
|
||||||
const keys = Object.keys(groups);
|
|
||||||
return keys.map(key => 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(predicate, expression) {
|
|
||||||
return this.$.restAPI.getSuggestedProjects(
|
|
||||||
expression,
|
|
||||||
MAX_AUTOCOMPLETE_RESULTS)
|
|
||||||
.then(projects => {
|
|
||||||
if (!projects) { return []; }
|
|
||||||
const keys = Object.keys(projects);
|
|
||||||
return keys.map(key => predicate + ':' + key);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine what array of possible suggestions should be provided
|
* Determine what array of possible suggestions should be provided
|
||||||
* to _getSearchSuggestions.
|
* to _getSearchSuggestions.
|
||||||
@ -268,12 +206,12 @@
|
|||||||
case 'ownerin':
|
case 'ownerin':
|
||||||
case 'reviewerin':
|
case 'reviewerin':
|
||||||
// Fetch groups.
|
// Fetch groups.
|
||||||
return this._fetchGroups(predicate, expression);
|
return this.groupSuggestions(predicate, expression);
|
||||||
|
|
||||||
case 'parentproject':
|
case 'parentproject':
|
||||||
case 'project':
|
case 'project':
|
||||||
// Fetch projects.
|
// Fetch projects.
|
||||||
return this._fetchProjects(predicate, expression);
|
return this.projectSuggestions(predicate, expression);
|
||||||
|
|
||||||
case 'author':
|
case 'author':
|
||||||
case 'cc':
|
case 'cc':
|
||||||
@ -284,7 +222,7 @@
|
|||||||
case 'reviewedby':
|
case 'reviewedby':
|
||||||
case 'reviewer':
|
case 'reviewer':
|
||||||
// Fetch accounts.
|
// Fetch accounts.
|
||||||
return this._fetchAccounts(predicate, expression);
|
return this.accountSuggestions(predicate, expression);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Promise.resolve(SEARCH_OPERATORS_WITH_NEGATIONS
|
return Promise.resolve(SEARCH_OPERATORS_WITH_NEGATIONS
|
||||||
|
@ -60,9 +60,8 @@ limitations under the License.
|
|||||||
document.activeElement;
|
document.activeElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
test('enter in search input triggers nav', done => {
|
test('enter in search input fires event', done => {
|
||||||
sandbox.stub(page, 'show', () => {
|
element.addEventListener('handle-search', () => {
|
||||||
page.show.restore();
|
|
||||||
assert.notEqual(getActiveElement(), element.$.searchInput);
|
assert.notEqual(getActiveElement(), element.$.searchInput);
|
||||||
assert.notEqual(getActiveElement(), element.$.searchButton);
|
assert.notEqual(getActiveElement(), element.$.searchButton);
|
||||||
done();
|
done();
|
||||||
@ -72,16 +71,7 @@ limitations under the License.
|
|||||||
null, 'enter');
|
null, 'enter');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('search query should be double-escaped', () => {
|
|
||||||
const showStub = sandbox.stub(page, 'show');
|
|
||||||
element.$.searchInput.text = 'fate/stay';
|
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
|
|
||||||
null, 'enter');
|
|
||||||
assert.equal(showStub.lastCall.args[0], '/q/fate%252Fstay');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('input blurred after commit', () => {
|
test('input blurred after commit', () => {
|
||||||
sandbox.stub(page, 'show');
|
|
||||||
const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
|
const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
|
||||||
element.$.searchInput.text = 'fate/stay';
|
element.$.searchInput.text = 'fate/stay';
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
|
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
|
||||||
@ -90,11 +80,12 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('empty search query does not trigger nav', () => {
|
test('empty search query does not trigger nav', () => {
|
||||||
const showSpy = sandbox.spy(page, 'show');
|
const searchSpy = sandbox.spy();
|
||||||
|
element.addEventListener('handle-search', searchSpy);
|
||||||
element.value = '';
|
element.value = '';
|
||||||
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
|
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
|
||||||
null, 'enter');
|
null, 'enter');
|
||||||
assert.isFalse(showSpy.called);
|
assert.isFalse(searchSpy.called);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keyboard shortcuts', () => {
|
test('keyboard shortcuts', () => {
|
||||||
@ -107,64 +98,20 @@ limitations under the License.
|
|||||||
|
|
||||||
suite('_getSearchSuggestions', () => {
|
suite('_getSearchSuggestions', () => {
|
||||||
test('Autocompletes accounts', () => {
|
test('Autocompletes accounts', () => {
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
sandbox.stub(element, 'accountSuggestions', () =>
|
||||||
Promise.resolve([
|
Promise.resolve(['owner:fred@goog.co'])
|
||||||
{
|
|
||||||
name: 'fred',
|
|
||||||
email: 'fred@goog.co',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
return element._getSearchSuggestions('owner:fr').then(s => {
|
return element._getSearchSuggestions('owner:fr').then(s => {
|
||||||
assert.equal(s[0].value, 'owner:fred@goog.co');
|
assert.equal(s[0].value, 'owner:fred@goog.co');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Inserts self as option when valid', done => {
|
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
name: 'fred',
|
|
||||||
email: 'fred@goog.co',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
element._getSearchSuggestions('owner:s').then(s => {
|
|
||||||
assert.equal(s[0].value, 'owner:self');
|
|
||||||
}).then(() => {
|
|
||||||
element._getSearchSuggestions('owner:selfs').then(s => {
|
|
||||||
assert.notEqual(s[0].value, 'owner:self');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Inserts me as option when valid', done => {
|
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
name: 'fred',
|
|
||||||
email: 'fred@goog.co',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
element._getSearchSuggestions('owner:m').then(s => {
|
|
||||||
assert.equal(s[0].value, 'owner:me');
|
|
||||||
}).then(() => {
|
|
||||||
element._getSearchSuggestions('owner:meme').then(s => {
|
|
||||||
assert.notEqual(s[0].value, 'owner:me');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Autocompletes groups', done => {
|
test('Autocompletes groups', done => {
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
|
sandbox.stub(element, 'groupSuggestions', () =>
|
||||||
Promise.resolve({
|
Promise.resolve([
|
||||||
Polygerrit: 0,
|
'ownerin:Polygerrit',
|
||||||
gerrit: 0,
|
'ownerin:gerrit',
|
||||||
gerrittest: 0,
|
])
|
||||||
})
|
|
||||||
);
|
);
|
||||||
element._getSearchSuggestions('ownerin:pol').then(s => {
|
element._getSearchSuggestions('ownerin:pol').then(s => {
|
||||||
assert.equal(s[0].value, 'ownerin:Polygerrit');
|
assert.equal(s[0].value, 'ownerin:Polygerrit');
|
||||||
@ -173,10 +120,12 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Autocompletes projects', done => {
|
test('Autocompletes projects', done => {
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
|
sandbox.stub(element, 'projectSuggestions', () =>
|
||||||
Promise.resolve({
|
Promise.resolve([
|
||||||
Polygerrit: 0,
|
'project:Polygerrit',
|
||||||
})
|
'project:gerrit',
|
||||||
|
'project:gerrittest',
|
||||||
|
])
|
||||||
);
|
);
|
||||||
element._getSearchSuggestions('project:pol').then(s => {
|
element._getSearchSuggestions('project:pol').then(s => {
|
||||||
assert.equal(s[0].value, 'project:Polygerrit');
|
assert.equal(s[0].value, 'project:Polygerrit');
|
||||||
@ -200,67 +149,6 @@ limitations under the License.
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Autocomplete doesnt override exact matches to input', done => {
|
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
|
|
||||||
Promise.resolve({
|
|
||||||
Polygerrit: 0,
|
|
||||||
gerrit: 0,
|
|
||||||
gerrittest: 0,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
element._getSearchSuggestions('ownerin:gerrit').then(s => {
|
|
||||||
assert.equal(s[0].value, 'ownerin:gerrit');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Autocomplete respects spaces', done => {
|
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
name: 'fred',
|
|
||||||
email: 'fred@goog.co',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
element._getSearchSuggestions('is:ope').then(s => {
|
|
||||||
assert.equal(s[0].name, 'is:open');
|
|
||||||
assert.equal(s[0].value, 'is:open');
|
|
||||||
element._getSearchSuggestions('is:ope ').then(s => {
|
|
||||||
assert.equal(s.length, 0);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Autocompletes accounts with no email', done => {
|
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
name: 'fred',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
element._getSearchSuggestions('owner:fr').then(s => {
|
|
||||||
assert.equal(s[0].value, 'owner:"fred"');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Autocompletes accounts with email', done => {
|
|
||||||
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
email: 'fred@goog.co',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
element._getSearchSuggestions('owner:fr').then(s => {
|
|
||||||
assert.equal(s[0].value, 'owner:fred@goog.co');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
@license
|
||||||
|
Copyright (C) 2016 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||||
|
|
||||||
|
<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
|
||||||
|
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
|
||||||
|
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||||
|
<link rel="import" href="../gr-search-bar/gr-search-bar.html">
|
||||||
|
|
||||||
|
<dom-module id="gr-smart-search">
|
||||||
|
<template>
|
||||||
|
<style include="shared-styles">
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<gr-search-bar id="search"
|
||||||
|
value="{{searchQuery}}"
|
||||||
|
on-handle-search="_handleSearch"
|
||||||
|
project-suggestions="[[_projectSuggestions]]"
|
||||||
|
group-suggestions="[[_groupSuggestions]]"
|
||||||
|
account-suggestions="[[_accountSuggestions]]"></gr-search-bar>
|
||||||
|
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||||
|
</template>
|
||||||
|
<script src="gr-smart-search.js"></script>
|
||||||
|
</dom-module>
|
@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const MAX_AUTOCOMPLETE_RESULTS = 10;
|
||||||
|
const SELF_EXPRESSION = 'self';
|
||||||
|
const ME_EXPRESSION = 'me';
|
||||||
|
|
||||||
|
Polymer({
|
||||||
|
is: 'gr-smart-search',
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
searchQuery: String,
|
||||||
|
_config: Object,
|
||||||
|
_projectSuggestions: {
|
||||||
|
type: Function,
|
||||||
|
value() {
|
||||||
|
return this._fetchProjects.bind(this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_groupSuggestions: {
|
||||||
|
type: Function,
|
||||||
|
value() {
|
||||||
|
return this._fetchGroups.bind(this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_accountSuggestions: {
|
||||||
|
type: Function,
|
||||||
|
value() {
|
||||||
|
return this._fetchAccounts.bind(this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
behaviors: [
|
||||||
|
Gerrit.AnonymousNameBehavior,
|
||||||
|
],
|
||||||
|
|
||||||
|
attached() {
|
||||||
|
this.$.restAPI.getConfig().then(cfg => {
|
||||||
|
this._config = cfg;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleSearch(e) {
|
||||||
|
const input = e.detail.inputVal;
|
||||||
|
if (input) {
|
||||||
|
Gerrit.Nav.navigateToSearchQuery(input);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_accountOrAnon(name) {
|
||||||
|
return this.getUserName(this._serverConfig, name, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(predicate, expression) {
|
||||||
|
return this.$.restAPI.getSuggestedProjects(
|
||||||
|
expression,
|
||||||
|
MAX_AUTOCOMPLETE_RESULTS)
|
||||||
|
.then(projects => {
|
||||||
|
if (!projects) { return []; }
|
||||||
|
const keys = Object.keys(projects);
|
||||||
|
return keys.map(key => predicate + ':' + key);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(predicate, expression) {
|
||||||
|
if (expression.length === 0) { return Promise.resolve([]); }
|
||||||
|
return this.$.restAPI.getSuggestedGroups(
|
||||||
|
expression,
|
||||||
|
MAX_AUTOCOMPLETE_RESULTS)
|
||||||
|
.then(groups => {
|
||||||
|
if (!groups) { return []; }
|
||||||
|
const keys = Object.keys(groups);
|
||||||
|
return keys.map(key => predicate + ':' + key);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(predicate, expression) {
|
||||||
|
if (expression.length === 0) { return Promise.resolve([]); }
|
||||||
|
return this.$.restAPI.getSuggestedAccounts(
|
||||||
|
expression,
|
||||||
|
MAX_AUTOCOMPLETE_RESULTS)
|
||||||
|
.then(accounts => {
|
||||||
|
if (!accounts) { return []; }
|
||||||
|
return accounts.map(acct => acct.email ?
|
||||||
|
`${predicate}:${acct.email}` :
|
||||||
|
`${predicate}:"${this._accountOrAnon(acct)}"`);
|
||||||
|
}).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]);
|
||||||
|
} else if (ME_EXPRESSION.startsWith(expression)) {
|
||||||
|
return accounts.concat([predicate + ':' + ME_EXPRESSION]);
|
||||||
|
} else {
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
@ -0,0 +1,174 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
@license
|
||||||
|
Copyright (C) 2016 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||||
|
<title>gr-smart-search</title>
|
||||||
|
|
||||||
|
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||||||
|
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||||
|
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||||||
|
<link rel="import" href="gr-smart-search.html">
|
||||||
|
|
||||||
|
<script>void(0);</script>
|
||||||
|
|
||||||
|
<test-fixture id="basic">
|
||||||
|
<template>
|
||||||
|
<gr-smart-search></gr-smart-search>
|
||||||
|
</template>
|
||||||
|
</test-fixture>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
suite('gr-smart-search tests', () => {
|
||||||
|
let element;
|
||||||
|
let sandbox;
|
||||||
|
|
||||||
|
setup(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
element = fixture('basic');
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Autocompletes accounts', () => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'fred',
|
||||||
|
email: 'fred@goog.co',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
return element._fetchAccounts('owner', 'fr').then(s => {
|
||||||
|
assert.equal(s[0], 'owner:fred@goog.co');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Inserts self as option when valid', done => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'fred',
|
||||||
|
email: 'fred@goog.co',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Inserts me as option when valid', done => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'fred',
|
||||||
|
email: 'fred@goog.co',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocompletes groups', done => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
|
||||||
|
Promise.resolve({
|
||||||
|
Polygerrit: 0,
|
||||||
|
gerrit: 0,
|
||||||
|
gerrittest: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
element._fetchGroups('ownerin', 'pol').then(s => {
|
||||||
|
assert.equal(s[0], 'ownerin:Polygerrit');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocompletes projects', done => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
|
||||||
|
Promise.resolve({
|
||||||
|
Polygerrit: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
element._fetchProjects('project', 'pol').then(s => {
|
||||||
|
assert.equal(s[0], 'project:Polygerrit');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocomplete doesnt override exact matches to input', done => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
|
||||||
|
Promise.resolve({
|
||||||
|
Polygerrit: 0,
|
||||||
|
gerrit: 0,
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocompletes accounts with no email', done => {
|
||||||
|
sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
name: 'fred',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
element._fetchAccounts('owner', 'fr').then(s => {
|
||||||
|
assert.equal(s[0], 'owner:"fred"');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocompletes accounts with email', done => {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
@ -91,6 +91,7 @@ limitations under the License.
|
|||||||
'core/gr-reporting/gr-reporting_test.html',
|
'core/gr-reporting/gr-reporting_test.html',
|
||||||
'core/gr-router/gr-router_test.html',
|
'core/gr-router/gr-router_test.html',
|
||||||
'core/gr-search-bar/gr-search-bar_test.html',
|
'core/gr-search-bar/gr-search-bar_test.html',
|
||||||
|
'core/gr-smart-search/gr-smart-search_test.html',
|
||||||
'diff/gr-comment-api/gr-comment-api_test.html',
|
'diff/gr-comment-api/gr-comment-api_test.html',
|
||||||
'diff/gr-diff-builder/gr-diff-builder_test.html',
|
'diff/gr-diff-builder/gr-diff-builder_test.html',
|
||||||
'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html',
|
'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html',
|
||||||
|
Loading…
Reference in New Issue
Block a user