Adds "Watched Projects"/Notifications section to PolyGerrit settings
Gives the PolyGerrit settings view a new section for Notifications which replicates the "Watched Projects" screen in the GWT settings UI. Users can add, remove and update which projects they receive email notifications for. Introduces the gr-watched-projects-editor element, which is given the user's watched projects data and is able to display and modify the info. In the same way that the GWT UI includes an autocomplete input for adding new projects to watch, this implementation uses gr-autocomplete. The gr-watched-projects-editor is added to the gr-settings-view, which manages the loading, saving and deleting of the user's watched-project data. Other changes: - Tidies the layout of gr-settings-view to permit larger tables such as gr-watched-projects-editor without disrupting the look. - Tweaks functionality of gr-autocomplete use in the watched projects editor. Specifically, it does not take on a value until the user "commits" to a suggestion, and it does not clear the input when committed unless a flag is enabled. Also now supports placeholder text. - Fixes some gr-settings-view unit-test failures in Safari. - Fixes the markup of some table headers and footers. - Lots of new unit tests. Bug: Issue 3911 Change-Id: I4667c29aa3addcd9b6676bc8763d8e3deb327539
This commit is contained in:
@@ -77,6 +77,7 @@ limitations under the License.
|
||||
<gr-autocomplete
|
||||
id="input"
|
||||
threshold="3"
|
||||
clear-on-commit
|
||||
query="[[_query]]"
|
||||
disabled="[[disabled]]"
|
||||
on-commit="_sendAddRequest"
|
||||
|
||||
@@ -127,7 +127,8 @@
|
||||
this.$.addReviewer.focus();
|
||||
},
|
||||
|
||||
_sendAddRequest: function(e, reviewer) {
|
||||
_sendAddRequest: function(e, detail) {
|
||||
var reviewer = detail.value;
|
||||
var reviewerID;
|
||||
if (reviewer.account) {
|
||||
reviewerID = reviewer.account._account_id;
|
||||
|
||||
@@ -28,9 +28,8 @@ limitations under the License.
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
th.name-header,
|
||||
th.url-header {
|
||||
width: 15em;
|
||||
th.nameHeader {
|
||||
width: 11em;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f4f4f4;
|
||||
@@ -41,13 +40,20 @@ limitations under the License.
|
||||
}
|
||||
input {
|
||||
font-size: 1em;
|
||||
width: 13em;
|
||||
}
|
||||
.newTitleInput {
|
||||
width: 10em;
|
||||
}
|
||||
.newUrlInput {
|
||||
width: 23em;
|
||||
}
|
||||
</style>
|
||||
<table>
|
||||
<thead>
|
||||
<th class="name-header">Name</th>
|
||||
<th class="url-header">URL</th>
|
||||
<tr>
|
||||
<th class="nameHeader">Name</th>
|
||||
<th class="url-header">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template is="dom-repeat" items="[[menuItems]]">
|
||||
@@ -76,25 +82,31 @@ limitations under the License.
|
||||
</template>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<th>
|
||||
<input
|
||||
is="iron-input"
|
||||
on-keydown="_handleInputKeydown"
|
||||
bind-value="{{_newName}}">
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
is="iron-input"
|
||||
on-keydown="_handleInputKeydown"
|
||||
bind-value="{{_newUrl}}">
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>
|
||||
<gr-button
|
||||
disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
|
||||
on-tap="_handleAddButton">Add</gr-button>
|
||||
</th>
|
||||
<tr>
|
||||
<th>
|
||||
<input
|
||||
class="newTitleInput"
|
||||
is="iron-input"
|
||||
placeholder="New Title"
|
||||
on-keydown="_handleInputKeydown"
|
||||
bind-value="{{_newName}}">
|
||||
</th>
|
||||
<th>
|
||||
<input
|
||||
class="newUrlInput"
|
||||
is="iron-input"
|
||||
placeholder="New URL"
|
||||
on-keydown="_handleInputKeydown"
|
||||
bind-value="{{_newUrl}}">
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>
|
||||
<gr-button
|
||||
disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
|
||||
on-tap="_handleAddButton">Add</gr-button>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.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">
|
||||
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
@@ -30,7 +31,7 @@ limitations under the License.
|
||||
}
|
||||
main {
|
||||
margin: 2em auto;
|
||||
max-width: 40em;
|
||||
max-width: 46em;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: .1em;
|
||||
@@ -51,7 +52,7 @@ limitations under the License.
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
padding-right: .5em;
|
||||
width: 15em;
|
||||
width: 11em;
|
||||
}
|
||||
.loading {
|
||||
color: #666;
|
||||
@@ -106,7 +107,7 @@ limitations under the License.
|
||||
<h2>Preferences</h2>
|
||||
<fieldset id="preferences">
|
||||
<section>
|
||||
<span class="title">Maximum Changes Per Page</span>
|
||||
<span class="title">Changes Per Page</span>
|
||||
<span class="value">
|
||||
<select
|
||||
is="gr-select"
|
||||
@@ -175,6 +176,16 @@ limitations under the License.
|
||||
on-tap="_handleSaveMenu"
|
||||
disabled="[[!_menuChanged]]">Save Changes</gr-button>
|
||||
</fieldset>
|
||||
<h2>Notifications</h2>
|
||||
<fieldset id="watchedProjects">
|
||||
<gr-watched-projects-editor
|
||||
on-project-removed="_handleWatchedProjectRemoved"
|
||||
projects="{{_watchedProjects}}"></gr-watched-projects-editor>
|
||||
<gr-button
|
||||
on-tap="_handleSaveWatchedProjects"
|
||||
disabled$="[[!_watchedProjectsChanged]]"
|
||||
id="_handleSaveWatchedProjects">Save Changes</gr-button>
|
||||
</fieldset>
|
||||
</main>
|
||||
</div>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
_watchedProjects: Array,
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
@@ -54,11 +55,20 @@
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_watchedProjectsChanged: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_watchedProjectsToRemove: {
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
},
|
||||
|
||||
observers: [
|
||||
'_handlePrefsChanged(_localPrefs.*)',
|
||||
'_handleMenuChanged(_localMenu.splices)',
|
||||
'_handleProjectsChanged(_watchedProjects.*)',
|
||||
],
|
||||
|
||||
attached: function() {
|
||||
@@ -74,6 +84,10 @@
|
||||
this._cloneMenu();
|
||||
}.bind(this)));
|
||||
|
||||
promises.push(this.$.restAPI.getWatchedProjects().then(function(projs) {
|
||||
this._watchedProjects = projs;
|
||||
}.bind(this)));
|
||||
|
||||
Promise.all(promises).then(function() {
|
||||
this._loading = false;
|
||||
}.bind(this));
|
||||
@@ -128,5 +142,32 @@
|
||||
this._menuChanged = false;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_handleWatchedProjectRemoved: function(e) {
|
||||
var project = e.detail;
|
||||
|
||||
// If it was never saved, then we don't need to do anything.
|
||||
if (project._is_local) { return; }
|
||||
|
||||
this._watchedProjectsToRemove.push(project);
|
||||
this._handleProjectsChanged();
|
||||
},
|
||||
|
||||
_handleProjectsChanged: function() {
|
||||
if (this._loading) { return; }
|
||||
this._watchedProjectsChanged = true;
|
||||
},
|
||||
|
||||
_handleSaveWatchedProjects: function() {
|
||||
this.$.restAPI.deleteWatchedProjects(this._watchedProjectsToRemove)
|
||||
.then(function() {
|
||||
return this.$.restAPI.saveWatchedProjects(this._watchedProjects);
|
||||
}.bind(this))
|
||||
.then(function(watchedProjects) {
|
||||
this._watchedProjects = watchedProjects;
|
||||
this._watchedProjectsChanged = false;
|
||||
this._watchedProjectsToRemove = [];
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -76,11 +76,15 @@ limitations under the License.
|
||||
{url: '/second/url', name: 'second name', target: '_blank'},
|
||||
],
|
||||
};
|
||||
watchedProjects = [];
|
||||
|
||||
stub('gr-rest-api-interface', {
|
||||
getLoggedIn: function() { return Promise.resolve(true); },
|
||||
getAccount: function() { return Promise.resolve(account); },
|
||||
getPreferences: function() { return Promise.resolve(preferences); },
|
||||
getWatchedProjects: function() {
|
||||
return Promise.resolve(watchedProjects);
|
||||
},
|
||||
});
|
||||
element = fixture('basic');
|
||||
|
||||
@@ -102,23 +106,23 @@ limitations under the License.
|
||||
|
||||
test('user preferences', function(done) {
|
||||
// Rendered with the expected preferences selected.
|
||||
assert.equal(valueOf('Maximum Changes Per Page', 'preferences')
|
||||
.firstElementChild.value, preferences.changes_per_page);
|
||||
assert.equal(valueOf('Changes Per Page', 'preferences')
|
||||
.firstElementChild.bindValue, preferences.changes_per_page);
|
||||
assert.equal(valueOf('Date/Time Format', 'preferences')
|
||||
.firstElementChild.value, preferences.date_format);
|
||||
.firstElementChild.bindValue, preferences.date_format);
|
||||
assert.equal(valueOf('Date/Time Format', 'preferences')
|
||||
.lastElementChild.value, preferences.time_format);
|
||||
.lastElementChild.bindValue, preferences.time_format);
|
||||
assert.equal(valueOf('Email Notifications', 'preferences')
|
||||
.firstElementChild.value, preferences.email_strategy);
|
||||
.firstElementChild.bindValue, preferences.email_strategy);
|
||||
assert.equal(valueOf('Diff View', 'preferences')
|
||||
.firstElementChild.value, preferences.diff_view);
|
||||
.firstElementChild.bindValue, preferences.diff_view);
|
||||
|
||||
assert.isFalse(element._prefsChanged);
|
||||
assert.isFalse(element._menuChanged);
|
||||
|
||||
// Change the diff view element.
|
||||
var diffSelect = valueOf('Diff View', 'preferences').firstElementChild;
|
||||
diffSelect.value = 'SIDE_BY_SIDE';
|
||||
diffSelect.bindValue = 'SIDE_BY_SIDE';
|
||||
diffSelect.fire('change');
|
||||
|
||||
assert.isTrue(element._prefsChanged);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<!--
|
||||
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="../../shared/gr-autocomplete/gr-autocomplete.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
|
||||
<dom-module id="gr-watched-projects-editor">
|
||||
<template>
|
||||
<style>
|
||||
th {
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
th.projectHeader {
|
||||
width: 11em;
|
||||
}
|
||||
th.notificationHeader {
|
||||
text-align: center;
|
||||
}
|
||||
th.notifType {
|
||||
text-align: center;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
td.notifControl {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
td.notifControl:hover {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.projectFilter {
|
||||
color: #777;
|
||||
font-style: italic;
|
||||
margin-left: 1em;
|
||||
}
|
||||
input {
|
||||
font-size: 1em;
|
||||
}
|
||||
.newProjectInput {
|
||||
width: 10em;
|
||||
}
|
||||
.newFilterInput {
|
||||
width: 26em;
|
||||
}
|
||||
</style>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="projectHeader">Project</th>
|
||||
<template is="dom-repeat" items="[[_getTypes()]]">
|
||||
<th class="notifType">[[item.name]]</th>
|
||||
</template>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[projects]]"
|
||||
as="project"
|
||||
index-as="projectIndex">
|
||||
<tr>
|
||||
<td>
|
||||
[[project.project]]
|
||||
<template is="dom-if" if="[[project.filter]]">
|
||||
<div class="projectFilter">[[project.filter]]</div>
|
||||
</template>
|
||||
</td>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[_getTypes()]]"
|
||||
as="type">
|
||||
<td class="notifControl" on-tap="_handleNotifCellTap">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-index$="[[projectIndex]]"
|
||||
data-key$="[[type.key]]"
|
||||
on-change="_handleCheckboxChange"
|
||||
checked$="[[_computeCheckboxChecked(project, type.key)]]">
|
||||
</td>
|
||||
</template>
|
||||
<td class="delete-column">
|
||||
<gr-button
|
||||
data-index$="[[projectIndex]]"
|
||||
on-tap="_handleRemoveProject">Delete</gr-button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>
|
||||
<gr-autocomplete
|
||||
id="newProject"
|
||||
class="newProjectInput"
|
||||
is="iron-input"
|
||||
query="[[_query]]"
|
||||
threshold="3"
|
||||
placeholder="Project"></gr-autocomplete>
|
||||
</th>
|
||||
<th colspan$="[[_getTypeCount()]]">
|
||||
<input
|
||||
id="newFilter"
|
||||
class="newFilterInput"
|
||||
is="iron-input"
|
||||
placeholder="branch:name, or other search expression">
|
||||
</th>
|
||||
<th>
|
||||
<gr-button on-tap="_handleAddProject">Add</gr-button>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
</template>
|
||||
<script src="gr-watched-projects-editor.js"></script>
|
||||
</dom-module>
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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';
|
||||
|
||||
var NOTIFICATION_TYPES = [
|
||||
{name: 'Changes', key: 'notify_new_changes'},
|
||||
{name: 'Patches', key: 'notify_new_patch_sets'},
|
||||
{name: 'Comments', key: 'notify_all_comments'},
|
||||
{name: 'Submits', key: 'notify_submitted_changes'},
|
||||
{name: 'Abandons', key: 'notify_abandoned_changes'},
|
||||
];
|
||||
|
||||
Polymer({
|
||||
is: 'gr-watched-projects-editor',
|
||||
|
||||
/**
|
||||
* Fired when a watched project is removed from the list.
|
||||
*
|
||||
* @event project-removed
|
||||
*/
|
||||
|
||||
properties: {
|
||||
projects: Array,
|
||||
_query: {
|
||||
type: Function,
|
||||
value: function() {
|
||||
return this._getProjectSuggestions.bind(this);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
_getTypes: function() {
|
||||
return NOTIFICATION_TYPES;
|
||||
},
|
||||
|
||||
_getTypeCount: function() {
|
||||
return this._getTypes().length;
|
||||
},
|
||||
|
||||
_computeCheckboxChecked: function(project, key) {
|
||||
return project.hasOwnProperty(key);
|
||||
},
|
||||
|
||||
_getProjectSuggestions: function(input) {
|
||||
return this.$.restAPI.getSuggestedProjects(input)
|
||||
.then(function(response) {
|
||||
var projects = [];
|
||||
for (var key in response) {
|
||||
projects.push({
|
||||
name: key,
|
||||
value: response[key],
|
||||
});
|
||||
}
|
||||
return projects;
|
||||
});
|
||||
},
|
||||
|
||||
_handleRemoveProject: function(e) {
|
||||
var index = parseInt(e.target.getAttribute('data-index'), 10);
|
||||
var project = this.projects[index];
|
||||
this.splice('projects', index, 1);
|
||||
this.fire('project-removed', project);
|
||||
},
|
||||
|
||||
_canAddProject: function(project, filter) {
|
||||
if (!project || !project.id) { return false; }
|
||||
|
||||
// Check if the project with filter is already in the list. Compare
|
||||
// filters using == to coalesce null and undefined.
|
||||
for (var i = 0; i < this.projects.length; i++) {
|
||||
if (this.projects[i].project === project.id &&
|
||||
this.projects[i].filter == filter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_getNewProjectIndex: function(name, filter) {
|
||||
for (var i = 0; i < this.projects.length; i++) {
|
||||
if (this.projects[i].project > name ||
|
||||
(this.projects[i].project === name &&
|
||||
this.projects[i].filter > filter)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
},
|
||||
|
||||
_handleAddProject: function() {
|
||||
var newProject = this.$.newProject.value;
|
||||
var newProjectName = this.$.newProject.text;
|
||||
var filter = this.$.newFilter.value || null;
|
||||
|
||||
if (!this._canAddProject(newProject, filter)) { return; }
|
||||
|
||||
var insertIndex = this._getNewProjectIndex(newProjectName, filter);
|
||||
|
||||
this.splice('projects', insertIndex, 0, {
|
||||
project: newProjectName,
|
||||
filter: filter,
|
||||
_is_local: true,
|
||||
});
|
||||
|
||||
this.$.newProject.clear();
|
||||
this.$.newFilter.bindValue = '';
|
||||
},
|
||||
|
||||
_handleCheckboxChange: function(e) {
|
||||
var index = parseInt(e.target.getAttribute('data-index'), 10);
|
||||
var key = e.target.getAttribute('data-key');
|
||||
var checked = e.target.checked;
|
||||
this.set(['projects', index, key], !!checked);
|
||||
},
|
||||
|
||||
_handleNotifCellTap: function(e) {
|
||||
var checkbox = Polymer.dom(e.target).querySelector('input');
|
||||
if (checkbox) { checkbox.click(); }
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,193 @@
|
||||
<!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-settings-view</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-watched-projects-editor.html">
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-watched-projects-editor></gr-watched-projects-editor>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-watched-projects-editor tests', function() {
|
||||
var element;
|
||||
|
||||
setup(function() {
|
||||
stub('gr-rest-api-interface', {
|
||||
getSuggestedProjects: function(input) {
|
||||
if (input.indexOf('the') === 0) {
|
||||
return Promise.resolve({'the project': {
|
||||
id: 'the project',
|
||||
state: 'ACTIVE',
|
||||
web_links: [],
|
||||
}});
|
||||
} else {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
element = fixture('basic');
|
||||
element.projects = [{
|
||||
project: 'project a',
|
||||
notify_submitted_changes: true,
|
||||
notify_abandoned_changes: true
|
||||
}, {
|
||||
project: 'project b',
|
||||
filter: 'filter 1',
|
||||
notify_new_changes: true,
|
||||
}, {
|
||||
project: 'project b',
|
||||
filter: 'filter 2',
|
||||
}, {
|
||||
project: 'project c',
|
||||
notify_new_changes: true,
|
||||
notify_new_patch_sets: true,
|
||||
notify_all_comments: true
|
||||
},
|
||||
];
|
||||
|
||||
Polymer.dom.flush();
|
||||
});
|
||||
|
||||
test('renders', function() {
|
||||
var rows = element.$$('table').querySelectorAll('tbody tr');
|
||||
assert.equal(rows.length, 4);
|
||||
|
||||
function getKeysOfRow(row) {
|
||||
var boxes = rows[row].querySelectorAll('input[checked]');
|
||||
return Array.prototype.map.call(boxes,
|
||||
function(e) { return e.getAttribute('data-key'); });
|
||||
}
|
||||
|
||||
var checkedKeys = getKeysOfRow(0);
|
||||
assert.equal(checkedKeys.length, 2);
|
||||
assert.equal(checkedKeys[0], 'notify_submitted_changes');
|
||||
assert.equal(checkedKeys[1], 'notify_abandoned_changes');
|
||||
|
||||
checkedKeys = getKeysOfRow(1);
|
||||
assert.equal(checkedKeys.length, 1);
|
||||
assert.equal(checkedKeys[0], 'notify_new_changes');
|
||||
|
||||
checkedKeys = getKeysOfRow(2);
|
||||
assert.equal(checkedKeys.length, 0);
|
||||
|
||||
checkedKeys = getKeysOfRow(3);
|
||||
assert.equal(checkedKeys.length, 3);
|
||||
assert.equal(checkedKeys[0], 'notify_new_changes');
|
||||
assert.equal(checkedKeys[1], 'notify_new_patch_sets');
|
||||
assert.equal(checkedKeys[2], 'notify_all_comments');
|
||||
});
|
||||
|
||||
test('_getProjectSuggestions empty', function(done) {
|
||||
element._getProjectSuggestions('nonexistent').then(function(projects) {
|
||||
assert.equal(projects.length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('_getProjectSuggestions non-empty', function(done) {
|
||||
element._getProjectSuggestions('the project').then(function(projects) {
|
||||
assert.equal(projects.length, 1);
|
||||
assert.equal(projects[0].name, 'the project');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('_canAddProject', function() {
|
||||
assert.isFalse(element._canAddProject(null, null));
|
||||
assert.isFalse(element._canAddProject({}, null));
|
||||
|
||||
// Can add a project that is not in the list.
|
||||
assert.isTrue(element._canAddProject({id: 'project d'}, null));
|
||||
assert.isTrue(element._canAddProject({id: 'project d'}, 'filter 3'));
|
||||
|
||||
// Cannot add a project that is in the list with no filter.
|
||||
assert.isFalse(element._canAddProject({id: 'project a'}, null));
|
||||
|
||||
// Can add a project that is in the list if the filter differs.
|
||||
assert.isTrue(element._canAddProject({id: 'project a'}, 'filter 4'));
|
||||
|
||||
// Cannot add a project that is in the list with the same filter.
|
||||
assert.isFalse(element._canAddProject({id: 'project b'}, 'filter 1'));
|
||||
assert.isFalse(element._canAddProject({id: 'project b'}, 'filter 2'));
|
||||
|
||||
// Can add a projec that is in the list using a new filter.
|
||||
assert.isTrue(element._canAddProject({id: 'project b'}, 'filter 3'));
|
||||
});
|
||||
|
||||
test('_getNewProjectIndex', function() {
|
||||
// Projects are sorted in ASCII order.
|
||||
assert.equal(element._getNewProjectIndex('project A', 'filter'), 0);
|
||||
assert.equal(element._getNewProjectIndex('project a', 'filter'), 1);
|
||||
|
||||
// Projects are sorted by filter when the names are equal
|
||||
assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1);
|
||||
assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2);
|
||||
assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3);
|
||||
|
||||
// Projects with filters follow those without
|
||||
assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
|
||||
});
|
||||
|
||||
test('_handleAddProject', function() {
|
||||
element.$.newProject.value = {id: 'project d'};
|
||||
element.$.newProject.setText('project d');
|
||||
element.$.newFilter.bindValue = '';
|
||||
|
||||
element._handleAddProject();
|
||||
|
||||
assert.equal(element.projects.length, 5);
|
||||
assert.equal(element.projects[4].project, 'project d');
|
||||
assert.isNotOk(element.projects[4].filter);
|
||||
assert.isTrue(element.projects[4]._is_local);
|
||||
});
|
||||
|
||||
test('_handleAddProject with invalid inputs', function() {
|
||||
element.$.newProject.value = {id: 'project b'};
|
||||
element.$.newProject.setText('project b');
|
||||
element.$.newFilter.bindValue = 'filter 1';
|
||||
|
||||
element._handleAddProject();
|
||||
|
||||
assert.equal(element.projects.length, 4);
|
||||
});
|
||||
|
||||
test('_handleRemoveProject', function() {
|
||||
var removedSpy = sinon.spy();
|
||||
element.addEventListener('project-removed', removedSpy);
|
||||
|
||||
var button = element.$$('table tbody tr:nth-child(2) gr-button');
|
||||
MockInteractions.tap(button);
|
||||
|
||||
var rows = element.$$('table tbody').querySelectorAll('tr');
|
||||
assert.equal(rows.length, 3);
|
||||
|
||||
assert.isTrue(removedSpy.called);
|
||||
assert.isOk(removedSpy.getCall(0).args[0].detail);
|
||||
assert.equal(removedSpy.getCall(0).args[0].detail.project, 'project b');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -45,6 +45,7 @@ limitations under the License.
|
||||
is="iron-input"
|
||||
disabled$="[[disabled]]"
|
||||
bind-value="{{text}}"
|
||||
placeholder="[[placeholder]]"
|
||||
on-keydown="_handleInputKeydown"
|
||||
on-focus="_updateSuggestions" />
|
||||
<div
|
||||
|
||||
@@ -66,17 +66,26 @@
|
||||
observer: '_updateSuggestions',
|
||||
},
|
||||
|
||||
_value: {
|
||||
type: Object,
|
||||
computed: '_getValue(_suggestions, _index)'
|
||||
placeholder: String,
|
||||
|
||||
clearOnCommit: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
value: Object,
|
||||
|
||||
_suggestions: {
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
|
||||
_index: Number,
|
||||
|
||||
_disableSuggestions: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
@@ -95,15 +104,31 @@
|
||||
this.text = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the text of the input without triggering the suggestion dropdown.
|
||||
* @param {String} text The new text for the input.
|
||||
*/
|
||||
setText: function(text) {
|
||||
this._disableSuggestions = true;
|
||||
this.text = text;
|
||||
this._disableSuggestions = false;
|
||||
},
|
||||
|
||||
_updateSuggestions: function() {
|
||||
if (this._disableSuggestions) { return; }
|
||||
|
||||
if (this.text.length < this.threshold) {
|
||||
this._suggestions = [];
|
||||
this.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.query(this.text).then(function(suggestions) {
|
||||
this._suggestions = suggestions;
|
||||
this.$.cursor.moveToStart();
|
||||
if (this._index === -1) {
|
||||
this.value = null;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
@@ -143,9 +168,9 @@
|
||||
this.fire('cancel');
|
||||
},
|
||||
|
||||
_getValue: function(suggestions, index) {
|
||||
if (!suggestions.length || index === -1) { return null; }
|
||||
return suggestions[index].value;
|
||||
_updateValue: function(suggestions, index) {
|
||||
if (!suggestions.length || index === -1) { return; }
|
||||
this.value = suggestions[index].value;
|
||||
},
|
||||
|
||||
_handleBodyClick: function(e) {
|
||||
@@ -164,8 +189,17 @@
|
||||
},
|
||||
|
||||
_commit: function() {
|
||||
this.fire('commit', this._value);
|
||||
this.clear();
|
||||
this._updateValue(this._suggestions, this._index);
|
||||
|
||||
var value = this.value;
|
||||
|
||||
if (!this.clearOnCommit && this._suggestions[this._index]) {
|
||||
this.setText(this._suggestions[this._index].name);
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
this.fire('commit', {value: value});
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -127,29 +127,67 @@ limitations under the License.
|
||||
element.addEventListener('commit', commitHandler);
|
||||
|
||||
assert.equal(element.$.cursor.index, 0);
|
||||
assert.equal(element._value, 0);
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
|
||||
|
||||
assert.equal(element.$.cursor.index, 1);
|
||||
assert.equal(element._value, 1);
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
|
||||
|
||||
assert.equal(element.$.cursor.index, 2);
|
||||
assert.equal(element._value, 2);
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.input, 38); // Up
|
||||
|
||||
assert.equal(element.$.cursor.index, 1);
|
||||
assert.equal(element._value, 1);
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
|
||||
|
||||
assert.equal(element.value, 1);
|
||||
assert.isTrue(commitHandler.called);
|
||||
assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
|
||||
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('clear-on-commit behavior (off)', function(done) {
|
||||
var promise;
|
||||
var queryStub = sinon.spy(function() {
|
||||
return promise = Promise.resolve([{name: 'suggestion', value: 0}]);
|
||||
});
|
||||
element.query = queryStub;
|
||||
element.text = 'blah';
|
||||
|
||||
promise.then(function() {
|
||||
var commitHandler = sinon.spy();
|
||||
element.addEventListener('commit', commitHandler);
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
|
||||
|
||||
assert.isTrue(commitHandler.called);
|
||||
assert.equal(commitHandler.getCall(0).args[0].detail, 1);
|
||||
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
|
||||
assert.equal(element.text, 'suggestion');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('clear-on-commit behavior (on)', function(done) {
|
||||
var promise;
|
||||
var queryStub = sinon.spy(function() {
|
||||
return promise = Promise.resolve([{name: 'suggestion', value: 0}]);
|
||||
});
|
||||
element.query = queryStub;
|
||||
element.text = 'blah';
|
||||
element.clearOnCommit = true;
|
||||
|
||||
promise.then(function() {
|
||||
var commitHandler = sinon.spy();
|
||||
element.addEventListener('commit', commitHandler);
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
|
||||
|
||||
assert.isTrue(commitHandler.called);
|
||||
assert.equal(element.text, '');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,6 +235,23 @@
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getWatchedProjects: function() {
|
||||
return this._fetchSharedCacheURL('/accounts/self/watched.projects');
|
||||
},
|
||||
|
||||
saveWatchedProjects: function(projects, opt_errFn, opt_ctx) {
|
||||
return this.send('POST', '/accounts/self/watched.projects', projects,
|
||||
opt_errFn, opt_ctx)
|
||||
.then(function(response) {
|
||||
return this.getResponseObject(response);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
deleteWatchedProjects: function(projects, opt_errFn, opt_ctx) {
|
||||
return this.send('POST', '/accounts/self/watched.projects:delete',
|
||||
projects, opt_errFn, opt_ctx);
|
||||
},
|
||||
|
||||
_fetchSharedCacheURL: function(url, opt_errFn) {
|
||||
if (this._sharedFetchPromises[url]) {
|
||||
return this._sharedFetchPromises[url];
|
||||
@@ -417,6 +434,10 @@
|
||||
});
|
||||
},
|
||||
|
||||
getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) {
|
||||
return this.fetchJSON('/projects/', opt_errFn, opt_ctx, { p: inputVal, });
|
||||
},
|
||||
|
||||
addChangeReviewer: function(changeNum, reviewerID) {
|
||||
return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
|
||||
},
|
||||
|
||||
@@ -55,6 +55,7 @@ limitations under the License.
|
||||
'diff/gr-selection-action-box/gr-selection-action-box_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',
|
||||
'shared/gr-autocomplete/gr-autocomplete_test.html',
|
||||
'shared/gr-account-label/gr-account-label_test.html',
|
||||
'shared/gr-account-link/gr-account-link_test.html',
|
||||
|
||||
Reference in New Issue
Block a user