Allows editing contact emails in PolyGerrit account settings
Adds a new section to the PolyGerrit settings screen to edit a user's contact email addresses. The user may add new addresses, remove addresses and choose which among their addresses is "preferred". Bug: Issue 3911 Change-Id: Id612762bef52cd1c1b35fdabe6671ecaf349d6b5
This commit is contained in:
parent
73a2b37711
commit
cf0284286d
@ -0,0 +1,82 @@
|
||||
<!--
|
||||
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="../../../bower_components/iron-input/iron-input.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
|
||||
<dom-module id="gr-email-editor">
|
||||
<template>
|
||||
<style>
|
||||
th {
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
th.emailHeader {
|
||||
width: 32.5em;
|
||||
}
|
||||
th.preferredHeader {
|
||||
text-align: center;
|
||||
width: 6em;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
td.preferredControl {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
td.preferredControl:hover {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="emailHeader">Email</th>
|
||||
<th class="preferredHeader">Preferred</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template is="dom-repeat" items="[[_emails]]">
|
||||
<tr>
|
||||
<td>[[item.email]]</td>
|
||||
<td class="preferredControl" on-tap="_handlePreferredControlTap">
|
||||
<input
|
||||
is="iron-input"
|
||||
type="radio"
|
||||
on-change="_handlePreferredChange"
|
||||
name="preferred"
|
||||
value="[[item.email]]"
|
||||
checked$="[[item.preferred]]">
|
||||
</td>
|
||||
<td>
|
||||
<gr-button
|
||||
data-index$="[[index]]"
|
||||
on-tap="_handleDeleteButton"
|
||||
disabled="[[item.preferred]]"
|
||||
class="remove-button">Delete</gr-button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
</template>
|
||||
<script src="gr-email-editor.js"></script>
|
||||
</dom-module>
|
@ -0,0 +1,91 @@
|
||||
// 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-email-editor',
|
||||
|
||||
properties: {
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_emails: Array,
|
||||
_emailsToRemove: {
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
_newPreferred: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
|
||||
loadData: function() {
|
||||
return this.$.restAPI.getAccountEmails().then(function(emails) {
|
||||
this._emails = emails;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
save: function() {
|
||||
var promises = [];
|
||||
|
||||
for (var i = 0; i < this._emailsToRemove.length; i++) {
|
||||
promises.push(this.$.restAPI.deleteAccountEmail(
|
||||
this._emailsToRemove[i].email));
|
||||
}
|
||||
|
||||
if (this._newPreferred) {
|
||||
promises.push(this.$.restAPI.setPreferredAccountEmail(
|
||||
this._newPreferred));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(function() {
|
||||
this._emailsToRemove = [];
|
||||
this._newPreferred = null;
|
||||
this.hasUnsavedChanges = false;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_handleDeleteButton: function(e) {
|
||||
var index = parseInt(e.target.getAttribute('data-index'));
|
||||
var email = this._emails[index];
|
||||
this.push('_emailsToRemove', email);
|
||||
this.splice('_emails', index, 1);
|
||||
this.hasUnsavedChanges = true;
|
||||
},
|
||||
|
||||
_handlePreferredControlTap: function(e) {
|
||||
if (e.target.classList.contains('preferredControl')) {
|
||||
e.target.firstElementChild.click();
|
||||
}
|
||||
},
|
||||
|
||||
_handlePreferredChange: function(e) {
|
||||
var preferred = e.target.value;
|
||||
for (var i = 0; i < this._emails.length; i++) {
|
||||
if (preferred === this._emails[i].email) {
|
||||
this.set(['_emails', i, 'preferred'], true);
|
||||
this._newPreferred = preferred;
|
||||
this.hasUnsavedChanges = true;
|
||||
} else if (this._emails[i].preferred) {
|
||||
this.set(['_emails', i, 'preferred'], false);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
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-email-editor</title>
|
||||
|
||||
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
|
||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||
|
||||
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
|
||||
<link rel="import" href="gr-email-editor.html">
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-email-editor></gr-email-editor>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-email-editor tests', function() {
|
||||
var element;
|
||||
|
||||
setup(function(done) {
|
||||
var emails = [
|
||||
{email: 'email@one.com'},
|
||||
{email: 'email@two.com', preferred: true},
|
||||
{email: 'email@three.com'},
|
||||
];
|
||||
|
||||
stub('gr-rest-api-interface', {
|
||||
getAccountEmails: function() { return Promise.resolve(emails); },
|
||||
});
|
||||
|
||||
element = fixture('basic');
|
||||
|
||||
element.loadData().then(done);
|
||||
});
|
||||
|
||||
test('renders', function() {
|
||||
var rows = element.$$('table').querySelectorAll('tbody tr');
|
||||
|
||||
assert.equal(rows.length, 3);
|
||||
|
||||
assert.isFalse(rows[0].querySelector('input[type=radio]').checked);
|
||||
assert.isNotOk(rows[0].querySelector('gr-button').disabled);
|
||||
|
||||
assert.isTrue(rows[1].querySelector('input[type=radio]').checked);
|
||||
assert.isOk(rows[1].querySelector('gr-button').disabled);
|
||||
|
||||
assert.isFalse(rows[2].querySelector('input[type=radio]').checked);
|
||||
assert.isNotOk(rows[2].querySelector('gr-button').disabled);
|
||||
|
||||
assert.isFalse(element.hasUnsavedChanges);
|
||||
});
|
||||
|
||||
test('edit preferred', function() {
|
||||
var preferredChangedSpy = sinon.spy(element, '_handlePreferredChange');
|
||||
var radios = element.$$('table').querySelectorAll('input[type=radio]');
|
||||
|
||||
assert.isFalse(element.hasUnsavedChanges);
|
||||
assert.isNotOk(element._newPreferred);
|
||||
assert.equal(element._emailsToRemove.length, 0);
|
||||
assert.equal(element._emails.length, 3);
|
||||
assert.isNotOk(radios[0].checked);
|
||||
assert.isOk(radios[1].checked);
|
||||
assert.isFalse(preferredChangedSpy.called);
|
||||
|
||||
radios[0].click();
|
||||
|
||||
assert.isTrue(element.hasUnsavedChanges);
|
||||
assert.isOk(element._newPreferred);
|
||||
assert.equal(element._emailsToRemove.length, 0);
|
||||
assert.equal(element._emails.length, 3);
|
||||
assert.isOk(radios[0].checked);
|
||||
assert.isNotOk(radios[1].checked);
|
||||
assert.isTrue(preferredChangedSpy.called);
|
||||
});
|
||||
|
||||
test('delete email', function() {
|
||||
var deleteSpy = sinon.spy(element, '_handleDeleteButton');
|
||||
var buttons = element.$$('table').querySelectorAll('gr-button');
|
||||
|
||||
assert.isFalse(element.hasUnsavedChanges);
|
||||
assert.isNotOk(element._newPreferred);
|
||||
assert.equal(element._emailsToRemove.length, 0);
|
||||
assert.equal(element._emails.length, 3);
|
||||
|
||||
buttons[2].click();
|
||||
|
||||
assert.isTrue(element.hasUnsavedChanges);
|
||||
assert.isNotOk(element._newPreferred);
|
||||
assert.equal(element._emailsToRemove.length, 1);
|
||||
assert.equal(element._emails.length, 2);
|
||||
|
||||
assert.equal(element._emailsToRemove[0].email, 'email@three.com');
|
||||
});
|
||||
|
||||
test('save changes', function(done) {
|
||||
var deleteEmailStub = sinon.stub(element.$.restAPI, 'deleteAccountEmail');
|
||||
var setPreferredStub = sinon.stub(element.$.restAPI,
|
||||
'setPreferredAccountEmail');
|
||||
var rows = element.$$('table').querySelectorAll('tbody tr');
|
||||
|
||||
assert.isFalse(element.hasUnsavedChanges);
|
||||
assert.isNotOk(element._newPreferred);
|
||||
assert.equal(element._emailsToRemove.length, 0);
|
||||
assert.equal(element._emails.length, 3);
|
||||
|
||||
// Delete the first email and set the last as preferred.
|
||||
rows[0].querySelector('gr-button').click();
|
||||
rows[2].querySelector('input[type=radio]').click();
|
||||
|
||||
assert.isTrue(element.hasUnsavedChanges);
|
||||
assert.equal(element._newPreferred, 'email@three.com');
|
||||
assert.equal(element._emailsToRemove.length, 1);
|
||||
assert.equal(element._emailsToRemove[0].email, 'email@one.com');
|
||||
assert.equal(element._emails.length, 2);
|
||||
|
||||
// Save the changes.
|
||||
element.save().then(function() {
|
||||
assert.equal(deleteEmailStub.callCount, 1);
|
||||
assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
|
||||
|
||||
assert.isTrue(setPreferredStub.called);
|
||||
assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../gr-email-editor/gr-email-editor.html">
|
||||
<link rel="import" href="../gr-menu-editor/gr-menu-editor.html">
|
||||
<link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
@ -62,6 +63,12 @@ limitations under the License.
|
||||
color: #666;
|
||||
padding: 1em var(--default-horizontal-margin);
|
||||
}
|
||||
input {
|
||||
font-size: 1em;
|
||||
}
|
||||
#newEmailInput {
|
||||
width: 20em;
|
||||
}
|
||||
@media only screen and (max-width: 40em) {
|
||||
.loading {
|
||||
padding: 0 var(--default-horizontal-margin);
|
||||
@ -249,6 +256,43 @@ limitations under the License.
|
||||
disabled$="[[!_watchedProjectsChanged]]"
|
||||
id="_handleSaveWatchedProjects">Save Changes</gr-button>
|
||||
</fieldset>
|
||||
<h2 class$="[[_computeHeaderClass(_emailsChanged)]]">
|
||||
Email Addresses
|
||||
</h2>
|
||||
<fieldset id="email">
|
||||
<gr-email-editor
|
||||
id="emailEditor"
|
||||
has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
|
||||
<gr-button
|
||||
on-tap="_handleSaveEmails"
|
||||
disabled$="[[!_emailsChanged]]">Save Changes</gr-button>
|
||||
</fieldset>
|
||||
<fieldset id="newEmail">
|
||||
<section>
|
||||
<span class="title">New Email Address</span>
|
||||
<span class="value">
|
||||
<input
|
||||
id="newEmailInput"
|
||||
bind-value="{{_newEmail}}"
|
||||
is="iron-input"
|
||||
type="text"
|
||||
disabled="[[_addingEmail]]"
|
||||
on-keydown="_handleNewEmailKeydown"
|
||||
placeholder="email@example.com">
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
id="verificationSentMessage"
|
||||
hidden$="[[!_lastSentVerificationEmail]]">
|
||||
<p>
|
||||
A verification email was sent to
|
||||
<em>[[_lastSentVerificationEmail]]</em>. Please check your inbox.
|
||||
</p>
|
||||
</section>
|
||||
<gr-button
|
||||
disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
|
||||
on-tap="_handleAddEmailButton">Send Verification</gr-button>
|
||||
</fieldset>
|
||||
</main>
|
||||
</div>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
|
@ -74,6 +74,15 @@
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
_newEmail: String,
|
||||
_addingEmail: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_lastSentVerificationEmail: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
|
||||
observers: [
|
||||
@ -106,6 +115,8 @@
|
||||
this._watchedProjects = projs;
|
||||
}.bind(this)));
|
||||
|
||||
promises.push(this.$.emailEditor.loadData());
|
||||
|
||||
Promise.all(promises).then(function() {
|
||||
this._loading = false;
|
||||
}.bind(this));
|
||||
@ -207,5 +218,39 @@
|
||||
_computeHeaderClass: function(changed) {
|
||||
return changed ? 'edited' : '';
|
||||
},
|
||||
|
||||
_handleSaveEmails: function() {
|
||||
this.$.emailEditor.save();
|
||||
},
|
||||
|
||||
_handleNewEmailKeydown: function(e) {
|
||||
if (e.keyCode === 13) { // Enter
|
||||
e.stopPropagation;
|
||||
this._handleAddEmailButton();
|
||||
}
|
||||
},
|
||||
|
||||
_isNewEmailValid: function(newEmail) {
|
||||
return newEmail.indexOf('@') !== -1;
|
||||
},
|
||||
|
||||
_computeAddEmailButtonEnabled: function(newEmail, addingEmail) {
|
||||
return this._isNewEmailValid(newEmail) && !addingEmail;
|
||||
},
|
||||
|
||||
_handleAddEmailButton: function() {
|
||||
if (!this._isNewEmailValid(this._newEmail)) { return; }
|
||||
|
||||
this._addingEmail = true;
|
||||
this.$.restAPI.addAccountEmail(this._newEmail).then(function(response) {
|
||||
this._addingEmail = false;
|
||||
|
||||
// If it was unsuccessful.
|
||||
if (response.status < 200 || response.status >= 300) { return; }
|
||||
|
||||
this._lastSentVerificationEmail = this._newEmail;
|
||||
this._newEmail = '';
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -63,6 +63,11 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
function stubAddAccountEmail(statusCode) {
|
||||
return sinon.stub(element.$.restAPI, 'addAccountEmail',
|
||||
function() { return Promise.resolve({ status: statusCode }); });
|
||||
}
|
||||
|
||||
setup(function(done) {
|
||||
account = {
|
||||
_account_id: 123,
|
||||
@ -109,6 +114,7 @@ limitations under the License.
|
||||
getWatchedProjects: function() {
|
||||
return Promise.resolve(watchedProjects);
|
||||
},
|
||||
getAccountEmails: function() { return Promise.resolve([]); },
|
||||
});
|
||||
element = fixture('basic');
|
||||
|
||||
@ -254,5 +260,67 @@ limitations under the License.
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('add email validation', function() {
|
||||
assert.isFalse(element._isNewEmailValid('invalid email'));
|
||||
assert.isTrue(element._isNewEmailValid('vaguely@valid.email'));
|
||||
|
||||
assert.isFalse(
|
||||
element._computeAddEmailButtonEnabled('invalid email'), true);
|
||||
assert.isFalse(
|
||||
element._computeAddEmailButtonEnabled('vaguely@valid.email', true));
|
||||
assert.isTrue(
|
||||
element._computeAddEmailButtonEnabled('vaguely@valid.email', false));
|
||||
});
|
||||
|
||||
test('add email does not save invalid', function() {
|
||||
var addEmailStub = stubAddAccountEmail(201);
|
||||
|
||||
assert.isFalse(element._addingEmail);
|
||||
assert.isNotOk(element._lastSentVerificationEmail);
|
||||
element._newEmail = 'invalid email';
|
||||
|
||||
element._handleAddEmailButton();
|
||||
|
||||
assert.isFalse(element._addingEmail);
|
||||
assert.isFalse(addEmailStub.called);
|
||||
assert.isNotOk(element._lastSentVerificationEmail);
|
||||
|
||||
assert.isFalse(addEmailStub.called);
|
||||
});
|
||||
|
||||
test('add email does save valid', function(done) {
|
||||
var addEmailStub = stubAddAccountEmail(201);
|
||||
|
||||
assert.isFalse(element._addingEmail);
|
||||
assert.isNotOk(element._lastSentVerificationEmail);
|
||||
element._newEmail = 'valid@email.com';
|
||||
|
||||
element._handleAddEmailButton();
|
||||
|
||||
assert.isTrue(element._addingEmail);
|
||||
assert.isTrue(addEmailStub.called);
|
||||
|
||||
assert.isTrue(addEmailStub.called);
|
||||
addEmailStub.lastCall.returnValue.then(function() {
|
||||
assert.isOk(element._lastSentVerificationEmail);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('add email does not set last-email if error', function(done) {
|
||||
var addEmailStub = stubAddAccountEmail(500);
|
||||
|
||||
assert.isNotOk(element._lastSentVerificationEmail);
|
||||
element._newEmail = 'valid@email.com';
|
||||
|
||||
element._handleAddEmailButton();
|
||||
|
||||
assert.isTrue(addEmailStub.called);
|
||||
addEmailStub.lastCall.returnValue.then(function() {
|
||||
assert.isNotOk(element._lastSentVerificationEmail);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -216,6 +216,25 @@
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getAccountEmails: function() {
|
||||
return this._fetchSharedCacheURL('/accounts/self/emails');
|
||||
},
|
||||
|
||||
addAccountEmail: function(email, opt_errFn, opt_ctx) {
|
||||
return this.send('PUT', '/accounts/self/emails/' +
|
||||
encodeURIComponent(email), null, opt_errFn, opt_ctx);
|
||||
},
|
||||
|
||||
deleteAccountEmail: function(email, opt_errFn, opt_ctx) {
|
||||
return this.send('DELETE', '/accounts/self/emails/' +
|
||||
encodeURIComponent(email), null, opt_errFn, opt_ctx);
|
||||
},
|
||||
|
||||
setPreferredAccountEmail: function(email, opt_errFn, opt_ctx) {
|
||||
return this.send('PUT', '/accounts/self/emails/' +
|
||||
encodeURIComponent(email) + '/preferred', null, opt_errFn, opt_ctx);
|
||||
},
|
||||
|
||||
getLoggedIn: function() {
|
||||
return this.getAccount().then(function(account) {
|
||||
return account != null;
|
||||
|
@ -54,6 +54,7 @@ limitations under the License.
|
||||
'diff/gr-diff/gr-diff_test.html',
|
||||
'diff/gr-patch-range-select/gr-patch-range-select_test.html',
|
||||
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
|
||||
'settings/gr-email-editor/gr-email-editor_test.html',
|
||||
'settings/gr-menu-editor/gr-menu-editor_test.html',
|
||||
'settings/gr-settings-view/gr-settings-view_test.html',
|
||||
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
|
||||
|
Loading…
Reference in New Issue
Block a user