Editor for accounts/groups list.
To implement some new UI feature, we need a way to specify list
of accounts and/or groups. Similar solutions already exists for 'change'
view, but it can't be reused as-is in other places.
In this commit the existing solution was reworked to use the same
editor in different places.
Editor was splitted to 2 parts - editor itself and suggestion provider.
Change-Id: I43ce060e568a69f9842fbfad6f5fd62361ab2022
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
similarity index 60%
rename from polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html
rename to polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
index 40379e4..3106fc8 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.html
@@ -15,33 +15,28 @@
limitations under the License.
-->
+<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+
<script>
(function(window) {
'use strict';
- const ANONYMOUS_NAME = 'Anonymous';
-
window.Gerrit = window.Gerrit || {};
- /** @polymerBehavior Gerrit.AnonymousNameBehavior */
- Gerrit.AnonymousNameBehavior = {
+ /** @polymerBehavior Gerrit.DisplayNameBehavior */
+ Gerrit.DisplayNameBehavior = {
+ // TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
+
/**
* enableEmail when true enables to fallback to using email if
* the account name is not avilable.
*/
getUserName(config, account, enableEmail) {
- if (account && account.name) {
- return account.name;
- } else if (account && account.username) {
- return account.username;
- } else if (enableEmail && account && account.email) {
- return account.email;
- } else if (config && config.user &&
- config.user.anonymous_coward_name !== 'Anonymous Coward') {
- return config.user.anonymous_coward_name;
- }
+ return GrDisplayNameUtils.getUserName(config, account, enableEmail);
+ },
- return ANONYMOUS_NAME;
+ getGroupDisplayName(group) {
+ return GrDisplayNameUtils.getGroupDisplayName(group);
},
};
})(window);
diff --git a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
similarity index 79%
rename from polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
rename to polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
index 64f0b3a..4c5c899 100644
--- a/polygerrit-ui/app/behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -17,14 +17,14 @@
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-anonymous-name-behavior</title>
+<title>gr-display-name-behavior</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.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-anonymous-name-behavior.html">
+<link rel="import" href="gr-display-name-behavior.html">
<test-fixture id="basic">
<template>
@@ -33,7 +33,7 @@
</test-fixture>
<script>
- suite('gr-anonymous-name-behavior tests', () => {
+ suite('gr-display-name-behavior tests', () => {
let element;
// eslint-disable-next-line no-unused-vars
const config = {
@@ -48,7 +48,7 @@
is: 'test-element-anon',
_legacyUndefinedCheck: true,
behaviors: [
- Gerrit.AnonymousNameBehavior,
+ Gerrit.DisplayNameBehavior,
],
});
});
@@ -57,21 +57,21 @@
element = fixture('basic');
});
- test('test for it to return name', () => {
+ test('getUserName name only', () => {
const account = {
name: 'test-name',
};
assert.deepEqual(element.getUserName(config, account, true), 'test-name');
});
- test('test for it to return username', () => {
+ test('getUserName username only', () => {
const account = {
username: 'test-user',
};
assert.deepEqual(element.getUserName(config, account, true), 'test-user');
});
- test('test for it to return email', () => {
+ test('getUserName email only', () => {
const account = {
email: 'test-user@test-url.com',
};
@@ -79,11 +79,11 @@
'test-user@test-url.com');
});
- test('test for it not to Anonymous Coward as the anon name', () => {
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
});
- test('test for the config returning the anon name', () => {
+ test('getUserName for the config returning the anon name', () => {
const config = {
user: {
anonymous_coward_name: 'Test Anon',
@@ -91,5 +91,10 @@
};
assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
});
+
+ test('getGroupDisplayName', () => {
+ assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
+ 'Some user name (group)');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
deleted file mode 100644
index 1cb1ca5..0000000
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ /dev/null
@@ -1,186 +0,0 @@
-/**
- * @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';
-
- Polymer({
- is: 'gr-account-entry',
- _legacyUndefinedCheck: true,
-
- /**
- * Fired when an account is entered.
- *
- * @event add
- */
-
- /**
- * When allowAnyInput is true, account-text-changed is fired when input text
- * changed. This is needed so that the reply dialog's save button can be
- * enabled for arbitrary cc's, which don't need a 'commit'.
- *
- * @event account-text-changed
- */
- properties: {
- allowAnyInput: Boolean,
- borderless: Boolean,
- change: Object,
- filter: Function,
- placeholder: String,
- /**
- * When true, account-entry uses the account suggest API endpoint, which
- * suggests any account in that Gerrit instance (and does not suggest
- * groups).
- *
- * When false/undefined, account-entry uses the suggest_reviewers API
- * endpoint, which suggests any account or group in that Gerrit instance
- * that is not already a reviewer (or is not CCed) on that change.
- */
- allowAnyUser: Boolean,
-
- // suggestFrom = 0 to enable default suggestions.
- suggestFrom: {
- type: Number,
- value: 0,
- },
-
- query: {
- type: Function,
- value() {
- return this._getReviewerSuggestions.bind(this);
- },
- },
-
- _config: Object,
- /** The value of the autocomplete entry. */
- _inputText: {
- type: String,
- observer: '_inputTextChanged',
- },
-
- _loggedIn: Boolean,
- },
-
- behaviors: [
- Gerrit.AnonymousNameBehavior,
- Gerrit.FireBehavior,
- ],
-
- attached() {
- this.$.restAPI.getConfig().then(cfg => {
- this._config = cfg;
- });
- this.$.restAPI.getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- },
-
- get focusStart() {
- return this.$.input.focusStart;
- },
-
- focus() {
- this.$.input.focus();
- },
-
- clear() {
- this.$.input.clear();
- },
-
- setText(text) {
- this.$.input.setText(text);
- },
-
- getText() {
- return this.$.input.text;
- },
-
- _handleInputCommit(e) {
- this.fire('add', {value: e.detail.value});
- this.$.input.focus();
- },
-
- _accountOrAnon(reviewer) {
- return this.getUserName(this._config, reviewer, false);
- },
-
- _inputTextChanged(text) {
- if (text.length && this.allowAnyInput) {
- this.dispatchEvent(new CustomEvent(
- 'account-text-changed', {bubbles: true, composed: true}));
- }
- },
-
- _makeSuggestion(reviewer) {
- let name;
- let value;
- const generateStatusStr = function(account) {
- return account.status ? '(' + account.status + ')' : '';
- };
- if (reviewer.account) {
- // Reviewer is an account suggestion from getChangeSuggestedReviewers.
- const reviewerName = this._accountOrAnon(reviewer.account);
- const reviewerEmail = this._reviewerEmail(reviewer.account.email);
- const reviewerStatus = generateStatusStr(reviewer.account);
- name = [reviewerName, reviewerEmail, reviewerStatus]
- .filter(p => p.length > 0).join(' ');
- value = reviewer;
- } else if (reviewer.group) {
- // Reviewer is a group suggestion from getChangeSuggestedReviewers.
- name = reviewer.group.name + ' (group)';
- value = reviewer;
- } else if (reviewer._account_id) {
- // Reviewer is an account suggestion from getSuggestedAccounts.
- const reviewerName = this._accountOrAnon(reviewer);
- const reviewerEmail = this._reviewerEmail(reviewer.email);
- const reviewerStatus = generateStatusStr(reviewer);
- name = [reviewerName, reviewerEmail, reviewerStatus]
- .filter(p => p.length > 0).join(' ');
- value = {account: reviewer, count: 1};
- }
- return {name, value};
- },
-
- _getReviewerSuggestions(input) {
- if (!this.change || !this.change._number || !this._loggedIn) {
- return Promise.resolve([]);
- }
-
- const api = this.$.restAPI;
- const xhr = this.allowAnyUser ?
- api.getSuggestedAccounts(`cansee:${this.change._number} ${input}`) :
- api.getChangeSuggestedReviewers(this.change._number, input);
-
- return xhr.then(reviewers => {
- if (!reviewers) { return []; }
- if (!this.filter) {
- return reviewers.map(this._makeSuggestion.bind(this));
- }
- return reviewers
- .filter(this.filter)
- .map(this._makeSuggestion.bind(this));
- });
- },
-
- _reviewerEmail(email) {
- if (typeof email !== 'undefined') {
- return '<' + email + '>';
- }
-
- return '';
- },
- });
-})();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
deleted file mode 100644
index 57bdd1d..0000000
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ /dev/null
@@ -1,276 +0,0 @@
-<!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-account-entry</title>
-<script src="/test/common-test-setup.js"></script>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="gr-account-entry.html">
-
-<script>void(0);</script>
-
-<test-fixture id="basic">
- <template>
- <gr-account-entry></gr-account-entry>
- </template>
-</test-fixture>
-
-<script>
- suite('gr-account-entry tests', () => {
- let sandbox;
- let _nextAccountId = 0;
- const makeAccount = function(opt_status) {
- const accountId = ++_nextAccountId;
- return {
- _account_id: accountId,
- name: 'name ' + accountId,
- email: 'email ' + accountId,
- status: opt_status,
- };
- };
- let _nextAccountId2 = 0;
- const makeAccount2 = function(opt_status) {
- const accountId2 = ++_nextAccountId2;
- return {
- _account_id: accountId2,
- email: 'email ' + accountId2,
- status: opt_status,
- };
- };
- let _nextAccountId3 = 0;
- const makeAccount3 = function(opt_status) {
- const accountId3 = ++_nextAccountId3;
- return {
- _account_id: accountId3,
- name: 'name ' + accountId3,
- status: opt_status,
- };
- };
-
- let owner;
- let existingReviewer1;
- let existingReviewer2;
- let suggestion1;
- let suggestion2;
- let suggestion3;
- let element;
-
- setup(done => {
- owner = makeAccount();
- existingReviewer1 = makeAccount();
- existingReviewer2 = makeAccount();
- suggestion1 = {account: makeAccount()};
- suggestion2 = {account: makeAccount()};
- suggestion3 = {
- group: {
- id: 'suggested group id',
- name: 'suggested group',
- },
- };
-
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
-
- element = fixture('basic');
- element.change = {
- _number: 42,
- owner,
- reviewers: {
- CC: [existingReviewer1],
- REVIEWER: [existingReviewer2],
- },
- };
- sandbox = sinon.sandbox.create();
- return flush(done);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- suite('stubbed values for _getReviewerSuggestions', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeSuggestedReviewers() {
- const redundantSuggestion1 = {account: existingReviewer1};
- const redundantSuggestion2 = {account: existingReviewer2};
- const redundantSuggestion3 = {account: owner};
- return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
- redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
- },
- });
- });
-
- test('_makeSuggestion formats account or group accordingly', () => {
- let account = makeAccount();
- const account2 = makeAccount2();
- const account3 = makeAccount3();
- let suggestion = element._makeSuggestion({account});
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '>',
- value: {account},
- });
-
- const group = {name: 'test'};
- suggestion = element._makeSuggestion({group});
- assert.deepEqual(suggestion, {
- name: group.name + ' (group)',
- value: {group},
- });
-
- suggestion = element._makeSuggestion(account);
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '>',
- value: {account, count: 1},
- });
-
- element._config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
- assert.deepEqual(element._accountOrAnon(account2), 'Anonymous');
-
- account = makeAccount('OOO');
-
- suggestion = element._makeSuggestion({account});
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '> (OOO)',
- value: {account},
- });
-
- suggestion = element._makeSuggestion(account);
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '> (OOO)',
- value: {account, count: 1},
- });
-
- sandbox.stub(element, '_reviewerEmail',
- () => { return ''; });
-
- suggestion = element._makeSuggestion(account3);
- assert.deepEqual(suggestion, {
- name: account3.name,
- value: {account: account3, count: 1},
- });
- });
-
- test('_reviewerEmail', () => {
- assert.equal(
- element._reviewerEmail('email@gerritreview.com'),
- '<email@gerritreview.com>');
- assert.equal(element._reviewerEmail(undefined), '');
- });
-
- test('_getReviewerSuggestions excludes owner+reviewers', done => {
- element._getReviewerSuggestions().then(reviewers => {
- // Default is no filtering.
- assert.equal(reviewers.length, 6);
-
- // Set up filter that only accepts suggestion1.
- const accountId = suggestion1.account._account_id;
- element.filter = function(suggestion) {
- return suggestion.account &&
- suggestion.account._account_id === accountId;
- };
-
- element._getReviewerSuggestions().then(reviewers => {
- assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]);
- }).then(done);
- });
- });
-
- test('_getReviewerSuggestions short circuits when logged out', () => {
- // API call is already stubbed.
- const xhrSpy = element.$.restAPI.getChangeSuggestedReviewers;
- element._loggedIn = false;
- return element._getReviewerSuggestions('').then(() => {
- assert.isFalse(xhrSpy.called);
- element._loggedIn = true;
- return element._getReviewerSuggestions('').then(() => {
- assert.isTrue(xhrSpy.called);
- });
- });
- });
- });
-
- test('allowAnyUser', done => {
- const suggestReviewerStub =
- sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- const suggestAccountStub =
- sandbox.stub(element.$.restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([]));
-
- element._getReviewerSuggestions('').then(() => {
- assert.isTrue(suggestReviewerStub.calledOnce);
- assert.isTrue(suggestReviewerStub.calledWith(42, ''));
- assert.isFalse(suggestAccountStub.called);
- element.allowAnyUser = true;
-
- element._getReviewerSuggestions('').then(() => {
- assert.isTrue(suggestReviewerStub.calledOnce);
- assert.isTrue(suggestAccountStub.calledOnce);
- assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
- done();
- });
- });
- });
- test('account-text-changed fired when input text changed and allowAnyInput',
- () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const changeStub = sandbox.stub();
- element.allowAnyInput = true;
- sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- element.addEventListener('account-text-changed', changeStub);
- element.$.input.text = 'a';
- assert.isTrue(changeStub.calledOnce);
- element.$.input.text = 'ab';
- assert.isTrue(changeStub.calledTwice);
- });
-
- test('account-text-changed not fired when input text changed without ' +
- 'allowAnyUser', () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const changeStub = sandbox.stub();
- sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- element.addEventListener('account-text-changed', changeStub);
- element.$.input.text = 'a';
- assert.isFalse(changeStub.called);
- });
-
- test('setText', () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const suggestSpy = sandbox.spy(element.$.input, 'query');
- element.setText('test text');
- flushAsynchronousOperations();
-
- assert.equal(element.$.input.$.input.value, 'test text');
- assert.isFalse(suggestSpy.called);
- });
- });
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 15892c9..ee35583 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -36,6 +36,8 @@
<link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
<link rel="import" href="../gr-commit-info/gr-commit-info.html">
<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
+<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
<dom-module id="gr-change-metadata">
<template>
@@ -172,9 +174,9 @@
id="assigneeValue"
placeholder="Set assignee..."
accounts="{{_assignee}}"
- change="[[change]]"
readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
- allow-any-user></gr-account-list>
+ suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+ </gr-account-list>
</span>
</section>
<section>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 6ee5a24..62446e4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -471,5 +471,12 @@
// dom-if.
this.$$('.topicEditableLabel').open();
},
+
+ _getReviewerSuggestionsProvider(change) {
+ const provider = new GrReviewerSuggestionsProvider(this.$.restAPI,
+ change._number, true);
+ provider.init();
+ return provider;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 08a3ec3..38c4a96 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -32,9 +32,11 @@
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-account-list/gr-account-list.html">
+<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
<link rel="import" href="../gr-label-scores/gr-label-scores.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
<dom-module id="gr-reply-dialog">
<template>
@@ -165,11 +167,11 @@
id="reviewers"
accounts="{{_reviewers}}"
removable-values="[[change.removable_reviewers]]"
- change="[[change]]"
filter="[[filterReviewerSuggestion]]"
pending-confirmation="{{_reviewerPendingConfirmation}}"
placeholder="Add reviewer..."
- on-account-text-changed="_handleAccountTextEntry">
+ on-account-text-changed="_handleAccountTextEntry"
+ suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list>
</div>
<div class="peopleList">
@@ -177,12 +179,12 @@
<gr-account-list
id="ccs"
accounts="{{_ccs}}"
- change="[[change]]"
filter="[[filterCCSuggestion]]"
pending-confirmation="{{_ccPendingConfirmation}}"
allow-any-input
placeholder="Add CC..."
- on-account-text-changed="_handleAccountTextEntry">
+ on-account-text-changed="_handleAccountTextEntry"
+ suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list>
</div>
<gr-overlay
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 3b97439..6b7359f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -895,5 +895,12 @@
_sendDisabledChanged(sendDisabled) {
this.dispatchEvent(new CustomEvent('send-disabled-changed'));
},
+
+ _getReviewerSuggestionsProvider(change) {
+ const provider = new GrReviewerSuggestionsProvider(this.$.restAPI,
+ change._number, false);
+ provider.init();
+ return provider;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 7949002..dc18f8a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 2ade782..f8288c6 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -58,7 +58,7 @@
},
behaviors: [
- Gerrit.AnonymousNameBehavior,
+ Gerrit.DisplayNameBehavior,
],
detached() {
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
index 06e354c..c4ae41b 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
@@ -16,7 +16,7 @@
-->
<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="../../../behaviors/gr-display-name-behavior/gr-display-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">
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index fed02d6..017310d 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -49,7 +49,7 @@
},
behaviors: [
- Gerrit.AnonymousNameBehavior,
+ Gerrit.DisplayNameBehavior,
],
attached() {
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
similarity index 79%
rename from polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
rename to polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
index 03fc606..ae656fd 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.html
@@ -15,12 +15,11 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../styles/shared-styles.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="../gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-account-entry">
<template>
@@ -36,14 +35,13 @@
borderless="[[borderless]]"
placeholder="[[placeholder]]"
threshold="[[suggestFrom]]"
- query="[[query]]"
+ query="[[querySuggestions]]"
allow-non-suggested-values="[[allowAnyInput]]"
on-commit="_handleInputCommit"
clear-on-commit
warn-uncommitted
text="{{_inputText}}">
</gr-autocomplete>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-account-entry.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
new file mode 100644
index 0000000..63bd252
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -0,0 +1,102 @@
+/**
+ * @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';
+
+ /**
+ * gr-account-entry is an element for entering account
+ * and/or group with autocomplete support.
+ */
+ Polymer({
+ is: 'gr-account-entry',
+ _legacyUndefinedCheck: true,
+
+ /**
+ * Fired when an account is entered.
+ *
+ * @event add
+ */
+
+ /**
+ * When allowAnyInput is true, account-text-changed is fired when input text
+ * changed. This is needed so that the reply dialog's save button can be
+ * enabled for arbitrary cc's, which don't need a 'commit'.
+ *
+ * @event account-text-changed
+ */
+ properties: {
+ allowAnyInput: Boolean,
+ borderless: Boolean,
+ placeholder: String,
+
+ // suggestFrom = 0 to enable default suggestions.
+ suggestFrom: {
+ type: Number,
+ value: 0,
+ },
+
+ /** @type {!function(string): !Promise<Array<{name, value}>>} */
+ querySuggestions: {
+ type: Function,
+ notify: true,
+ value() {
+ return input => Promise.resolve([]);
+ },
+ },
+
+ _config: Object,
+ /** The value of the autocomplete entry. */
+ _inputText: {
+ type: String,
+ observer: '_inputTextChanged',
+ },
+
+ },
+
+ get focusStart() {
+ return this.$.input.focusStart;
+ },
+
+ focus() {
+ this.$.input.focus();
+ },
+
+ clear() {
+ this.$.input.clear();
+ },
+
+ setText(text) {
+ this.$.input.setText(text);
+ },
+
+ getText() {
+ return this.$.input.text;
+ },
+
+ _handleInputCommit(e) {
+ this.fire('add', {value: e.detail.value});
+ this.$.input.focus();
+ },
+
+ _inputTextChanged(text) {
+ if (text.length && this.allowAnyInput) {
+ this.dispatchEvent(new CustomEvent(
+ 'account-text-changed', {bubbles: true, composed: true}));
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
new file mode 100644
index 0000000..59792a7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -0,0 +1,113 @@
+<!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-account-entry</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="gr-account-entry.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-account-entry></gr-account-entry>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-entry tests', () => {
+ let sandbox;
+
+ const suggestion1 = {
+ email: 'email1@example.com',
+ _account_id: 1,
+ some_property: 'value',
+ };
+ const suggestion2 = {
+ email: 'email2@example.com',
+ _account_id: 2,
+ };
+ const suggestion3 = {
+ email: 'email25@example.com',
+ _account_id: 25,
+ some_other_property: 'other value',
+ };
+
+ setup(done => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return flush(done);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('stubbed values for querySuggestions', () => {
+ setup(() => {
+ element.querySuggestions = input => {
+ return Promise.resolve([
+ suggestion1,
+ suggestion2,
+ suggestion3,
+ ]);
+ };
+ });
+ });
+
+ test('account-text-changed fired when input text changed and allowAnyInput',
+ () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const changeStub = sandbox.stub();
+ element.allowAnyInput = true;
+ element.querySuggestions = input => Promise.resolve([]);
+ element.addEventListener('account-text-changed', changeStub);
+ element.$.input.text = 'a';
+ assert.isTrue(changeStub.calledOnce);
+ element.$.input.text = 'ab';
+ assert.isTrue(changeStub.calledTwice);
+ });
+
+ test('account-text-changed not fired when input text changed without ' +
+ 'allowAnyInput', () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const changeStub = sandbox.stub();
+ element.querySuggestions = input => Promise.resolve([]);
+ element.addEventListener('account-text-changed', changeStub);
+ element.$.input.text = 'a';
+ assert.isFalse(changeStub.called);
+ });
+
+ test('setText', () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const suggestSpy = sandbox.spy(element.$.input, 'query');
+ element.setText('test text');
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.input.$.input.value, 'test text');
+ assert.isFalse(suggestSpy.called);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index 9d0782f..f807a74 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 88df33b..a778e8b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -52,7 +52,7 @@
},
behaviors: [
- Gerrit.AnonymousNameBehavior,
+ Gerrit.DisplayNameBehavior,
Gerrit.TooltipBehavior,
],
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
similarity index 91%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
index 31c1be5..c793c07 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.html
@@ -17,7 +17,7 @@
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
+<link rel="import" href="../gr-account-chip/gr-account-chip.html">
<link rel="import" href="../gr-account-entry/gr-account-entry.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -56,7 +56,7 @@
account="[[account]]"
class$="[[_computeChipClass(account)]]"
data-account-id$="[[account._account_id]]"
- removable="[[_computeRemovable(account)]]"
+ removable="[[_computeRemovable(account, readonly)]]"
on-keydown="_handleChipKeydown"
tabindex="-1">
</gr-account-chip>
@@ -67,13 +67,13 @@
hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
id="entry"
change="[[change]]"
- filter="[[filter]]"
placeholder="[[placeholder]]"
on-add="_handleAdd"
on-input-keydown="_handleInputKeydown"
allow-any-input="[[allowAnyInput]]"
- allow-any-user="[[allowAnyUser]]">
+ query-suggestions="[[_querySuggestions]]">
</gr-account-entry>
+ <slot></slot>
</template>
<script src="gr-account-list.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
similarity index 79%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 479fee2..9edc9c8 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -19,6 +19,24 @@
const VALID_EMAIL_ALERT = 'Please input a valid email.';
+ const Defs = {};
+
+ /**
+ * @typedef {{
+ * name: string,
+ * value: Object,
+ * }}
+ */
+ Defs.GrSuggestionItem;
+
+ /**
+ * @typedef {{
+ * getSuggestions: function(string): Promise<Array<Object>>,
+ * makeSuggestionItem: function(Object): Defs.GrSuggestionItem,
+ * }}
+ */
+ Defs.GrSuggestionsProvider;
+
Polymer({
is: 'gr-account-list',
_legacyUndefinedCheck: true,
@@ -38,6 +56,19 @@
change: Object,
filter: Function,
placeholder: String,
+ disabled: {
+ type: Function,
+ value: false,
+ },
+
+ /**
+ * Returns suggestions and convert them to list item
+ * @type {Defs.GrSuggestionsProvider}
+ */
+ suggestionsProvider: {
+ type: Object,
+ },
+
/**
* Needed for template checking since value is initially set to null.
* @type {?Object} */
@@ -50,21 +81,6 @@
type: Boolean,
value: false,
},
-
- /**
- * When true, the account-entry autocomplete uses the account suggest API
- * endpoint, which suggests any account in that Gerrit instance (and does
- * not suggest groups).
- *
- * When false/undefined, account-entry uses the suggest_reviewers API
- * endpoint, which suggests any account or group in that Gerrit instance
- * that is not already a reviewer (or is not CCed) on that change.
- */
- allowAnyUser: {
- type: Boolean,
- value: false,
- },
-
/**
* When true, allows for non-suggested inputs to be added.
*/
@@ -82,6 +98,16 @@
type: Number,
value: 0,
},
+
+ /** Returns suggestion items
+ * @type {!function(string): Promise<Array<Defs.GrSuggestionItem>>}
+ */
+ _querySuggestions: {
+ type: Function,
+ value() {
+ return this._getSuggestions.bind(this);
+ },
+ },
},
behaviors: [
@@ -103,31 +129,46 @@
return this.$.entry.focusStart;
},
- _handleAdd(e) {
- this._addReviewer(e.detail.value);
+ _getSuggestions(input) {
+ const provider = this.suggestionsProvider;
+ if (!provider) {
+ return Promise.resolve([]);
+ }
+ return provider.getSuggestions(input).then(suggestions => {
+ if (!suggestions) { return []; }
+ if (this.filter) {
+ suggestions = suggestions.filter(this.filter);
+ }
+ return suggestions.map(suggestion =>
+ provider.makeSuggestionItem(suggestion));
+ });
},
- _addReviewer(reviewer) {
+ _handleAdd(e) {
+ this._addAccountItem(e.detail.value);
+ },
+
+ _addAccountItem(item) {
// Append new account or group to the accounts property. We add our own
// internal properties to the account/group here, so we clone the object
// to avoid cluttering up the shared change object.
- if (reviewer.account) {
+ if (item.account) {
const account =
- Object.assign({}, reviewer.account, {_pendingAdd: true});
+ Object.assign({}, item.account, {_pendingAdd: true});
this.push('accounts', account);
- } else if (reviewer.group) {
- if (reviewer.confirm) {
- this.pendingConfirmation = reviewer;
+ } else if (item.group) {
+ if (item.confirm) {
+ this.pendingConfirmation = item;
return;
}
- const group = Object.assign({}, reviewer.group,
+ const group = Object.assign({}, item.group,
{_pendingAdd: true, _group: true});
this.push('accounts', group);
} else if (this.allowAnyInput) {
- if (!reviewer.includes('@')) {
+ if (!item.includes('@')) {
// Repopulate the input with what the user tried to enter and have
// a toast tell them why they can't enter it.
- this.$.entry.setText(reviewer);
+ this.$.entry.setText(item);
this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: VALID_EMAIL_ALERT},
bubbles: true,
@@ -135,7 +176,7 @@
}));
return false;
} else {
- const account = {email: reviewer, _pendingAdd: true};
+ const account = {email: item, _pendingAdd: true};
this.push('accounts', account);
}
}
@@ -173,8 +214,8 @@
return a === b;
},
- _computeRemovable(account) {
- if (this.readonly) { return false; }
+ _computeRemovable(account, readonly) {
+ if (readonly) { return false; }
if (this.removableValues) {
for (let i = 0; i < this.removableValues.length; i++) {
if (this._accountMatches(this.removableValues[i], account)) {
@@ -193,7 +234,9 @@
},
_removeAccount(toRemove) {
- if (!toRemove || !this._computeRemovable(toRemove)) { return; }
+ if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ return;
+ }
for (let i = 0; i < this.accounts.length; i++) {
let matches;
const account = this.accounts[i];
@@ -277,7 +320,7 @@
submitEntryText() {
const text = this.$.entry.getText();
if (!text.length) { return true; }
- const wasSubmitted = this._addReviewer(text);
+ const wasSubmitted = this._addAccountItem(text);
if (wasSubmitted) { this.$.entry.clear(); }
return wasSubmitted;
},
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
similarity index 77%
rename from polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index d32c269..22e3a3d 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -35,6 +35,15 @@
</test-fixture>
<script>
+ class MockSuggestionsProvider {
+ getSuggestions(input) {
+ return Promise.resolve([]);
+ }
+
+ makeSuggestionItem(item) {
+ return item;
+ }
+ }
suite('gr-account-list tests', () => {
let _nextAccountId = 0;
const makeAccount = function() {
@@ -51,10 +60,11 @@
};
};
- let existingReviewer1;
- let existingReviewer2;
+ let existingAccount1;
+ let existingAccount2;
let sandbox;
let element;
+ let suggestionsProvider;
function getChips() {
return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
@@ -62,14 +72,16 @@
setup(() => {
sandbox = sinon.sandbox.create();
- existingReviewer1 = makeAccount();
- existingReviewer2 = makeAccount();
+ existingAccount1 = makeAccount();
+ existingAccount2 = makeAccount();
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
});
element = fixture('basic');
- element.accounts = [existingReviewer1, existingReviewer2];
+ element.accounts = [existingAccount1, existingAccount2];
+ suggestionsProvider = new MockSuggestionsProvider();
+ element.suggestionsProvider = suggestionsProvider;
});
teardown(() => {
@@ -109,7 +121,7 @@
assert.isTrue(chips[2].classList.contains('pendingAdd'));
// Removed accounts are taken out of the list.
- element.fire('remove', {account: existingReviewer1});
+ element.fire('remove', {account: existingAccount1});
flushAsynchronousOperations();
chips = getChips();
assert.equal(chips.length, 2);
@@ -117,7 +129,7 @@
assert.isTrue(chips[1].classList.contains('pendingAdd'));
// Invalid remove is ignored.
- element.fire('remove', {account: existingReviewer1});
+ element.fire('remove', {account: existingAccount1});
element.fire('remove', {account: newAccount});
flushAsynchronousOperations();
chips = getChips();
@@ -147,6 +159,52 @@
assert.isFalse(chips[0].classList.contains('pendingAdd'));
});
+ test('_getSuggestions uses filter correctly', done => {
+ const originalSuggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ _account_id: 3,
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ _account_id: 1,
+ },
+ {
+ email: 'xyz@example.com',
+ text: 'aaaaa',
+ _account_id: 25,
+ },
+ ];
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(originalSuggestions));
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
+ return {
+ name: suggestion.email,
+ value: suggestion._account_id,
+ };
+ });
+
+
+ element._getSuggestions().then(suggestions => {
+ // Default is no filtering.
+ assert.equal(suggestions.length, 3);
+
+ // Set up filter that only accepts suggestion1.
+ const accountId = originalSuggestions[0]._account_id;
+ element.filter = function(suggestion) {
+ return suggestion._account_id === accountId;
+ };
+
+ element._getSuggestions().then(suggestions => {
+ assert.deepEqual(suggestions,
+ [{name: originalSuggestions[0].email,
+ value: originalSuggestions[0]._account_id}]);
+ }).then(done);
+ });
+ });
+
test('_computeChipClass', () => {
const account = makeAccount();
assert.equal(element._computeChipClass(account), '');
@@ -163,18 +221,18 @@
newAccount._pendingAdd = true;
element.readonly = false;
element.removableValues = [];
- assert.isFalse(element._computeRemovable(existingReviewer1));
- assert.isTrue(element._computeRemovable(newAccount));
+ assert.isFalse(element._computeRemovable(existingAccount1, false));
+ assert.isTrue(element._computeRemovable(newAccount, false));
- element.removableValues = [existingReviewer1];
- assert.isTrue(element._computeRemovable(existingReviewer1));
- assert.isTrue(element._computeRemovable(newAccount));
- assert.isFalse(element._computeRemovable(existingReviewer2));
+ element.removableValues = [existingAccount1];
+ assert.isTrue(element._computeRemovable(existingAccount1, false));
+ assert.isTrue(element._computeRemovable(newAccount, false));
+ assert.isFalse(element._computeRemovable(existingAccount2, false));
element.readonly = true;
- assert.isFalse(element._computeRemovable(existingReviewer1));
- assert.isFalse(element._computeRemovable(newAccount));
+ assert.isFalse(element._computeRemovable(existingAccount1, true));
+ assert.isFalse(element._computeRemovable(newAccount, true));
});
test('submitEntryText', () => {
@@ -293,13 +351,40 @@
assert.isTrue(element.$.entry.hasAttribute('hidden'));
});
- suite('allowAnyInput', () => {
- let entry;
+ test('enter text calls suggestions provider', done => {
+ const suggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ },
+ ];
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(suggestions));
+ const makeSuggestionItemStub =
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+ const input = element.$.entry.$.input;
+
+ input.text = 'newTest';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.calledOnce);
+ assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
+ assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+ done();
+ });
+ });
+
+ suite('allowAnyInput', () => {
setup(() => {
- entry = element.$.entry;
- sandbox.stub(entry, '_getReviewerSuggestions');
- sandbox.stub(entry.$.input, '_updateSuggestions');
element.allowAnyInput = true;
});
@@ -334,7 +419,6 @@
suite('keyboard interactions', () => {
test('backspace at text input start removes last account', () => {
const input = element.$.entry.$.input;
- sandbox.stub(element.$.entry, '_getReviewerSuggestions');
sandbox.stub(input, '_updateSuggestions');
sandbox.stub(element, '_computeRemovable').returns(true);
// Next line is a workaround for Firefix not moving cursor
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
new file mode 100644
index 0000000..238cf15
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
@@ -0,0 +1,55 @@
+(function(window) {
+ 'use strict';
+
+ if (window.GrDisplayNameUtils) {
+ return;
+ }
+
+ const ANONYMOUS_NAME = 'Anonymous';
+
+ class GrDisplayNameUtils {
+ /**
+ * enableEmail when true enables to fallback to using email if
+ * the account name is not avilable.
+ */
+ static getUserName(config, account, enableEmail) {
+ if (account && account.name) {
+ return account.name;
+ } else if (account && account.username) {
+ return account.username;
+ } else if (enableEmail && account && account.email) {
+ return account.email;
+ } else if (config && config.user &&
+ config.user.anonymous_coward_name !== 'Anonymous Coward') {
+ return config.user.anonymous_coward_name;
+ }
+
+ return ANONYMOUS_NAME;
+ }
+
+ static getAccountDisplayName(config, account, enableEmail) {
+ const reviewerName = this._accountOrAnon(config, account, enableEmail);
+ const reviewerEmail = this._accountEmail(account.email);
+ const reviewerStatus = account.status ? '(' + account.status + ')' : '';
+ return [reviewerName, reviewerEmail, reviewerStatus]
+ .filter(p => p.length > 0).join(' ');
+ }
+
+ static _accountOrAnon(config, reviewer, enableEmail) {
+ return this.getUserName(config, reviewer, !!enableEmail);
+ }
+
+ static _accountEmail(email) {
+ if (typeof email !== 'undefined') {
+ return '<' + email + '>';
+ }
+ return '';
+ }
+
+ static getGroupDisplayName(group) {
+ return group.name + ' (group)';
+ }
+ }
+
+ window.GrDisplayNameUtils = GrDisplayNameUtils;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
new file mode 100644
index 0000000..25ca4c5
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 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-display-name-utils</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<script src="gr-display-name-utils.js"></script>
+
+<script>
+ suite('gr-display-name-utils tests', () => {
+ // eslint-disable-next-line no-unused-vars
+ const config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
+
+
+ test('getUserName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ 'test-name');
+ });
+
+ test('getUserName username only', () => {
+ const account = {
+ username: 'test-user',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ 'test-user');
+ });
+
+ test('getUserName email only', () => {
+ const account = {
+ email: 'test-user@test-url.com',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ 'test-user@test-url.com');
+ });
+
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+ 'Anonymous');
+ });
+
+ test('getUserName for the config returning the anon name', () => {
+ const config = {
+ user: {
+ anonymous_coward_name: 'Test Anon',
+ },
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+ 'Test Anon');
+ });
+
+ test('getAccountDisplayName - account with name only', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {name: 'Some user name'}),
+ 'Some user name');
+ });
+
+ test('getAccountDisplayName - account with email only', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {email: 'my@example.com'}),
+ 'Anonymous <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with email only - allowEmail', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {email: 'my@example.com'}, true),
+ 'my@example.com <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with name and status', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ status: 'OOO',
+ }),
+ 'Some name (OOO)');
+ });
+
+ test('getAccountDisplayName - account with name and email', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com',
+ }),
+ 'Some name <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with name, email and status', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com',
+ status: 'OOO',
+ }),
+ 'Some name <my@example.com> (OOO)');
+ });
+
+ test('getGroupDisplayName', () => {
+ assert.equal(
+ GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
+ 'Some user name (group)');
+ });
+
+ test('_accountEmail', () => {
+ assert.equal(
+ GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
+ '<email@gerritreview.com>');
+ assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
new file mode 100644
index 0000000..67001d2
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright (C) 2019 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(window) {
+ 'use strict';
+
+ if (window.GrEmailSuggestionsProvider) {
+ return;
+ }
+
+ class GrEmailSuggestionsProvider {
+ constructor(restAPI) {
+ this._restAPI = restAPI;
+ }
+
+ getSuggestions(input) {
+ return this._restAPI.getSuggestedAccounts(`${input}`)
+ .then(accounts => {
+ if (!accounts) { return []; }
+ return accounts;
+ });
+ }
+
+ makeSuggestionItem(account) {
+ return {
+ name: GrDisplayNameUtils.getAccountDisplayName(null, account, true),
+ value: {account, count: 1},
+ };
+ }
+ }
+
+ window.GrEmailSuggestionsProvider = GrEmailSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
new file mode 100644
index 0000000..fb6b5d4
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 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-email-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-email-suggestions-provider.js"></script>
+
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+</test-fixture>
+
+<script>
+ suite('GrEmailSuggestionsProvider tests', () => {
+ let sandbox;
+ let restAPI;
+ let provider;
+ const account1 = {
+ name: 'Some name',
+ email: 'some@example.com',
+ };
+ const account2 = {
+ email: 'other@example.com',
+ _account_id: 3,
+ };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ restAPI = fixture('basic');
+ provider = new GrEmailSuggestionsProvider(restAPI);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('getSuggestions', done => {
+ const getSuggestedAccountsStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([account1, account2]));
+
+ provider.getSuggestions('Some input').then(res => {
+ assert.deepEqual(res, [account1, account2]);
+ assert.isTrue(getSuggestedAccountsStub.calledOnce);
+ assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+ done();
+ });
+ });
+
+ test('makeSuggestionItem', () => {
+ assert.deepEqual(provider.makeSuggestionItem(account1), {
+ name: 'Some name <some@example.com>',
+ value: {
+ account: account1,
+ count: 1,
+ },
+ });
+
+ assert.deepEqual(provider.makeSuggestionItem(account2), {
+ name: 'other@example.com <other@example.com>',
+ value: {
+ account: account2,
+ count: 1,
+ },
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
new file mode 100644
index 0000000..a95670b
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright (C) 2019 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(window) {
+ 'use strict';
+
+ if (window.GrGroupSuggestionsProvider) {
+ return;
+ }
+
+ class GrGroupSuggestionsProvider {
+ constructor(restAPI) {
+ this._restAPI = restAPI;
+ }
+
+ getSuggestions(input) {
+ return this._restAPI.getSuggestedGroups(`${input}`)
+ .then(groups => {
+ if (!groups) { return []; }
+ const keys = Object.keys(groups);
+ return keys.map(key => {
+ return Object.assign({}, groups[key], {name: key});
+ });
+ });
+ }
+
+ makeSuggestionItem(suggestion) {
+ return {name: suggestion.name,
+ value: {group: {name: suggestion.name, id: suggestion.id}}};
+ }
+ }
+
+ window.GrGroupSuggestionsProvider = GrGroupSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
new file mode 100644
index 0000000..b60aaa9
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 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-group-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-group-suggestions-provider.js"></script>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+</test-fixture>
+
+<script>
+ suite('GrGroupSuggestionsProvider tests', () => {
+ let sandbox;
+ let restAPI;
+ let provider;
+ const group1 = {
+ name: 'Some name',
+ id: 1,
+ };
+ const group2 = {
+ name: 'Other name',
+ id: 3,
+ url: 'abcd',
+ };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ restAPI = fixture('basic');
+ provider = new GrGroupSuggestionsProvider(restAPI);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('getSuggestions', done => {
+ const getSuggestedAccountsStub =
+ sandbox.stub(restAPI, 'getSuggestedGroups')
+ .returns(Promise.resolve({
+ 'Some name': {id: 1},
+ 'Other name': {id: 3, url: 'abcd'},
+ }));
+
+ provider.getSuggestions('Some input').then(res => {
+ assert.deepEqual(res, [group1, group2]);
+ assert.isTrue(getSuggestedAccountsStub.calledOnce);
+ assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+ done();
+ });
+ });
+
+ test('makeSuggestionItem', () => {
+ assert.deepEqual(provider.makeSuggestionItem(group1), {
+ name: 'Some name',
+ value: {
+ group: {
+ name: 'Some name',
+ id: 1,
+ },
+ },
+ });
+
+ assert.deepEqual(provider.makeSuggestionItem(group2), {
+ name: 'Other name',
+ value: {
+ group: {
+ name: 'Other name',
+ id: 3,
+ },
+ },
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
new file mode 100644
index 0000000..7f1a9b1
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -0,0 +1,90 @@
+/**
+ * @license
+ * Copyright (C) 2019 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(window) {
+ 'use strict';
+
+ if (window.GrReviewerSuggestionsProvider) {
+ return;
+ }
+
+ class GrReviewerSuggestionsProvider {
+ constructor(restAPI, changeNumber, allowAnyUser) {
+ this._changeNumber = changeNumber;
+ this._allowAnyUser = allowAnyUser;
+ this._restAPI = restAPI;
+ }
+
+ init() {
+ if (this._initPromise) {
+ return this._initPromise;
+ }
+ const getConfigPromise = this._restAPI.getConfig().then(cfg => {
+ this._config = cfg;
+ });
+ const getLoggedInPromise = this._restAPI.getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ this._initPromise = Promise.all([getConfigPromise, getLoggedInPromise])
+ .then(() => {
+ this._initialized = true;
+ });
+ return this._initPromise;
+ }
+
+ getSuggestions(input) {
+ if (!this._initialized || !this._loggedIn) {
+ return Promise.resolve([]);
+ }
+ const api = this._restAPI;
+ const xhr = this._allowAnyUser ?
+ api.getSuggestedAccounts(`cansee:${this._changeNumber} ${input}`) :
+ api.getChangeSuggestedReviewers(this._changeNumber, input);
+
+ return xhr.then(reviewers => (reviewers || []));
+ }
+
+ makeSuggestionItem(suggestion) {
+ if (suggestion.account) {
+ // Reviewer is an account suggestion from getChangeSuggestedReviewers.
+ return {
+ name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+ suggestion.account, false),
+ value: suggestion,
+ };
+ }
+
+ if (suggestion.group) {
+ // Reviewer is a group suggestion from getChangeSuggestedReviewers.
+ return {
+ name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
+ value: suggestion,
+ };
+ }
+
+ if (suggestion._account_id) {
+ // Reviewer is an account suggestion from getSuggestedAccounts.
+ return {
+ name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+ suggestion, false),
+ value: {account: suggestion, count: 1},
+ };
+ }
+ }
+ }
+
+ window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
+})(window);
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
new file mode 100644
index 0000000..bb73520
--- /dev/null
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -0,0 +1,260 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 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-reviewer-suggestions-provider</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
+<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script src="gr-reviewer-suggestions-provider.js"></script>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+</test-fixture>
+
+<script>
+ suite('GrReviewerSuggestionsProvider tests', () => {
+ let sandbox;
+ let _nextAccountId = 0;
+ const makeAccount = function(opt_status) {
+ const accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
+ name: 'name ' + accountId,
+ email: 'email ' + accountId,
+ status: opt_status,
+ };
+ };
+ let _nextAccountId2 = 0;
+ const makeAccount2 = function(opt_status) {
+ const accountId2 = ++_nextAccountId2;
+ return {
+ _account_id: accountId2,
+ name: 'name ' + accountId2,
+ status: opt_status,
+ };
+ };
+
+ let owner;
+ let existingReviewer1;
+ let existingReviewer2;
+ let suggestion1;
+ let suggestion2;
+ let suggestion3;
+ let restAPI;
+ let provider;
+
+ let redundantSuggestion1;
+ let redundantSuggestion2;
+ let redundantSuggestion3;
+ let change;
+
+ setup(done => {
+ owner = makeAccount();
+ existingReviewer1 = makeAccount();
+ existingReviewer2 = makeAccount();
+ suggestion1 = {account: makeAccount()};
+ suggestion2 = {account: makeAccount()};
+ suggestion3 = {
+ group: {
+ id: 'suggested group id',
+ name: 'suggested group',
+ },
+ };
+
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() { return Promise.resolve({}); },
+ });
+
+ restAPI = fixture('basic');
+ change = {
+ _number: 42,
+ owner,
+ reviewers: {
+ CC: [existingReviewer1],
+ REVIEWER: [existingReviewer2],
+ },
+ };
+ sandbox = sinon.sandbox.create();
+ return flush(done);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+ suite('allowAnyUser set to false', () => {
+ setup(done => {
+ provider = new GrReviewerSuggestionsProvider(restAPI, change._number,
+ false);
+ provider.init().then(done);
+ });
+ suite('stubbed values for _getReviewerSuggestions', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeSuggestedReviewers() {
+ redundantSuggestion1 = {account: existingReviewer1};
+ redundantSuggestion2 = {account: existingReviewer2};
+ redundantSuggestion3 = {account: owner};
+ return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+ },
+ });
+ });
+
+ test('makeSuggestionItem formats account or group accordingly', () => {
+ let account = makeAccount();
+ const account3 = makeAccount2();
+ let suggestion = provider.makeSuggestionItem({account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '>',
+ value: {account},
+ });
+
+ const group = {name: 'test'};
+ suggestion = provider.makeSuggestionItem({group});
+ assert.deepEqual(suggestion, {
+ name: group.name + ' (group)',
+ value: {group},
+ });
+
+ suggestion = provider.makeSuggestionItem(account);
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '>',
+ value: {account, count: 1},
+ });
+
+ suggestion = provider.makeSuggestionItem({account: {}});
+ assert.deepEqual(suggestion, {
+ name: 'Anonymous',
+ value: {account: {}},
+ });
+
+ provider._config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward Name',
+ },
+ };
+
+ suggestion = provider.makeSuggestionItem({account: {}});
+ assert.deepEqual(suggestion, {
+ name: 'Anonymous Coward Name',
+ value: {account: {}},
+ });
+
+ account = makeAccount('OOO');
+
+ suggestion = provider.makeSuggestionItem({account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '> (OOO)',
+ value: {account},
+ });
+
+ suggestion = provider.makeSuggestionItem(account);
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '> (OOO)',
+ value: {account, count: 1},
+ });
+
+ sandbox.stub(GrDisplayNameUtils, '_accountEmail',
+ () => {
+ return '';
+ });
+
+ suggestion = provider.makeSuggestionItem(account3);
+ assert.deepEqual(suggestion, {
+ name: account3.name,
+ value: {account: account3, count: 1},
+ });
+ });
+
+ test('getSuggestions', done => {
+ provider.getSuggestions().then(reviewers => {
+ // Default is no filtering.
+ assert.equal(reviewers.length, 6);
+ assert.deepEqual(reviewers,
+ [redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+ }).then(done);
+ });
+
+ test('getSuggestions short circuits when logged out', () => {
+ // API call is already stubbed.
+ const xhrSpy = restAPI.getChangeSuggestedReviewers;
+ provider._loggedIn = false;
+ return provider.getSuggestions('').then(() => {
+ assert.isFalse(xhrSpy.called);
+ provider._loggedIn = true;
+ return provider.getSuggestions('').then(() => {
+ assert.isTrue(xhrSpy.called);
+ });
+ });
+ });
+ });
+
+ test('getChangeSuggestedReviewers is used', done => {
+ const suggestReviewerStub =
+ sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+ .returns(Promise.resolve([]));
+ const suggestAccountStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([]));
+
+ provider.getSuggestions('').then(() => {
+ assert.isTrue(suggestReviewerStub.calledOnce);
+ assert.isTrue(suggestReviewerStub.calledWith(42, ''));
+ assert.isFalse(suggestAccountStub.called);
+ done();
+ });
+ });
+ });
+
+ suite('allowAnyUser set to true', () => {
+ setup(done => {
+ provider = new GrReviewerSuggestionsProvider(restAPI, change._number,
+ true);
+ provider.init().then(done);
+ });
+
+ test('getSuggestedAccounts is used', done => {
+ const suggestReviewerStub =
+ sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+ .returns(Promise.resolve([]));
+ const suggestAccountStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([]));
+
+ provider.getSuggestions('').then(() => {
+ assert.isFalse(suggestReviewerStub.called);
+ assert.isTrue(suggestAccountStub.calledOnce);
+ assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
+ done();
+ });
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index ac0dbeb..ec3b7d5 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -38,6 +38,8 @@
'SiteBasedCache',
'FetchPromisesCache',
'GrRestApiHelper',
+ 'GrDisplayNameUtils',
+ 'GrReviewerSuggestionsProvider',
'moment',
'page',
'util',
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 9c06113..4754cd8 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -23,6 +23,7 @@
<script src="/bower_components/web-component-tester/browser.js"></script>
<script>
const testFiles = [];
+ const scriptsPath = '../scripts/';
const elementsPath = '../elements/';
const behaviorsPath = '../behaviors/';
@@ -61,9 +62,9 @@
'change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html',
'change-list/gr-create-change-help/gr-create-change-help_test.html',
'change-list/gr-dashboard-view/gr-dashboard-view_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'change-list/gr-repo-header/gr-repo-header_test.html',
'change-list/gr-user-header/gr-user-header_test.html',
- 'change/gr-account-entry/gr-account-entry_test.html',
- 'change/gr-account-list/gr-account-list_test.html',
'change/gr-change-actions/gr-change-actions_test.html',
'change/gr-change-metadata/gr-change-metadata-it_test.html',
'change/gr-change-metadata/gr-change-metadata_test.html',
@@ -105,10 +106,13 @@
'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-coverage-layer/gr-coverage-layer_test.html',
'diff/gr-diff-builder/gr-diff-builder_test.html',
'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-annotation_test.html',
'diff/gr-diff-highlight/gr-diff-highlight_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'diff/gr-diff-host/gr-diff-host_test.html',
'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
'diff/gr-diff-processor/gr-diff-processor_test.html',
'diff/gr-diff-selection/gr-diff-selection_test.html',
@@ -127,6 +131,8 @@
'plugins/gr-admin-api/gr-admin-api_test.html',
'plugins/gr-styles-api/gr-styles-api_test.html',
'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'plugins/gr-dom-hooks/gr-dom-hooks_test.html',
'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html',
'plugins/gr-event-helper/gr-event-helper_test.html',
'plugins/gr-external-style/gr-external-style_test.html',
@@ -135,7 +141,10 @@
'plugins/gr-popup-interface/gr-popup-interface_test.html',
'plugins/gr-repo-api/gr-repo-api_test.html',
'plugins/gr-settings-api/gr-settings-api_test.html',
+ 'plugins/gr-theme-api/gr-theme-api_test.html',
'settings/gr-account-info/gr-account-info_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'settings/gr-agreements-list/gr-agreements-list_test.html',
'settings/gr-change-table-editor/gr-change-table-editor_test.html',
'settings/gr-cla-view/gr-cla-view_test.html',
'settings/gr-edit-preferences/gr-edit-preferences_test.html',
@@ -149,7 +158,9 @@
'settings/gr-settings-view/gr-settings-view_test.html',
'settings/gr-ssh-editor/gr-ssh-editor_test.html',
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
+ 'shared/gr-account-entry/gr-account-entry_test.html',
'shared/gr-account-label/gr-account-label_test.html',
+ 'shared/gr-account-list/gr-account-list_test.html',
'shared/gr-account-link/gr-account-link_test.html',
'shared/gr-alert/gr-alert_test.html',
'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
@@ -161,34 +172,48 @@
'shared/gr-comment-thread/gr-comment-thread_test.html',
'shared/gr-comment/gr-comment_test.html',
'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
+ 'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
'shared/gr-cursor-manager/gr-cursor-manager_test.html',
'shared/gr-date-formatter/gr-date-formatter_test.html',
'shared/gr-dialog/gr-dialog_test.html',
'shared/gr-diff-preferences/gr-diff-preferences_test.html',
'shared/gr-download-commands/gr-download-commands_test.html',
+ 'shared/gr-dropdown/gr-dropdown_test.html',
'shared/gr-dropdown-list/gr-dropdown-list_test.html',
'shared/gr-editable-content/gr-editable-content_test.html',
'shared/gr-editable-label/gr-editable-label_test.html',
'shared/gr-formatted-text/gr-formatted-text_test.html',
+ 'shared/gr-hovercard/gr-hovercard_test.html',
+ 'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
+ 'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
'shared/gr-js-api-interface/gr-js-api-interface_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
'shared/gr-fixed-panel/gr-fixed-panel_test.html',
'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'shared/gr-label-info/gr-label-info_test.html',
'shared/gr-lib-loader/gr-lib-loader_test.html',
'shared/gr-limited-text/gr-limited-text_test.html',
'shared/gr-linked-chip/gr-linked-chip_test.html',
'shared/gr-linked-text/gr-linked-text_test.html',
'shared/gr-list-view/gr-list-view_test.html',
+ 'shared/gr-overlay/gr-overlay_test.html',
'shared/gr-page-nav/gr-page-nav_test.html',
'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html',
'shared/gr-rest-api-interface/gr-auth_test.html',
+ 'shared/gr-rest-api-interface/gr-etag-decorator_test.html',
'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'shared/gr-rest-api-interface/mock-diff-response_test.html',
'shared/gr-select/gr-select_test.html',
+ 'shared/gr-shell-command/gr-shell-command_test.html',
'shared/gr-storage/gr-storage_test.html',
'shared/gr-textarea/gr-textarea_test.html',
'shared/gr-tooltip-content/gr-tooltip-content_test.html',
@@ -212,8 +237,10 @@
'rest-client-behavior/rest-client-behavior_test.html',
'gr-access-behavior/gr-access-behavior_test.html',
'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html',
- 'gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html',
'gr-change-table-behavior/gr-change-table-behavior_test.html',
+ // TODO: uncomment file & fix tests. The file was missed in this list for a long time.
+ // 'gr-list-view-behavior/gr-list-view-behavior_test.html',
+ 'gr-display-name-behavior/gr-display-name-behavior_test.html',
'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
'gr-path-list-behavior/gr-path-list-behavior_test.html',
'gr-tooltip-behavior/gr-tooltip-behavior_test.html',
@@ -227,5 +254,17 @@
testFiles.push(file);
}
+ const scripts = [
+ 'gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html',
+ 'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
+ 'gr-display-name-utils/gr-display-name-utils_test.html',
+ 'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
+ ];
+ /* eslint-enable max-len */
+ for (let file of scripts) {
+ file = scriptsPath + file;
+ testFiles.push(file);
+ }
+
WCT.loadSuites(testFiles);
</script>