Refactor directory structure of components

There is no change in functionality. Only moving things around.

+ Separate html from the js.
+ Place the unit test for a component within the same folder.
+ Organize the components in subfolders.

Change-Id: I51fdc510db75fc1b33f040ca63decbbdfd4d5513
This commit is contained in:
Andrew Bonventre
2016-03-04 17:48:22 -05:00
parent bd1eca6207
commit 78792e8e98
133 changed files with 8191 additions and 7747 deletions

View File

@@ -0,0 +1,89 @@
<!--
Copyright (C) 2015 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="../../../styles/gr-change-list-styles.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<dom-module id="gr-change-list-item">
<template>
<style>
:host {
display: flex;
border-bottom: 1px solid #eee;
}
:host([selected]) {
background-color: #ebf5fb;
}
:host([needs-review]) {
font-weight: bold;
}
.cell {
flex-shrink: 0;
padding: .3em .5em;
}
a {
color: var(--default-text-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.positionIndicator {
visibility: hidden;
}
:host([selected]) .positionIndicator {
visibility: visible;
}
.u-monospace {
font-family: var(--monospace-font-family);
}
.u-green {
color: #388E3C;
}
.u-red {
color: #D32F2F;
}
</style>
<style include="gr-change-list-styles"></style>
<span class="cell keyboard">
<span class="positionIndicator">&#x25b6;</span>
</span>
<span class="cell star" hidden$="[[!showStar]]">
<gr-change-star change="{{change}}"></gr-change-star>
</span>
<a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
<span class="cell status">[[_computeChangeStatusString(change)]]</span>
<span class="cell owner">
<gr-account-link account="[[change.owner]]"></gr-account-link>
</span>
<a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
<a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
<gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter>
<span class="cell size u-monospace">
<span class="u-green"><span>+</span>[[change.insertions]]</span>,
<span class="u-red"><span>-</span>[[change.deletions]]</span>
</span>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
<span title$="[[_computeLabelTitle(change, labelName)]]"
class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span>
</template>
</template>
<script src="gr-change-list-item.js"></script>
</dom-module>

View File

@@ -0,0 +1,132 @@
// 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-change-list-item',
properties: {
selected: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
needsReview: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
labelNames: {
type: Array,
},
change: Object,
changeURL: {
type: String,
computed: '_computeChangeURL(change._number)',
},
showStar: {
type: Boolean,
value: false,
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
_computeChangeURL: function(changeNum) {
if (!changeNum) { return ''; }
return '/c/' + changeNum + '/';
},
_computeChangeStatusString: function(change) {
if (change.status == this.ChangeStatus.MERGED) {
return 'Merged';
}
if (change.mergeable != null && change.mergeable == false) {
return 'Merge Conflict';
}
if (change.status == this.ChangeStatus.DRAFT) {
return 'Draft';
}
if (change.status == this.ChangeStatus.ABANDONED) {
return 'Abandoned';
}
return '';
},
_computeLabelTitle: function(change, labelName) {
var label = change.labels[labelName];
if (!label) { return labelName; }
var significantLabel = label.rejected || label.approved ||
label.disliked || label.recommended;
if (significantLabel && significantLabel.name) {
return labelName + '\nby ' + significantLabel.name;
}
return labelName;
},
_computeLabelClass: function(change, labelName) {
var label = change.labels[labelName];
// Mimic a Set.
var classes = {
'cell': true,
'label': true,
};
if (label) {
if (label.approved) {
classes['u-green'] = true;
}
if (label.value == 1) {
classes['u-monospace'] = true;
classes['u-green'] = true;
} else if (label.value == -1) {
classes['u-monospace'] = true;
classes['u-red'] = true;
}
if (label.rejected) {
classes['u-red'] = true;
}
}
return Object.keys(classes).sort().join(' ');
},
_computeLabelValue: function(change, labelName) {
var label = change.labels[labelName];
if (!label) { return ''; }
if (label.approved) {
return '✓';
}
if (label.rejected) {
return '✕';
}
if (label.value > 0) {
return '+' + label.value;
}
if (label.value < 0) {
return label.value;
}
return '';
},
_computeProjectURL: function(project) {
return '/projects/' + project + ',dashboards/default';
},
_computeProjectBranchURL: function(project, branch) {
return '/q/status:open+project:' + project + '+branch:' + branch;
},
});
})();

View File

@@ -18,12 +18,12 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-item</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../elements/gr-change-list-item.html">
<link rel="import" href="gr-change-list-item.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,91 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../gr-change-list/gr-change-list.html">
<dom-module id="gr-change-list-view">
<template>
<style>
:host {
background-color: var(--view-background-color);
display: block;
margin: 0 var(--default-horizontal-margin);
}
.loading,
.error {
margin-top: 1em;
background-color: #f1f2f3;
}
.loading {
color: #666;
}
.error {
color: #D32F2F;
}
gr-change-list {
margin-top: 1em;
width: 100%;
}
nav {
margin-bottom: 1em;
padding: .5em 0;
text-align: center;
}
nav a {
display: inline-block;
}
nav a:first-of-type {
margin-right: .5em;
}
@media only screen and (max-width: 50em) {
:host {
margin: 0;
}
.loading,
.error {
padding: 0 var(--default-horizontal-margin);
}
}
</style>
<gr-ajax
auto
url="/changes/"
params="[[_computeQueryParams(_query, _offset)]]"
last-response="{{_changes}}"
last-error="{{_lastError}}"
loading="{{_loading}}"></gr-ajax>
<div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
<div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden>
[[_lastError.request.xhr.responseText]]
</div>
<div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden>
<gr-change-list
changes="{{_changes}}"
selected-index="{{viewState.selectedChangeIndex}}"
show-star="[[loggedIn]]"></gr-change-list>
<nav>
<a href$="[[_computeNavLink(_query, _offset, -1)]]"
hidden$="[[_hidePrevArrow(_offset)]]">&larr; Prev</a>
<a href$="[[_computeNavLink(_query, _offset, 1)]]"
hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</a>
</nav>
</div>
</template>
<script src="gr-change-list-view.js"></script>
</dom-module>

View File

@@ -0,0 +1,149 @@
// 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 DEFAULT_NUM_CHANGES = 25;
Polymer({
is: 'gr-change-list-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* True when user is logged in.
*/
loggedIn: {
type: Boolean,
value: false,
},
/**
* State persisted across restamps of the element.
*/
viewState: {
type: Object,
notify: true,
value: function() { return {}; },
},
/**
* Currently active query.
*/
_query: String,
/**
* Offset of currently visible query results.
*/
_offset: Number,
/**
* Change objects loaded from the server.
*/
_changes: Array,
/**
* Contains error of last request (in case of change loading error).
*/
_lastError: Object,
/**
* For showing a "loading..." string during ajax requests.
*/
_loading: {
type: Boolean,
value: true,
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
attached: function() {
this.fire('title-change', {title: this._query});
},
_paramsChanged: function(value) {
if (value.view != this.tagName.toLowerCase()) { return; }
this._query = value.query;
this._offset = value.offset || 0;
if (this.viewState.query != this._query ||
this.viewState.offset != this._offset) {
this.set('viewState.selectedChangeIndex', 0);
this.set('viewState.query', this._query);
this.set('viewState.offset', this._offset);
}
this.fire('title-change', {title: this._query});
},
_computeQueryParams: function(query, offset) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.DETAILED_ACCOUNTS
);
var obj = {
n: DEFAULT_NUM_CHANGES, // Number of results to return.
O: options,
S: offset || 0,
};
if (query && query.length > 0) {
obj.q = query;
}
return obj;
},
_computeNavLink: function(query, offset, direction) {
// Offset could be a string when passed from the router.
offset = +(offset || 0);
var newOffset = Math.max(0, offset + (25 * direction));
var href = '/q/' + query;
if (newOffset > 0) {
href += ',' + newOffset;
}
return href;
},
_computeErrorHidden: function(loading, lastError) {
return loading || lastError == null;
},
_computeListHidden: function(loading, lastError) {
return loading || lastError != null;
},
_hidePrevArrow: function(offset) {
return offset == 0;
},
_hideNextArrow: function(changesLen) {
return changesLen < DEFAULT_NUM_CHANGES;
},
});
})();

View File

@@ -0,0 +1,66 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../../styles/gr-change-list-styles.html">
<link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
<dom-module id="gr-change-list">
<template>
<style>
:host {
display: flex;
flex-direction: column;
}
</style>
<style include="gr-change-list-styles"></style>
<div class="headerRow">
<span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
<span class="topHeader star" hidden$="[[!showStar]]"></span>
<span class="topHeader subject">Subject</span>
<span class="topHeader status">Status</span>
<span class="topHeader owner">Owner</span>
<span class="topHeader project">Project</span>
<span class="topHeader branch">Branch</span>
<span class="topHeader updated">Updated</span>
<span class="topHeader size">Size</span>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
<span class="topHeader label" title$="[[labelName]]">
[[_computeLabelShortcut(labelName)]]
</span>
</template>
</div>
<template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex">
<template is="dom-if" if="[[_groupTitle(groupIndex)]]">
<div class="groupHeader">[[_groupTitle(groupIndex)]]</div>
</template>
<template is="dom-if" if="[[!changeGroup.length]]">
<div class="noChanges">No changes</div>
</template>
<template is="dom-repeat" items="[[changeGroup]]" as="change">
<gr-change-list-item
selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]"
needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
change="[[change]]"
show-star="[[showStar]]"
label-names="[[labelNames]]"></gr-change-list-item>
</template>
</template>
</template>
<script src="gr-change-list.js"></script>
</dom-module>

View File

@@ -0,0 +1,165 @@
// 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-change-list',
hostAttributes: {
tabindex: 0,
},
properties: {
/**
* The logged-in user's account, or an empty object if no user is logged
* in.
*/
account: {
type: Object,
value: function() { return {}; },
},
/**
* An array of ChangeInfo objects to render.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
*/
changes: {
type: Array,
observer: '_changesChanged',
},
/**
* ChangeInfo objects grouped into arrays. The groups and changes
* properties should not be used together.
*/
groups: {
type: Array,
value: function() { return []; },
},
groupTitles: {
type: Array,
value: function() { return []; },
},
labelNames: {
type: Array,
computed: '_computeLabelNames(groups)',
},
selectedIndex: {
type: Number,
notify: true,
},
showStar: {
type: Boolean,
value: false,
},
showReviewedState: {
type: Boolean,
value: false,
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
_computeLabelNames: function(groups) {
if (!groups) { return []; }
var labels = [];
var nonExistingLabel = function(item) {
return labels.indexOf(item) < 0;
};
for (var i = 0; i < groups.length; i++) {
var group = groups[i];
for (var j = 0; j < group.length; j++) {
var change = group[j];
if (!change.labels) { continue; }
var currentLabels = Object.keys(change.labels);
labels = labels.concat(currentLabels.filter(nonExistingLabel));
}
}
return labels.sort();
},
_computeLabelShortcut: function(labelName) {
return labelName.replace(/[a-z-]/g, '');
},
_changesChanged: function(changes) {
this.groups = changes ? [changes] : [];
},
_groupTitle: function(groupIndex) {
if (groupIndex > this.groupTitles.length - 1) { return null; }
return this.groupTitles[groupIndex];
},
_computeItemSelected: function(index, groupIndex, selectedIndex) {
var idx = 0;
for (var i = 0; i < groupIndex; i++) {
idx += this.groups[i].length;
}
idx += index;
return idx == selectedIndex;
},
_computeItemNeedsReview: function(account, change, showReviewedState) {
return showReviewedState && !change.reviewed &&
change.status != this.ChangeStatus.MERGED &&
account._account_id != change.owner._account_id;
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
if (this.groups == null) { return; }
var len = 0;
this.groups.forEach(function(group) {
len += group.length;
});
switch (e.keyCode) {
case 74: // 'j'
e.preventDefault();
if (this.selectedIndex == len - 1) { return; }
this.selectedIndex += 1;
break;
case 75: // 'k'
e.preventDefault();
if (this.selectedIndex == 0) { return; }
this.selectedIndex -= 1;
break;
case 79: // 'o'
case 13: // 'enter'
e.preventDefault();
page.show(this._changeURLForIndex(this.selectedIndex));
break;
}
},
_changeURLForIndex: function(index) {
var changeEls = this._getListItems();
if (index < changeEls.length && changeEls[index]) {
return changeEls[index].changeURL;
}
return '';
},
_getListItems: function() {
return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
},
});
})();

View File

@@ -18,14 +18,14 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-change-list.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-list.html">
<test-fixture id="basic">
<template>

View File

@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<dom-module id="gr-dashboard-view">
<template>
@@ -60,70 +60,5 @@ limitations under the License.
group-titles="[[_groupTitles]]"></gr-change-list>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-dashboard-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
account: {
type: Object,
value: function() { return {}; },
},
viewState: Object,
_results: Array,
_groupTitles: {
type: Array,
value: [
'Outgoing reviews',
'Incoming reviews',
'Recently closed',
],
},
/**
* For showing a "loading..." string during ajax requests.
*/
_loading: {
type: Boolean,
value: true,
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
attached: function() {
this.fire('title-change', {title: 'My Reviews'});
},
_computeQueryParams: function() {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.DETAILED_ACCOUNTS,
this.ListChangesOption.REVIEWED
);
return {
O: options,
q: [
'is:open owner:self',
'is:open reviewer:self -owner:self',
'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
],
};
},
});
})();
</script>
<script src="gr-dashboard-view.js"></script>
</dom-module>

View File

@@ -0,0 +1,76 @@
// 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-dashboard-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
account: {
type: Object,
value: function() { return {}; },
},
viewState: Object,
_results: Array,
_groupTitles: {
type: Array,
value: [
'Outgoing reviews',
'Incoming reviews',
'Recently closed',
],
},
/**
* For showing a "loading..." string during ajax requests.
*/
_loading: {
type: Boolean,
value: true,
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
attached: function() {
this.fire('title-change', {title: 'My Reviews'});
},
_computeQueryParams: function() {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.DETAILED_ACCOUNTS,
this.ListChangesOption.REVIEWED
);
return {
O: options,
q: [
'is:open owner:self',
'is:open reviewer:self -owner:self',
'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
],
};
},
});
})();

View File

@@ -0,0 +1,84 @@
<!--
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="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
<dom-module id="gr-change-actions">
<template>
<style>
:host {
display: block;
}
gr-button {
display: block;
margin-bottom: .5em;
}
gr-button:before {
content: attr(data-label);
}
gr-button[loading]:before {
content: attr(data-loading-label);
}
@media screen and (max-width: 50em) {
.confirmDialog {
width: 90vw;
}
}
</style>
<gr-ajax id="actionsXHR"
url="[[_computeRevisionActionsPath(changeNum, patchNum)]]"
last-response="{{_revisionActions}}"
loading="{{_loading}}"></gr-ajax>
<div>
<template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
hidden$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
on-tap="_handleActionTap"></gr-button>
</template>
<template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
disabled$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
on-tap="_handleActionTap"></gr-button>
</template>
</div>
<gr-overlay id="overlay" with-backdrop>
<gr-confirm-rebase-dialog id="confirmRebase"
class="confirmDialog"
on-confirm="_handleRebaseConfirm"
on-cancel="_handleConfirmDialogCancel"
hidden></gr-confirm-rebase-dialog>
</gr-overlay>
</template>
<script src="gr-change-actions.js"></script>
</dom-module>

View File

@@ -0,0 +1,225 @@
// 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';
// TODO(davido): Add the rest of the change actions.
var ChangeActions = {
ABANDON: 'abandon',
DELETE: '/',
RESTORE: 'restore',
};
// TODO(andybons): Add the rest of the revision actions.
var RevisionActions = {
DELETE: '/',
PUBLISH: 'publish',
REBASE: 'rebase',
SUBMIT: 'submit',
};
Polymer({
is: 'gr-change-actions',
/**
* Fired when the change should be reloaded.
*
* @event reload-change
*/
properties: {
actions: {
type: Object,
},
changeNum: String,
patchNum: String,
_loading: {
type: Boolean,
value: true,
},
_revisionActions: Object,
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_actionsChanged(actions, _revisionActions)',
],
reload: function() {
if (!this.changeNum || !this.patchNum) {
return Promise.resolve();
}
return this.$.actionsXHR.generateRequest().completes;
},
_actionsChanged: function(actions, revisionActions) {
this.hidden =
revisionActions.rebase == null &&
revisionActions.submit == null &&
revisionActions.publish == null &&
actions.abandon == null &&
actions.restore == null;
},
_computeRevisionActionsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/actions';
},
_getValuesFor: function(obj) {
return Object.keys(obj).map(function(key) {
return obj[key];
});
},
_computeActionValues: function(actions, type) {
var result = [];
var values = this._getValuesFor(
type == 'change' ? ChangeActions : RevisionActions);
for (var a in actions) {
if (values.indexOf(a) == -1) { continue; }
actions[a].__key = a;
actions[a].__type = type;
result.push(actions[a]);
}
return result;
},
_computeLoadingLabel: function(action) {
return {
'rebase': 'Rebasing...',
'submit': 'Submitting...',
}[action];
},
_computePrimary: function(actionKey) {
return actionKey == 'submit';
},
_computeButtonClass: function(action) {
if ([RevisionActions.SUBMIT,
RevisionActions.PUBLISH].indexOf(action) != -1) {
return 'primary';
}
return '';
},
_handleActionTap: function(e) {
e.preventDefault();
var el = Polymer.dom(e).rootTarget;
var key = el.getAttribute('data-action-key');
var type = el.getAttribute('data-action-type');
if (type == 'revision') {
if (key == RevisionActions.REBASE) {
this._showRebaseDialog();
return;
}
this._fireRevisionAction(this._prependSlash(key),
this._revisionActions[key]);
} else {
this._fireChangeAction(this._prependSlash(key), this.actions[key]);
}
},
_prependSlash: function(key) {
return key == '/' ? key : '/' + key;
},
_handleConfirmDialogCancel: function() {
var dialogEls =
Polymer.dom(this.root).querySelectorAll('.confirmDialog');
for (var i = 0; i < dialogEls.length; i++) {
dialogEls[i].hidden = true;
}
this.$.overlay.close();
},
_handleRebaseConfirm: function() {
var payload = {};
var el = this.$.confirmRebase;
if (el.clearParent) {
// There is a subtle but important difference between setting the base
// to an empty string and omitting it entirely from the payload. An
// empty string implies that the parent should be cleared and the
// change should be rebased on top of the target branch. Leaving out
// the base implies that it should be rebased on top of its current
// parent.
payload.base = '';
} else if (el.base && el.base.length > 0) {
payload.base = el.base;
}
this.$.overlay.close();
el.hidden = false;
this._fireRevisionAction('/rebase', this._revisionActions.rebase,
payload);
},
_fireChangeAction: function(endpoint, action) {
this._send(action.method, {}, endpoint).then(
function() {
// We cant reload a change that was deleted.
if (endpoint == ChangeActions.DELETE) {
page.show('/');
} else {
this.fire('reload-change', null, {bubbles: false});
}
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
},
_fireRevisionAction: function(endpoint, action, opt_payload) {
var buttonEl = this.$$('[data-action-key="' + action.__key + '"]');
buttonEl.setAttribute('loading', true);
buttonEl.disabled = true;
function enableButton() {
buttonEl.removeAttribute('loading');
buttonEl.disabled = false;
}
this._send(action.method, opt_payload, endpoint, true).then(
function() {
this.fire('reload-change', null, {bubbles: false});
enableButton();
}.bind(this)).catch(function(err) {
// TODO(andybons): Handle merge conflict (409 status);
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
enableButton();
throw err;
});
},
_showRebaseDialog: function() {
this.$.confirmRebase.hidden = false;
this.$.overlay.open();
},
_send: function(method, payload, actionEndpoint, revisionAction) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: this.changeBaseURL(this.changeNum,
revisionAction ? this.patchNum : null) + actionEndpoint,
body: payload,
});
return this._xhrPromise;
},
});
})();

View File

@@ -18,12 +18,12 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-change-actions.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-actions.html">
<test-fixture id="basic">
<template>

View File

@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-account-link.html">
<link rel="import" href="gr-date-formatter.html">
<link rel="import" href="gr-reviewer-list.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
<script src="../scripts/fake-app.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<dom-module id="gr-change-metadata">
<template>
@@ -123,69 +123,5 @@ limitations under the License.
</section>
</template>
</template>
<script>
(function() {
'use strict';
var SubmitTypeLabel = {
FAST_FORWARD_ONLY: 'Fast Forward Only',
MERGE_IF_NECESSARY: 'Merge if Necessary',
REBASE_IF_NECESSARY: 'Rebase if Necessary',
MERGE_ALWAYS: 'Always Merge',
CHERRY_PICK: 'Cherry Pick',
};
Polymer({
is: 'gr-change-metadata',
properties: {
change: Object,
mutable: Boolean,
},
behaviors: [
Gerrit.RESTClientBehavior,
],
_computeHideStrategy: function(change) {
var open = change.status == this.ChangeStatus.NEW ||
change.status == this.ChangeStatus.DRAFT;
return !open;
},
_computeStrategy: function(change) {
return SubmitTypeLabel[change.submit_type];
},
_computeLabelNames: function(labels) {
return Object.keys(labels).sort();
},
_computeLabelValues: function(labelName, labels) {
var result = [];
var t = labels[labelName];
if (!t) { return result; }
var approvals = t.all || [];
approvals.forEach(function(label) {
if (label.value && label.value != labels[labelName].default_value) {
var labelClassName;
var labelValPrefix = '';
if (label.value > 0) {
labelValPrefix = '+';
labelClassName = 'approved';
} else if (label.value < 0) {
labelClassName = 'notApproved';
}
result.push({
value: labelValPrefix + label.value,
className: labelClassName,
account: label,
});
}
});
return result;
},
});
})();
</script>
<script src="gr-change-metadata.js"></script>
</dom-module>

View File

@@ -0,0 +1,76 @@
// 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 SubmitTypeLabel = {
FAST_FORWARD_ONLY: 'Fast Forward Only',
MERGE_IF_NECESSARY: 'Merge if Necessary',
REBASE_IF_NECESSARY: 'Rebase if Necessary',
MERGE_ALWAYS: 'Always Merge',
CHERRY_PICK: 'Cherry Pick',
};
Polymer({
is: 'gr-change-metadata',
properties: {
change: Object,
mutable: Boolean,
},
behaviors: [
Gerrit.RESTClientBehavior,
],
_computeHideStrategy: function(change) {
var open = change.status == this.ChangeStatus.NEW ||
change.status == this.ChangeStatus.DRAFT;
return !open;
},
_computeStrategy: function(change) {
return SubmitTypeLabel[change.submit_type];
},
_computeLabelNames: function(labels) {
return Object.keys(labels).sort();
},
_computeLabelValues: function(labelName, labels) {
var result = [];
var t = labels[labelName];
if (!t) { return result; }
var approvals = t.all || [];
approvals.forEach(function(label) {
if (label.value && label.value != labels[labelName].default_value) {
var labelClassName;
var labelValPrefix = '';
if (label.value > 0) {
labelValPrefix = '+';
labelClassName = 'approved';
} else if (label.value < 0) {
labelClassName = 'notApproved';
}
result.push({
value: labelValPrefix + label.value,
className: labelClassName,
account: label,
});
}
});
return result;
},
});
})();

View File

@@ -18,13 +18,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-change-metadata.html">
<script src="../scripts/util.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-metadata.html">
<script src="../../../scripts/util.js"></script>
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,319 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
<link rel="import" href="../gr-change-actions/gr-change-actions.html">
<link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
<link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
<link rel="import" href="../gr-file-list/gr-file-list.html">
<link rel="import" href="../gr-messages-list/gr-messages-list.html">
<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html">
<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html">
<dom-module id="gr-change-view">
<template>
<style>
.container {
margin: 1em var(--default-horizontal-margin);
}
.container:not(.loading) {
background-color: var(--view-background-color);
}
.container.loading {
color: #666;
}
.headerContainer {
height: 4.1em;
margin-bottom: .5em;
}
.header {
align-items: center;
background-color: var(--view-background-color);
border-bottom: 1px solid #ddd;
display: flex;
padding: 1em var(--default-horizontal-margin);
z-index: 99; /* Less than gr-overlay's backdrop */
}
.header.pinned {
border-bottom-color: transparent;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
position: fixed;
top: 0;
transition: box-shadow 250ms linear;
width: calc(100% - (2 * var(--default-horizontal-margin)));
}
.header-title {
flex: 1;
font-size: 1.2em;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
gr-change-star {
margin-right: .25em;
vertical-align: -.425em;
}
.download,
.patchSelectLabel {
margin-left: var(--default-horizontal-margin);
}
.header select {
margin-left: .5em;
}
.header .reply {
margin-left: var(--default-horizontal-margin);
}
gr-reply-dialog {
min-width: 30em;
max-width: 50em;
}
.changeStatus {
color: #999;
text-transform: capitalize;
}
section {
margin: 10px 0;
padding: 10px var(--default-horizontal-margin);
}
/* Strong specificity here is needed due to
https://github.com/Polymer/polymer/issues/2531 */
.container section.changeInfo {
border-bottom: 1px solid #ddd;
display: flex;
margin-top: 0;
padding-top: 0;
}
.changeInfo-column:not(:last-of-type) {
margin-right: 1em;
padding-right: 1em;
}
.changeMetadata {
border-right: 1px solid #ddd;
font-size: .9em;
}
gr-change-actions {
margin-top: 1em;
}
.commitMessage {
font-family: var(--monospace-font-family);
flex: 0 0 72ch;
margin-right: 2em;
margin-bottom: 1em;
}
.commitMessage h4 {
font-family: var(--font-family);
font-weight: bold;
margin-bottom: .25em;
}
.commitAndRelated {
align-content: flex-start;
display: flex;
flex: 1;
flex-wrap: wrap;
}
gr-file-list {
margin-bottom: 1em;
padding: 0 var(--default-horizontal-margin);
}
@media screen and (max-width: 50em) {
.container {
margin: .5em 0 !important;
}
.container.loading {
margin: 1em var(--default-horizontal-margin) !important;
}
.headerContainer {
height: 5.15em;
}
.header {
align-items: flex-start;
flex-direction: column;
padding: .5em var(--default-horizontal-margin) !important;
}
gr-change-star {
vertical-align: middle;
}
.header-title,
.header-actions,
.header.pinned {
width: 100% !important;
}
.header-title {
font-size: 1.1em;
}
.header-actions {
align-items: center;
display: flex;
justify-content: space-between;
margin-top: .5em;
}
gr-reply-dialog {
min-width: initial;
width: 90vw;
}
.download {
display: none;
}
.patchSelectLabel {
margin-left: 0 !important;
margin-right: .5em;
}
.header select {
margin-left: 0 !important;
margin-right: .5em;
}
.header .reply {
margin-left: 0 !important;
margin-right: .5em;
}
.changeInfo-column:not(:last-of-type) {
margin-right: 0;
padding-right: 0;
}
.changeInfo,
.commitAndRelated {
flex-direction: column;
flex-wrap: nowrap;
}
.changeMetadata {
font-size: 1em;
border-right: none;
margin-bottom: 1em;
margin-top: .25em;
max-width: none;
}
.commitMessage {
flex: initial;
margin-right: 0;
}
}
</style>
<gr-ajax id="detailXHR"
url="[[_computeDetailPath(_changeNum)]]"
params="[[_computeDetailQueryParams()]]"
last-response="{{_change}}"
loading="{{_loading}}"></gr-ajax>
<gr-ajax id="commentsXHR"
url="[[_computeCommentsPath(_changeNum)]]"
last-response="{{_comments}}"></gr-ajax>
<gr-ajax id="commitInfoXHR"
url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]"
last-response="{{_commitInfo}}"></gr-ajax>
<!-- TODO(andybons): Cache the project config. -->
<gr-ajax id="configXHR"
auto
url="[[_computeProjectConfigPath(_change.project)]]"
last-response="{{_projectConfig}}"></gr-ajax>
<div class="container loading" hidden$="{{!_loading}}">Loading...</div>
<div class="container" hidden$="{{_loading}}">
<div class="headerContainer">
<div class="header">
<span class="header-title">
<gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
<a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
<span>[[_change.subject]]</span>
<span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
</span>
<span class="header-actions">
<gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button>
<gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button>
<span>
<label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
<select id="patchSetSelect" on-change="_handlePatchChange">
<template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
<option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
<span>[[patchNumber]]</span>
/
<span>[[_computeLatestPatchNum(_change)]]</span>
</option>
</template>
</select>
</span>
</span>
</div>
</div>
<section class="changeInfo">
<div class="changeInfo-column changeMetadata">
<gr-change-metadata
change="[[_change]]"
mutable="[[_loggedIn]]"></gr-change-metadata>
<gr-change-actions id="actions"
actions="[[_change.actions]]"
change-num="[[_changeNum]]"
patch-num="[[_patchNum]]"
on-reload-change="_handleReloadChange"></gr-change-actions>
</div>
<div class="changeInfo-column commitAndRelated">
<div class="commitMessage">
<h4>Commit message</h4>
<gr-linked-text pre
content="[[_commitInfo.message]]"
config="[[_projectConfig.commentlinks]]"></gr-linked-text>
</div>
<div class="relatedChanges">
<gr-related-changes-list id="relatedChanges"
change="[[_change]]"
server-config="[[serverConfig]]"
patch-num="[[_patchNum]]"></gr-related-changes-list>
</div>
</div>
</section>
<gr-file-list id="fileList"
change-num="[[_changeNum]]"
patch-num="[[_patchNum]]"
comments="[[_comments]]"
selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
<gr-messages-list id="messageList"
change-num="[[_changeNum]]"
messages="[[_change.messages]]"
comments="[[_comments]]"
project-config="[[_projectConfig]]"
show-reply-buttons="[[_loggedIn]]"
on-reply="_handleMessageReply"></gr-messages-list>
</div>
<gr-overlay id="downloadOverlay" with-backdrop>
<gr-download-dialog
change="[[_change]]"
patch-num="[[_patchNum]]"
config="[[serverConfig.download]]"
on-close="_handleDownloadDialogClose"></gr-download-dialog>
</gr-overlay>
<gr-overlay id="replyOverlay"
on-iron-overlay-opened="_handleReplyOverlayOpen"
with-backdrop>
<gr-reply-dialog id="replyDialog"
change-num="[[_changeNum]]"
patch-num="[[_patchNum]]"
labels="[[_change.labels]]"
permitted-labels="[[_change.permitted_labels]]"
on-send="_handleReplySent"
on-cancel="_handleReplyCancel"
hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
</gr-overlay>
</template>
<script src="gr-change-view.js"></script>
</dom-module>

View File

@@ -0,0 +1,354 @@
// 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-change-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
viewState: {
type: Object,
notify: true,
value: function() { return {}; },
},
serverConfig: Object,
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_comments: Object,
_change: {
type: Object,
observer: '_changeChanged',
},
_commitInfo: Object,
_changeNum: String,
_patchNum: String,
_allPatchSets: {
type: Array,
computed: '_computeAllPatchSets(_change)',
},
_loggedIn: {
type: Boolean,
value: false,
},
_loading: Boolean,
_headerContainerEl: Object,
_headerEl: Object,
_projectConfig: Object,
_boundScrollHandler: {
type: Function,
value: function() { return this._handleBodyScroll.bind(this); },
},
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
}.bind(this));
this._headerEl = this.$$('.header');
},
attached: function() {
window.addEventListener('scroll', this._boundScrollHandler);
},
detached: function() {
window.removeEventListener('scroll', this._boundScrollHandler);
},
_handleBodyScroll: function(e) {
var containerEl = this._headerContainerEl ||
this.$$('.headerContainer');
// Calculate where the header is relative to the window.
var top = containerEl.offsetTop;
for (var offsetParent = containerEl.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
// The element may not be displayed yet, in which case do nothing.
if (top == 0) { return; }
this._headerEl.classList.toggle('pinned', window.scrollY >= top);
},
_resetHeaderEl: function() {
var el = this._headerEl || this.$$('.header');
this._headerEl = el;
el.classList.remove('pinned');
},
_handlePatchChange: function(e) {
var patchNum = e.target.value;
var currentPatchNum =
this._change.revisions[this._change.current_revision]._number;
if (patchNum == currentPatchNum) {
page.show(this._computeChangePath(this._changeNum));
return;
}
page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
},
_handleReplyTap: function(e) {
e.preventDefault();
this.$.replyOverlay.open();
},
_handleDownloadTap: function(e) {
e.preventDefault();
this.$.downloadOverlay.open();
},
_handleDownloadDialogClose: function(e) {
this.$.downloadOverlay.close();
},
_handleMessageReply: function(e) {
var msg = e.detail.message.message;
var quoteStr = msg.split('\n').map(
function(line) { return '> ' + line; }).join('\n') + '\n\n';
this.$.replyDialog.draft += quoteStr;
this.$.replyOverlay.open();
},
_handleReplyOverlayOpen: function(e) {
this.$.replyDialog.reload().then(function() {
this.async(function() { this.$.replyOverlay.center() }, 1);
}.bind(this));
this.$.replyDialog.focus();
},
_handleReplySent: function(e) {
this.$.replyOverlay.close();
this._reload();
},
_handleReplyCancel: function(e) {
this.$.replyOverlay.close();
},
_paramsChanged: function(value) {
if (value.view != this.tagName.toLowerCase()) { return; }
this._changeNum = value.changeNum;
this._patchNum = value.patchNum;
if (this.viewState.changeNum != this._changeNum ||
this.viewState.patchNum != this._patchNum) {
this.set('viewState.selectedFileIndex', 0);
this.set('viewState.changeNum', this._changeNum);
this.set('viewState.patchNum', this._patchNum);
}
if (!this._changeNum) {
return;
}
this._reload().then(function() {
this.$.messageList.topMargin = this._headerEl.offsetHeight;
// Allow the message list to render before scrolling.
this.async(function() {
var msgPrefix = '#message-';
var hash = window.location.hash;
if (hash.indexOf(msgPrefix) == 0) {
this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
}
}.bind(this), 1);
app.accountReady.then(function() {
if (!this._loggedIn) { return; }
if (this.viewState.showReplyDialog) {
this.$.replyOverlay.open();
this.set('viewState.showReplyDialog', false);
}
}.bind(this));
}.bind(this));
},
_changeChanged: function(change) {
if (!change) { return; }
this._patchNum = this._patchNum ||
change.revisions[change.current_revision]._number;
var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
this.fire('title-change', {title: title});
},
_computeChangePath: function(changeNum) {
return '/c/' + changeNum;
},
_computeChangePermalink: function(changeNum) {
return '/' + changeNum;
},
_computeChangeStatus: function(change, patchNum) {
var status = change.status;
if (status == this.ChangeStatus.NEW) {
var rev = this._getRevisionNumber(change, patchNum);
// TODO(davido): Figure out, why sometimes revision is not there
if (rev == undefined || !rev.draft) { return ''; }
status = this.ChangeStatus.DRAFT;
}
return '(' + status.toLowerCase() + ')';
},
_computeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeCommitInfoPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/commit?links';
},
_computeCommentsPath: function(changeNum) {
return '/changes/' + changeNum + '/comments';
},
_computeProjectConfigPath: function(project) {
return '/projects/' + encodeURIComponent(project) + '/config';
},
_computeDetailQueryParams: function() {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.ALL_REVISIONS,
this.ListChangesOption.CHANGE_ACTIONS,
this.ListChangesOption.DOWNLOAD_COMMANDS
);
return {O: options};
},
_computeLatestPatchNum: function(change) {
return change.revisions[change.current_revision]._number;
},
_computeAllPatchSets: function(change) {
var patchNums = [];
for (var rev in change.revisions) {
patchNums.push(change.revisions[rev]._number);
}
return patchNums.sort(function(a, b) {
return a - b;
});
},
_getRevisionNumber: function(change, patchNum) {
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
return change.revisions[rev];
}
}
},
_computePatchIndexIsSelected: function(index, patchNum) {
return this._allPatchSets[index] == patchNum;
},
_computeLabelNames: function(labels) {
return Object.keys(labels).sort();
},
_computeLabelValues: function(labelName, labels) {
var result = [];
var t = labels[labelName];
if (!t) { return result; }
var approvals = t.all || [];
approvals.forEach(function(label) {
if (label.value && label.value != labels[labelName].default_value) {
var labelClassName;
var labelValPrefix = '';
if (label.value > 0) {
labelValPrefix = '+';
labelClassName = 'approved';
} else if (label.value < 0) {
labelClassName = 'notApproved';
}
result.push({
value: labelValPrefix + label.value,
className: labelClassName,
account: label,
});
}
});
return result;
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 65: // 'a'
e.preventDefault();
this.$.replyOverlay.open();
break;
case 85: // 'u'
e.preventDefault();
page.show('/');
break;
}
},
_handleReloadChange: function() {
page.show(this._computeChangePath(this._changeNum));
},
_reload: function() {
var detailCompletes = this.$.detailXHR.generateRequest().completes;
this.$.commentsXHR.generateRequest();
var reloadPatchNumDependentResources = function() {
return Promise.all([
this.$.commitInfoXHR.generateRequest().completes,
this.$.actions.reload(),
this.$.fileList.reload(),
]);
}.bind(this);
var reloadDetailDependentResources = function() {
return this.$.relatedChanges.reload();
}.bind(this);
this._resetHeaderEl();
if (this._patchNum) {
return reloadPatchNumDependentResources().then(function() {
return detailCompletes;
}).then(reloadDetailDependentResources);
} else {
// The patch number is reliant on the change detail request.
return detailCompletes.then(reloadPatchNumDependentResources).then(
reloadDetailDependentResources);
}
},
});
})();

View File

@@ -18,14 +18,14 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-view</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-change-view.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-view.html">
<test-fixture id="basic">
<template>

View File

@@ -14,7 +14,7 @@ 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/polymer/polymer.html">
<dom-module id="gr-comment-list">
<template>
@@ -64,55 +64,5 @@ limitations under the License.
</template>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-comment-list',
properties: {
changeNum: Number,
comments: {
type: Object,
observer: '_commentsChanged',
},
patchNum: Number,
_files: Array,
},
_commentsChanged: function(value) {
this._files = Object.keys(value || {}).sort();
},
_computeFileDiffURL: function(file, changeNum, patchNum) {
return '/c/' + changeNum + '/' + patchNum + '/' + file;
},
_computeDiffLineURL: function(file, changeNum, patchNum, comment) {
var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
if (comment.line) {
// TODO(andybons): This is not correct if the comment is on the base.
diffURL += '#' + comment.line;
}
return diffURL;
},
_computeCommentsForFile: function(file) {
return this.comments[file];
},
_computePatchDisplayName: function(comment) {
if (comment.side == 'PARENT') {
return 'Base, ';
}
if (comment.patch_set != this.patchNum) {
return 'PS' + comment.patch_set + ', ';
}
return '';
}
});
})();
</script>
<script src="gr-comment-list.js"></script>
</dom-module>

View File

@@ -0,0 +1,62 @@
// 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-comment-list',
properties: {
changeNum: Number,
comments: {
type: Object,
observer: '_commentsChanged',
},
patchNum: Number,
_files: Array,
},
_commentsChanged: function(value) {
this._files = Object.keys(value || {}).sort();
},
_computeFileDiffURL: function(file, changeNum, patchNum) {
return '/c/' + changeNum + '/' + patchNum + '/' + file;
},
_computeDiffLineURL: function(file, changeNum, patchNum, comment) {
var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
if (comment.line) {
// TODO(andybons): This is not correct if the comment is on the base.
diffURL += '#' + comment.line;
}
return diffURL;
},
_computeCommentsForFile: function(file) {
return this.comments[file];
},
_computePatchDisplayName: function(comment) {
if (comment.side == 'PARENT') {
return 'Base, ';
}
if (comment.patch_set != this.patchNum) {
return 'PS' + comment.patch_set + ', ';
}
return '';
}
});
})();

View File

@@ -14,8 +14,8 @@ 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="gr-confirm-dialog.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
<dom-module id="gr-confirm-rebase-dialog">
<template>
@@ -71,49 +71,5 @@ limitations under the License.
</div>
</gr-confirm-dialog>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-confirm-rebase-dialog',
/**
* Fired when the confirm button is pressed.
*
* @event confirm
*/
/**
* Fired when the cancel button is pressed.
*
* @event cancel
*/
properties: {
base: String,
clearParent: Boolean,
},
_handleConfirmTap: function(e) {
e.preventDefault();
this.fire('confirm', null, {bubbles: false});
},
_handleCancelTap: function(e) {
e.preventDefault();
this.fire('cancel', null, {bubbles: false});
},
_handleClearParentTap: function(e) {
var clear = Polymer.dom(e).rootTarget.checked;
if (clear) {
this.base = '';
}
this.$.parentInput.disabled = clear;
this.clearParent = clear;
},
});
})();
</script>
<script src="gr-confirm-rebase-dialog.js"></script>
</dom-module>

View File

@@ -0,0 +1,56 @@
// 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-confirm-rebase-dialog',
/**
* Fired when the confirm button is pressed.
*
* @event confirm
*/
/**
* Fired when the cancel button is pressed.
*
* @event cancel
*/
properties: {
base: String,
clearParent: Boolean,
},
_handleConfirmTap: function(e) {
e.preventDefault();
this.fire('confirm', null, {bubbles: false});
},
_handleCancelTap: function(e) {
e.preventDefault();
this.fire('cancel', null, {bubbles: false});
},
_handleClearParentTap: function(e) {
var clear = Polymer.dom(e).rootTarget.checked;
if (clear) {
this.base = '';
}
this.$.parentInput.disabled = clear;
this.clearParent = clear;
},
});
})();

View File

@@ -18,11 +18,11 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-rebase-dialog</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<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="../elements/gr-confirm-rebase-dialog.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-confirm-rebase-dialog.html">
<test-fixture id="basic">
<template>

View File

@@ -14,10 +14,10 @@ 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="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<dom-module id="gr-download-dialog">
<template>
@@ -140,130 +140,5 @@ limitations under the License.
</div>
</footer>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-download-dialog',
/**
* Fired when the user presses the close button.
*
* @event close
*/
properties: {
change: Object,
patchNum: String,
config: Object,
_schemes: {
type: Array,
value: function() { return []; },
computed: '_computeSchemes(change, patchNum)',
observer: '_schemesChanged',
},
_selectedScheme: String,
},
hostAttributes: {
role: 'dialog',
},
behaviors: [
Gerrit.RESTClientBehavior,
],
_computeDownloadCommands: function(change, patchNum, _selectedScheme) {
var commandObj;
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
commandObj = change.revisions[rev].fetch[_selectedScheme].commands;
break;
}
}
var commands = [];
for (var title in commandObj) {
commands.push({
title: title,
command: commandObj[title],
});
}
return commands;
},
_computeZipDownloadLink: function(change, patchNum) {
return this._computeDownloadLink(change, patchNum, true);
},
_computeZipDownloadFilename: function(change, patchNum) {
return this._computeDownloadFilename(change, patchNum, true);
},
_computeDownloadLink: function(change, patchNum, zip) {
return this.changeBaseURL(change._number, patchNum) + '/patch?' +
(zip ? 'zip' : 'download');
},
_computeDownloadFilename: function(change, patchNum, zip) {
var shortRev;
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
shortRev = rev.substr(0, 7);
break;
}
}
return shortRev + '.diff.' + (zip ? 'zip' : 'base64');
},
_computeArchiveDownloadLink: function(change, patchNum, format) {
return this.changeBaseURL(change._number, patchNum) +
'/archive?format=' + format;
},
_computeSchemes: function(change, patchNum) {
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
var fetch = change.revisions[rev].fetch;
if (fetch) {
return Object.keys(fetch).sort();
}
break;
}
}
return [];
},
_computeSchemeSelected: function(scheme, selectedScheme) {
return scheme == selectedScheme;
},
_handleSchemeTap: function(e) {
e.preventDefault();
var el = Polymer.dom(e).rootTarget;
// TODO(andybons): Save as default scheme in preferences.
this._selectedScheme = el.getAttribute('data-scheme');
},
_handleInputTap: function(e) {
e.preventDefault();
Polymer.dom(e).rootTarget.select();
},
_handleCloseTap: function(e) {
e.preventDefault();
this.fire('close', null, {bubbles: false});
},
_schemesChanged: function(schemes) {
if (schemes.length == 0) { return; }
if (schemes.indexOf(this._selectedScheme) == -1) {
this._selectedScheme = schemes.sort()[0];
}
},
});
})();
</script>
<script src="gr-download-dialog.js"></script>
</dom-module>

View File

@@ -0,0 +1,136 @@
// 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-download-dialog',
/**
* Fired when the user presses the close button.
*
* @event close
*/
properties: {
change: Object,
patchNum: String,
config: Object,
_schemes: {
type: Array,
value: function() { return []; },
computed: '_computeSchemes(change, patchNum)',
observer: '_schemesChanged',
},
_selectedScheme: String,
},
hostAttributes: {
role: 'dialog',
},
behaviors: [
Gerrit.RESTClientBehavior,
],
_computeDownloadCommands: function(change, patchNum, _selectedScheme) {
var commandObj;
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
commandObj = change.revisions[rev].fetch[_selectedScheme].commands;
break;
}
}
var commands = [];
for (var title in commandObj) {
commands.push({
title: title,
command: commandObj[title],
});
}
return commands;
},
_computeZipDownloadLink: function(change, patchNum) {
return this._computeDownloadLink(change, patchNum, true);
},
_computeZipDownloadFilename: function(change, patchNum) {
return this._computeDownloadFilename(change, patchNum, true);
},
_computeDownloadLink: function(change, patchNum, zip) {
return this.changeBaseURL(change._number, patchNum) + '/patch?' +
(zip ? 'zip' : 'download');
},
_computeDownloadFilename: function(change, patchNum, zip) {
var shortRev;
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
shortRev = rev.substr(0, 7);
break;
}
}
return shortRev + '.diff.' + (zip ? 'zip' : 'base64');
},
_computeArchiveDownloadLink: function(change, patchNum, format) {
return this.changeBaseURL(change._number, patchNum) +
'/archive?format=' + format;
},
_computeSchemes: function(change, patchNum) {
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
var fetch = change.revisions[rev].fetch;
if (fetch) {
return Object.keys(fetch).sort();
}
break;
}
}
return [];
},
_computeSchemeSelected: function(scheme, selectedScheme) {
return scheme == selectedScheme;
},
_handleSchemeTap: function(e) {
e.preventDefault();
var el = Polymer.dom(e).rootTarget;
// TODO(andybons): Save as default scheme in preferences.
this._selectedScheme = el.getAttribute('data-scheme');
},
_handleInputTap: function(e) {
e.preventDefault();
Polymer.dom(e).rootTarget.select();
},
_handleCloseTap: function(e) {
e.preventDefault();
this.fire('close', null, {bubbles: false});
},
_schemesChanged: function(schemes) {
if (schemes.length == 0) { return; }
if (schemes.indexOf(this._selectedScheme) == -1) {
this._selectedScheme = schemes.sort()[0];
}
},
});
})();

View File

@@ -18,11 +18,11 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-download-dialog</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.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="../elements/gr-download-dialog.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-download-dialog.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,159 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<dom-module id="gr-file-list">
<template>
<style>
:host {
display: block;
}
.row {
display: flex;
padding: .1em .25em;
}
.header {
font-weight: bold;
}
.positionIndicator,
.reviewed,
.status {
align-items: center;
display: inline-flex;
}
.reviewed,
.status {
justify-content: center;
width: 1.5em;
}
.positionIndicator {
justify-content: flex-start;
visibility: hidden;
width: 1.25em;
}
.row[selected] {
background-color: #ebf5fb;
}
.row[selected] .positionIndicator {
visibility: visible;
}
.path {
flex: 1;
overflow: hidden;
padding-left: .35em;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.row:not(.header) .path:hover {
text-decoration: underline;
}
.comments,
.stats {
text-align: right;
}
.comments {
min-width: 10em;
}
.stats {
min-width: 7em;
}
.invisible {
visibility: hidden;
}
.row:not(.header) .stats {
font-family: var(--monospace-font-family);
}
.added {
color: #388E3C;
}
.removed {
color: #D32F2F;
}
.reviewed input[type="checkbox"] {
display: inline-block;
}
.drafts {
color: #C62828;
font-weight: bold;
}
@media screen and (max-width: 50em) {
.row[selected] {
background-color: transparent;
}
.positionIndicator,
.stats {
display: none;
}
.reviewed,
.status {
justify-content: flex-start;
}
.comments {
min-width: initial;
}
}
</style>
<gr-ajax id="filesXHR"
url="[[_computeFilesURL(changeNum, patchNum)]]"
on-response="_handleResponse"></gr-ajax>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsURL(changeNum, patchNum)]]"
last-response="{{_drafts}}"></gr-ajax>
<gr-ajax id="reviewedXHR"
url="[[_computeReviewedURL(changeNum, patchNum)]]"
last-response="{{_reviewed}}"></gr-ajax>
</gr-ajax>
<div class="row header">
<div class="positionIndicator"></div>
<div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
<div class="status"></div>
<div class="path">Path</div>
<div class="comments">Comments</div>
<div class="stats">Stats</div>
</div>
<template is="dom-repeat" items="{{files}}" as="file">
<div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
<div class="positionIndicator">&#x25b6;</div>
<div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
<input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
data-path$="[[file.__path]]" on-change="_handleReviewedChange">
</div>
<div class$="[[_computeClass('status', file.__path)]]">
[[_computeFileStatus(file.status)]]
</div>
<a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">
[[_computeFileDisplayName(file.__path)]]
</a>
<div class="comments">
<span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
[[_computeCommentsString(comments, patchNum, file.__path)]]
</div>
<div class$="[[_computeClass('stats', file.__path)]]">
<span class="added">+[[file.lines_inserted]]</span>
<span class="removed">-[[file.lines_deleted]]</span>
</div>
</div>
</template>
</template>
<script src="gr-file-list.js"></script>
</dom-module>

View File

@@ -0,0 +1,205 @@
// 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 COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
Polymer({
is: 'gr-file-list',
properties: {
patchNum: String,
changeNum: String,
comments: Object,
files: Array,
selectedIndex: {
type: Number,
notify: true,
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_loggedIn: {
type: Boolean,
value: false,
},
_drafts: Object,
_reviewed: {
type: Array,
value: function() { return []; },
},
_xhrPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
reload: function() {
if (!this.changeNum || !this.patchNum) {
return Promise.resolve();
}
return Promise.all([
this.$.filesXHR.generateRequest().completes,
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
if (!app.loggedIn) { return; }
this.$.draftsXHR.generateRequest();
this.$.reviewedXHR.generateRequest();
}.bind(this)),
]);
},
_computeFilesURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/files';
},
_computeCommentsString: function(comments, patchNum, path) {
var patchComments = (comments[path] || []).filter(function(c) {
return c.patch_set == patchNum;
});
var num = patchComments.length;
if (num == 0) { return ''; }
if (num == 1) { return '1 comment'; }
if (num > 1) { return num + ' comments'; }
},
_computeReviewedURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed';
},
_computeReviewed: function(file, _reviewed) {
return _reviewed.indexOf(file.__path) != -1;
},
_handleReviewedChange: function(e) {
var path = Polymer.dom(e).rootTarget.getAttribute('data-path');
var index = this._reviewed.indexOf(path);
var reviewed = index != -1;
if (reviewed) {
this.splice('_reviewed', index, 1);
} else {
this.push('_reviewed', path);
}
var method = reviewed ? 'DELETE' : 'PUT';
var url = this.changeBaseURL(this.changeNum, this.patchNum) +
'/files/' + encodeURIComponent(path) + '/reviewed';
this._send(method, url).catch(function(err) {
alert('Couldnt change file review status. Check the console ' +
'and contact the PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_computeDraftsURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/drafts';
},
_computeDraftsString: function(drafts, path) {
var num = (drafts[path] || []).length;
if (num == 0) { return ''; }
if (num == 1) { return '1 draft'; }
if (num > 1) { return num + ' drafts'; }
},
_handleResponse: function(e, req) {
var result = e.detail.response;
var paths = Object.keys(result).sort();
var files = [];
for (var i = 0; i < paths.length; i++) {
var info = result[paths[i]];
info.__path = paths[i];
info.lines_inserted = info.lines_inserted || 0;
info.lines_deleted = info.lines_deleted || 0;
files.push(info);
}
this.files = files;
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 74: // 'j'
e.preventDefault();
this.selectedIndex =
Math.min(this.files.length - 1, this.selectedIndex + 1);
break;
case 75: // 'k'
e.preventDefault();
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
break;
case 219: // '['
e.preventDefault();
this._openSelectedFile(this.files.length - 1);
break;
case 221: // ']'
e.preventDefault();
this._openSelectedFile(0);
break;
case 13: // <enter>
case 79: // 'o'
e.preventDefault();
this._openSelectedFile();
break;
}
},
_openSelectedFile: function(opt_index) {
if (opt_index != null) {
this.selectedIndex = opt_index;
}
page.show(this._computeDiffURL(this.changeNum, this.patchNum,
this.files[this.selectedIndex].__path));
},
_computeFileSelected: function(index, selectedIndex) {
return index == selectedIndex;
},
_computeFileStatus: function(status) {
return status || 'M';
},
_computeDiffURL: function(changeNum, patchNum, path) {
return '/c/' + changeNum + '/' + patchNum + '/' + path;
},
_computeFileDisplayName: function(path) {
return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
},
_computeClass: function(baseClass, path) {
var classes = [baseClass];
if (path == COMMIT_MESSAGE_PATH) {
classes.push('invisible');
}
return classes.join(' ');
},
_send: function(method, url) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: url,
});
return this._xhrPromise;
},
});
})();

View File

@@ -18,14 +18,14 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-file-list.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-file-list.html">
<test-fixture id="basic">
<template>

View File

@@ -14,12 +14,13 @@ 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="gr-account-link.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-comment-list.html">
<link rel="import" href="gr-date-formatter.html">
<link rel="import" href="gr-linked-text.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.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-linked-text/gr-linked-text.html">
<link rel="import" href="../gr-comment-list/gr-comment-list.html">
<dom-module id="gr-message">
<template>
@@ -120,104 +121,5 @@ limitations under the License.
</div>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-message',
/**
* Fired when this message's permalink is tapped.
*
* @event scroll-to
*/
/**
* Fired when this message's reply link is tapped.
*
* @event reply
*/
listeners: {
'tap': '_handleTap',
},
properties: {
changeNum: Number,
message: Object,
comments: {
type: Object,
observer: '_commentsChanged',
},
expanded: {
type: Boolean,
value: true,
reflectToAttribute: true,
},
showAvatar: {
type: Boolean,
value: false,
},
showReplyButton: {
type: Boolean,
value: false,
},
projectConfig: Object,
},
ready: function() {
app.configReady.then(function(cfg) {
this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) &&
this.message && this.message.author;
}.bind(this));
},
_commentsChanged: function(value) {
this.expanded = Object.keys(value || {}).length > 0;
},
_handleTap: function(e) {
if (this.expanded) { return; }
this.expanded = true;
},
_handleNameTap: function(e) {
if (!this.expanded) { return; }
e.stopPropagation();
this.expanded = false;
},
_computeClass: function(expanded, showAvatar) {
var classes = [];
classes.push(expanded ? 'expanded' : 'collapsed');
classes.push(showAvatar ? 'showAvatar' : 'hideAvatar');
return classes.join(' ');
},
_computeMessageHash: function(message) {
return '#message-' + message.id;
},
_handleLinkTap: function(e) {
e.preventDefault();
this.fire('scroll-to', {message: this.message}, {bubbles: false});
var hash = this._computeMessageHash(this.message);
// Don't add the hash to the window history if it's already there.
// Otherwise you mess up expected back button behavior.
if (window.location.hash == hash) { return; }
// Change the URL but dont trigger a nav event. Otherwise it will
// reload the page.
page.show(window.location.pathname + hash, null, false);
},
_handleReplyTap: function(e) {
e.preventDefault();
this.fire('reply', {message: this.message});
},
});
})();
</script>
<script src="gr-message.js"></script>
</dom-module>

View File

@@ -0,0 +1,111 @@
// 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-message',
/**
* Fired when this message's permalink is tapped.
*
* @event scroll-to
*/
/**
* Fired when this message's reply link is tapped.
*
* @event reply
*/
listeners: {
'tap': '_handleTap',
},
properties: {
changeNum: Number,
message: Object,
comments: {
type: Object,
observer: '_commentsChanged',
},
expanded: {
type: Boolean,
value: true,
reflectToAttribute: true,
},
showAvatar: {
type: Boolean,
value: false,
},
showReplyButton: {
type: Boolean,
value: false,
},
projectConfig: Object,
},
ready: function() {
app.configReady.then(function(cfg) {
this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) &&
this.message && this.message.author;
}.bind(this));
},
_commentsChanged: function(value) {
this.expanded = Object.keys(value || {}).length > 0;
},
_handleTap: function(e) {
if (this.expanded) { return; }
this.expanded = true;
},
_handleNameTap: function(e) {
if (!this.expanded) { return; }
e.stopPropagation();
this.expanded = false;
},
_computeClass: function(expanded, showAvatar) {
var classes = [];
classes.push(expanded ? 'expanded' : 'collapsed');
classes.push(showAvatar ? 'showAvatar' : 'hideAvatar');
return classes.join(' ');
},
_computeMessageHash: function(message) {
return '#message-' + message.id;
},
_handleLinkTap: function(e) {
e.preventDefault();
this.fire('scroll-to', {message: this.message}, {bubbles: false});
var hash = this._computeMessageHash(this.message);
// Don't add the hash to the window history if it's already there.
// Otherwise you mess up expected back button behavior.
if (window.location.hash == hash) { return; }
// Change the URL but dont trigger a nav event. Otherwise it will
// reload the page.
page.show(window.location.pathname + hash, null, false);
},
_handleReplyTap: function(e) {
e.preventDefault();
this.fire('reply', {message: this.message});
},
});
})();

View File

@@ -18,13 +18,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-message</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-message.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-message.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,62 @@
<!--
Copyright (C) 2015 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-button/gr-button.html">
<link rel="import" href="../gr-message/gr-message.html">
<dom-module id="gr-messages-list">
<template>
<style>
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: .35em;
}
.header,
gr-message {
padding: 0 var(--default-horizontal-margin);
}
.highlighted {
animation: 3s fadeOut;
}
@keyframes fadeOut {
0% { background-color: #fff9c4; }
100% { background-color: #fff; }
}
</style>
<div class="header">
<h3>Messages</h3>
<gr-button link on-tap="_handleExpandCollapseTap">
[[_computeExpandCollapseMessage(_expanded)]]
</gr-button>
</div>
<template is="dom-repeat" items="[[messages]]" as="message">
<gr-message
change-num="[[changeNum]]"
message="[[message]]"
comments="[[_computeCommentsForMessage(comments, message, index)]]"
project-config="[[projectConfig]]"
show-reply-button="[[showReplyButtons]]"
on-scroll-to="_handleScrollTo"
data-message-id$="[[message.id]]"></gr-message>
</template>
</template>
<script src="gr-messages-list.js"></script>
</dom-module>

View File

@@ -0,0 +1,111 @@
// 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-messages-list',
properties: {
changeNum: Number,
messages: {
type: Array,
value: function() { return []; },
},
comments: Object,
projectConfig: Object,
topMargin: Number,
showReplyButtons: {
type: Boolean,
value: false,
},
_expanded: {
type: Boolean,
value: false,
},
},
scrollToMessage: function(messageID) {
var el = this.$$('[data-message-id="' + messageID + '"]');
if (!el) { return; }
el.expanded = true;
var top = el.offsetTop;
for (var offsetParent = el.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
window.scrollTo(0, top - this.topMargin);
this._highlightEl(el);
},
_highlightEl: function(el) {
var highlightedEls =
Polymer.dom(this.root).querySelectorAll('.highlighted');
for (var i = 0; i < highlightedEls.length; i++) {
highlightedEls[i].classList.remove('highlighted');
}
function handleAnimationEnd() {
el.removeEventListener('animationend', handleAnimationEnd);
el.classList.remove('highlighted');
}
el.addEventListener('animationend', handleAnimationEnd);
el.classList.add('highlighted');
},
_handleExpandCollapseTap: function(e) {
e.preventDefault();
this._expanded = !this._expanded;
var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message');
for (var i = 0; i < messageEls.length; i++) {
messageEls[i].expanded = this._expanded;
}
},
_handleScrollTo: function(e) {
this.scrollToMessage(e.detail.message.id);
},
_computeExpandCollapseMessage: function(expanded) {
return expanded ? 'Collapse all' : 'Expand all';
},
_computeCommentsForMessage: function(comments, message, index) {
comments = comments || {};
var messages = this.messages || [];
var msgComments = {};
var mDate = util.parseDate(message.date);
var nextMDate;
if (index < messages.length - 1) {
nextMDate = util.parseDate(messages[index + 1].date);
}
for (var file in comments) {
var fileComments = comments[file];
for (var i = 0; i < fileComments.length; i++) {
var cDate = util.parseDate(fileComments[i].updated);
if (cDate >= mDate) {
if (nextMDate && cDate >= nextMDate) {
continue;
}
msgComments[file] = msgComments[file] || [];
msgComments[file].push(fileComments[i]);
}
}
}
return msgComments;
},
});
})();

View File

@@ -18,13 +18,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-messages-list</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-messages-list.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-messages-list.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,132 @@
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<dom-module id="gr-related-changes-list">
<template>
<style>
:host {
display: block;
}
h3 {
margin: .5em 0 0;
}
section {
margin-bottom: 1em;
}
a {
display: block;
}
.relatedChanges a {
display: inline-block;
}
.strikethrough {
color: #666;
text-decoration: line-through;
}
.status {
color: #666;
font-weight: bold;
}
.notCurrent {
color: #e65100;
}
.indirectAncestor {
color: #33691e;
}
.submittable {
color: #1b5e20;
}
.hidden {
display: none;
}
</style>
<gr-ajax id="relatedXHR"
url="[[_computeRelatedURL(change._number, patchNum)]]"
last-response="{{_relatedResponse}}"></gr-ajax>
<gr-ajax id="submittedTogetherXHR"
url="[[_computeSubmittedTogetherURL(change._number)]]"
last-response="{{_submittedTogether}}"></gr-ajax>
<gr-ajax id="conflictsXHR"
url="/changes/"
params="[[_computeConflictsQueryParams(change._number)]]"
last-response="{{_conflicts}}"></gr-ajax>
<gr-ajax id="cherryPicksXHR"
url="/changes/"
params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]"
last-response="{{_cherryPicks}}"></gr-ajax>
<gr-ajax id="sameTopicXHR"
url="/changes/"
params="[[_computeSameTopicQueryParams(change.topic)]]"
last-response="{{_sameTopic}}"></gr-ajax>
<div hidden$="[[!_loading]]">Loading...</div>
<section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
<h4>Relation Chain</h4>
<template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change">
<div>
<a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.commit.subject]]
</a>
<span class$="[[_computeChangeStatusClass(change)]]">
([[_computeChangeStatus(change)]])
</span>
</div>
</template>
</section>
<section hidden$="[[!_submittedTogether.length]]" hidden>
<h4>Submitted together</h4>
<template is="dom-repeat" items="[[_submittedTogether]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.project]]: [[change.branch]]: [[change.subject]]
</a>
</template>
</section>
<section hidden$="[[!_sameTopic.length]]" hidden>
<h4>Same topic</h4>
<template is="dom-repeat" items="[[_sameTopic]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.project]]: [[change.branch]]: [[change.subject]]
</a>
</template>
</section>
<section hidden$="[[!_conflicts.length]]" hidden>
<h4>Merge conflicts</h4>
<template is="dom-repeat" items="[[_conflicts]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.subject]]
</a>
</template>
</section>
<section hidden$="[[!_cherryPicks.length]]" hidden>
<h4>Cherry picks</h4>
<template is="dom-repeat" items="[[_cherryPicks]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.subject]]
</a>
</template>
</section>
</template>
<script src="gr-related-changes-list.js"></script>
</dom-module>

View File

@@ -0,0 +1,242 @@
// 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-related-changes-list',
properties: {
change: Object,
patchNum: String,
serverConfig: {
type: Object,
observer: '_serverConfigChanged',
},
hidden: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
_loading: Boolean,
_resolveServerConfigReady: Function,
_serverConfigReady: {
type: Object,
value: function() {
return new Promise(function(resolve) {
this._resolveServerConfigReady = resolve;
}.bind(this));
}
},
_connectedRevisions: {
type: Array,
computed: '_computeConnectedRevisions(change, patchNum, ' +
'_relatedResponse.changes)',
},
_relatedResponse: Object,
_submittedTogether: Array,
_conflicts: Array,
_cherryPicks: Array,
_sameTopic: Array,
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_resultsChanged(_relatedResponse.changes, _submittedTogether, ' +
'_conflicts, _cherryPicks, _sameTopic)',
],
reload: function() {
if (!this.change || !this.patchNum) {
return Promise.resolve();
}
this._loading = true;
var promises = [
this.$.relatedXHR.generateRequest().completes,
this.$.submittedTogetherXHR.generateRequest().completes,
this.$.conflictsXHR.generateRequest().completes,
this.$.cherryPicksXHR.generateRequest().completes,
];
return this._serverConfigReady.then(function() {
if (this.change.topic &&
!this.serverConfig.change.submit_whole_topic) {
return this.$.sameTopicXHR.generateRequest().completes;
} else {
this._sameTopic = [];
}
return Promise.resolve();
}.bind(this)).then(Promise.all(promises)).then(function() {
this._loading = false;
}.bind(this));
},
_computeRelatedURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/related';
},
_computeSubmittedTogetherURL: function(changeNum) {
return this.changeBaseURL(changeNum) + '/submitted_together';
},
_computeConflictsQueryParams: function(changeNum) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
return {
O: options,
q: 'status:open is:mergeable conflicts:' + changeNum,
};
},
_computeCherryPicksQueryParams: function(project, changeID, changeNum) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
var query = [
'project:' + project,
'change:' + changeID,
'-change:' + changeNum,
'-is:abandoned',
].join(' ');
return {
O: options,
q: query
}
},
_computeSameTopicQueryParams: function(topic) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT,
this.ListChangesOption.DETAILED_LABELS
);
return {
O: options,
q: 'status:open topic:' + topic,
};
},
_computeChangeURL: function(changeNum, patchNum) {
var urlStr = '/c/' + changeNum;
if (patchNum != null) {
urlStr += '/' + patchNum;
}
return urlStr;
},
_computeLinkClass: function(change) {
if (change.status == this.ChangeStatus.ABANDONED) {
return 'strikethrough';
}
},
_computeChangeStatusClass: function(change) {
var classes = ['status'];
if (change._revision_number != change._current_revision_number) {
classes.push('notCurrent');
} else if (this._isIndirectAncestor(change)) {
classes.push('indirectAncestor');
} else if (change.submittable) {
classes.push('submittable');
} else if (change.status == this.ChangeStatus.NEW) {
classes.push('hidden');
}
return classes.join(' ');
},
_computeChangeStatus: function(change) {
switch (change.status) {
case this.ChangeStatus.MERGED:
return 'Merged';
case this.ChangeStatus.ABANDONED:
return 'Abandoned';
case this.ChangeStatus.DRAFT:
return 'Draft';
}
if (change._revision_number != change._current_revision_number) {
return 'Not current';
} else if (this._isIndirectAncestor(change)) {
return 'Indirect ancestor';
} else if (change.submittable) {
return 'Submittable';
}
return ''
},
_serverConfigChanged: function(config) {
this._resolveServerConfigReady(config);
},
_resultsChanged: function(related, submittedTogether, conflicts,
cherryPicks, sameTopic) {
var results = [
related,
submittedTogether,
conflicts,
cherryPicks,
sameTopic
];
for (var i = 0; i < results.length; i++) {
if (results[i].length > 0) {
this.hidden = false;
return;
}
}
this.hidden = true;
},
_isIndirectAncestor: function(change) {
return this._connectedRevisions.indexOf(change.commit.commit) == -1;
},
_computeConnectedRevisions: function(change, patchNum, relatedChanges) {
var connected = [];
var changeRevision;
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
changeRevision = rev;
}
}
var commits = relatedChanges.map(function(c) { return c.commit; });
var pos = commits.length - 1;
while (pos >= 0) {
var commit = commits[pos].commit;
connected.push(commit);
if (commit == changeRevision) {
break;
}
pos--;
}
while (pos >= 0) {
for (var i = 0; i < commits[pos].parents.length; i++) {
if (connected.indexOf(commits[pos].parents[i].commit) != -1) {
connected.push(commits[pos].commit);
break;
}
}
--pos;
}
return connected;
},
});
})();

View File

@@ -18,11 +18,11 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-related-changes-list</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.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="../elements/gr-related-changes-list.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-related-changes-list.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,148 @@
<!--
Copyright (C) 2015 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-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<dom-module id="gr-reply-dialog">
<style>
:host {
display: block;
max-height: 90vh;
}
:host([disabled]) {
pointer-events: none;
}
:host([disabled]) .container {
opacity: .5;
}
.container {
display: flex;
flex-direction: column;
max-height: 90vh;
}
section {
border-top: 1px solid #ddd;
padding: .5em .75em;
}
.textareaContainer,
.labelsContainer,
.actionsContainer {
flex-shrink: 0;
}
.textareaContainer {
position: relative;
}
iron-autogrow-textarea {
padding: 0;
font-family: var(--monospace-font-family);
}
.message {
border: none;
width: 100%;
}
.labelContainer:not(:first-of-type) {
margin-top: .5em;
}
.labelName {
display: inline-block;
width: 7em;
margin-right: .5em;
white-space: nowrap;
}
iron-selector {
display: inline-flex;
}
iron-selector > gr-button {
margin-right: .25em;
}
iron-selector > gr-button:first-of-type {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
iron-selector > gr-button:last-of-type {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
iron-selector > gr-button.iron-selected {
background-color: #ddd;
}
.draftsContainer {
overflow-y: auto;
}
.draftsContainer h3 {
margin-top: .25em;
}
.actionsContainer {
display: flex;
justify-content: space-between;
}
.action:link,
.action:visited {
color: #00e;
}
</style>
<template>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsURL(changeNum)]]"
last-response="{{_drafts}}"></gr-ajax>
<div class="container">
<section class="textareaContainer">
<iron-autogrow-textarea
id="textarea"
class="message"
placeholder="Say something..."
disabled="{{disabled}}"
rows="4"
max-rows="15"
bind-value="{{draft}}"></iron-autogrow-textarea>
</section>
<section class="labelsContainer">
<template is="dom-repeat"
items="[[_computeLabelArray(permittedLabels)]]" as="label">
<div class="labelContainer">
<span class="labelName">[[label]]</span>
<iron-selector data-label$="[[label]]"
selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
<template is="dom-repeat"
items="[[_computePermittedLabelValues(permittedLabels, label)]]"
as="value">
<gr-button data-value$="[[value]]">[[value]]</gr-button>
</template>
</iron-selector>
</div>
</template>
</section>
<section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
<h3>[[_computeDraftsTitle(_drafts)]]</h3>
<gr-comment-list
comments="[[_drafts]]"
change-num="[[changeNum]]"
patch-num="[[patchNum]]"></gr-comment-list>
</section>
<section class="actionsContainer">
<gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
<gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button>
</section>
</div>
</template>
<script src="gr-reply-dialog.js"></script>
</dom-module>

View File

@@ -0,0 +1,171 @@
// 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-reply-dialog',
/**
* Fired when a reply is successfully sent.
*
* @event send
*/
/**
* Fired when the user presses the cancel button.
*
* @event cancel
*/
properties: {
changeNum: String,
patchNum: String,
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
draft: {
type: String,
value: '',
},
labels: Object,
permittedLabels: Object,
_account: Object,
_drafts: Object,
_xhrPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.RESTClientBehavior,
],
ready: function() {
app.accountReady.then(function() {
this._account = app.account;
}.bind(this));
},
reload: function() {
return this.$.draftsXHR.generateRequest().completes;
},
focus: function() {
this.async(function() {
this.$.textarea.textarea.focus();
}.bind(this));
},
_computeDraftsURL: function(changeNum) {
return '/changes/' + changeNum + '/drafts';
},
_computeHideDraftList: function(drafts) {
return Object.keys(drafts || {}).length == 0;
},
_computeDraftsTitle: function(drafts) {
var total = 0;
for (var file in drafts) {
total += drafts[file].length;
}
if (total == 0) { return ''; }
if (total == 1) { return '1 Draft'; }
if (total > 1) { return total + ' Drafts'; }
},
_computeLabelArray: function(labelsObj) {
return Object.keys(labelsObj).sort();
},
_computeIndexOfLabelValue: function(
labels, permittedLabels, labelName, account) {
var t = labels[labelName];
if (!t) { return null; }
var labelValue = t.default_value;
// Is there an existing vote for the current user? If so, use that.
var votes = labels[labelName];
if (votes.all && votes.all.length > 0) {
for (var i = 0; i < votes.all.length; i++) {
if (votes.all[i]._account_id == account._account_id) {
labelValue = votes.all[i].value;
break;
}
}
}
var len = permittedLabels[labelName] != null ?
permittedLabels[labelName].length : 0;
for (var i = 0; i < len; i++) {
var val = parseInt(permittedLabels[labelName][i], 10);
if (val == labelValue) {
return i;
}
}
return null;
},
_computePermittedLabelValues: function(permittedLabels, label) {
return permittedLabels[label];
},
_cancelTapHandler: function(e) {
e.preventDefault();
this._drafts = null;
this.fire('cancel', null, {bubbles: false});
},
_sendTapHandler: function(e) {
e.preventDefault();
var obj = {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {},
};
for (var label in this.permittedLabels) {
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
selectedVal = parseInt(selectedVal, 10);
obj.labels[label] = selectedVal;
}
if (this.draft != null) {
obj.message = this.draft;
}
this.disabled = true;
this._send(obj).then(function(req) {
this.fire('send', null, {bubbles: false});
this.draft = '';
this.disabled = false;
this._drafts = null;
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_send: function(payload) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: 'POST',
url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review',
body: payload,
});
return this._xhrPromise;
},
});
})();

View File

@@ -18,13 +18,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-dialog</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-reply-dialog.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-reply-dialog.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,118 @@
<!--
Copyright (C) 2015 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="../../../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<dom-module id="gr-reviewer-list">
<style>
:host {
display: block;
}
:host([disabled]) {
opacity: .8;
pointer-events: none;
}
.autocompleteContainer {
position: relative;
}
.inputContainer {
display: flex;
margin-top: .25em;
}
.inputContainer input {
flex: 1;
font: inherit;
}
.dropdown {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
position: absolute;
left: 0;
top: 100%;
}
.dropdown .reviewer {
cursor: pointer;
padding: .5em .75em;
}
.dropdown .reviewer[selected] {
background-color: #ccc;
}
.remove,
.cancel {
color: #999;
}
.remove {
font-size: .9em;
}
.cancel {
font-size: 2em;
line-height: 1;
padding: 0 .15em;
text-decoration: none;
}
</style>
<template>
<gr-ajax id="autocompleteXHR"
url="[[_computeAutocompleteURL(change)]]"
params="[[_computeAutocompleteParams(_inputVal)]]"
on-response="_handleResponse"></gr-ajax>
<template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
<div class="reviewer">
<gr-account-link account="[[reviewer]]" show-email></gr-account-link>
<gr-button link
class="remove"
data-account-id$="[[reviewer._account_id]]"
on-tap="_handleRemoveTap"
hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom>
</div>
</template>
<div class="controlsContainer" hidden$="[[!mutable]]">
<div class="autocompleteContainer" hidden$="[[!_showInput]]">
<div class="inputContainer">
<input is="iron-input" id="input"
bind-value="{{_inputVal}}" disabled$="[[disabled]]">
<gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button>
</div>
<div class="dropdown" hidden$="[[_hideAutocomplete]]">
<template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer">
<div class="reviewer"
data-index$="[[index]]"
on-mouseenter="_handleMouseEnterItem"
on-tap="_handleItemTap"
selected$="[[_computeSelected(index, _selectedIndex)]]">
<template is="dom-if" if="[[reviewer.account]]">
<gr-account-label
account="[[reviewer.account]]" show-email></gr-account-label>
</template>
<template is="dom-if" if="[[reviewer.group]]">
<span>[[reviewer.group.name]] (group)</span>
</template>
</div>
</template>
</div>
</div>
<gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
hidden$="[[_showInput]]">Add reviewer</gr-button>
</div>
</template>
<script src="gr-reviewer-list.js"></script>
</dom-module>

View File

@@ -0,0 +1,344 @@
// 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-reviewer-list',
properties: {
change: Object,
mutable: {
type: Boolean,
value: false,
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
suggestFrom: {
type: Number,
value: 3,
},
_reviewers: {
type: Array,
value: function() { return []; },
},
_autocompleteData: {
type: Array,
value: function() { return []; },
observer: '_autocompleteDataChanged',
},
_inputVal: {
type: String,
value: '',
observer: '_inputValChanged',
},
_inputRequestHandle: Number,
_inputRequestTimeout: {
type: Number,
value: 250,
},
_showInput: {
type: Boolean,
value: false,
},
_hideAutocomplete: {
type: Boolean,
value: true,
observer: '_hideAutocompleteChanged',
},
_selectedIndex: {
type: Number,
value: 0,
},
_boundBodyClickHandler: {
type: Function,
value: function() {
return this._handleBodyClick.bind(this);
},
},
// Used for testing.
_lastAutocompleteRequest: Object,
_xhrPromise: Object,
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
],
observers: [
'_reviewersChanged(change.reviewers.*, change.owner)',
],
detached: function() {
this._clearInputRequestHandle();
},
_clearInputRequestHandle: function() {
if (this._inputRequestHandle != null) {
this.cancelAsync(this._inputRequestHandle);
this._inputRequestHandle = null;
}
},
_reviewersChanged: function(changeRecord, owner) {
var result = [];
var reviewers = changeRecord.base;
for (var key in reviewers) {
if (key == 'REVIEWER' || key == 'CC') {
result = result.concat(reviewers[key]);
}
}
this._reviewers = result.filter(function(reviewer) {
return reviewer._account_id != owner._account_id;
});
},
_computeCanRemoveReviewer: function(reviewer, mutable) {
if (!mutable) { return false; }
for (var i = 0; i < this.change.removable_reviewers.length; i++) {
if (this.change.removable_reviewers[i]._account_id ==
reviewer._account_id) {
return true;
}
}
return false;
},
_computeAutocompleteURL: function(change) {
return '/changes/' + change._number + '/suggest_reviewers';
},
_computeAutocompleteParams: function(inputVal) {
return {
n: 10, // Return max 10 results
q: inputVal,
};
},
_computeSelected: function(index, selectedIndex) {
return index == selectedIndex;
},
_handleResponse: function(e) {
this._autocompleteData = e.detail.response.filter(function(reviewer) {
var account = reviewer.account;
if (!account) { return true; }
for (var i = 0; i < this._reviewers.length; i++) {
if (account._account_id == this.change.owner._account_id ||
account._account_id == this._reviewers[i]._account_id) {
return false;
}
}
return true;
}, this);
},
_handleBodyClick: function(e) {
var eventPath = Polymer.dom(e).path;
for (var i = 0; i < eventPath.length; i++) {
if (eventPath[i] == this) {
return;
}
}
this._selectedIndex = -1;
this._autocompleteData = [];
},
_handleRemoveTap: function(e) {
e.preventDefault();
var target = Polymer.dom(e).rootTarget;
var accountID = parseInt(target.getAttribute('data-account-id'), 10);
this._send('DELETE', this._restEndpoint(accountID)).then(function(req) {
var reviewers = this.change.reviewers;
['REVIEWER', 'CC'].forEach(function(type) {
reviewers[type] = reviewers[type] || [];
for (var i = 0; i < reviewers[type].length; i++) {
if (reviewers[type][i]._account_id == accountID) {
this.splice('change.reviewers.' + type, i, 1);
break;
}
}
}, this);
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_handleAddTap: function(e) {
e.preventDefault();
this._showInput = true;
this.$.input.focus();
},
_handleCancelTap: function(e) {
e.preventDefault();
this._cancel();
},
_handleMouseEnterItem: function(e) {
this._selectedIndex =
parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10);
},
_handleItemTap: function(e) {
var reviewerEl;
var eventPath = Polymer.dom(e).path;
for (var i = 0; i < eventPath.length; i++) {
var el = eventPath[i];
if (el.classList && el.classList.contains('reviewer')) {
reviewerEl = el;
break;
}
}
this._selectedIndex =
parseInt(reviewerEl.getAttribute('data-index'), 10);
this._sendAddRequest();
},
_autocompleteDataChanged: function(data) {
this._hideAutocomplete = data.length == 0;
},
_hideAutocompleteChanged: function(hidden) {
if (hidden) {
document.body.removeEventListener('click',
this._boundBodyClickHandler);
this._selectedIndex = -1;
} else {
document.body.addEventListener('click', this._boundBodyClickHandler);
this._selectedIndex = 0;
}
},
_inputValChanged: function(val) {
var sendRequest = function() {
if (this.disabled || val == null || val.trim().length == 0) {
return;
}
if (val.length < this.suggestFrom) {
this._clearInputRequestHandle();
this._hideAutocomplete = true;
this._selectedIndex = -1;
return;
}
this._lastAutocompleteRequest =
this.$.autocompleteXHR.generateRequest();
}.bind(this);
this._clearInputRequestHandle();
if (this._inputRequestTimeout == 0) {
sendRequest();
} else {
this._inputRequestHandle =
this.async(sendRequest, this._inputRequestTimeout);
}
},
_handleKey: function(e) {
if (this._hideAutocomplete) {
if (e.keyCode == 27) { // 'esc'
e.preventDefault();
this._cancel();
}
return;
}
switch (e.keyCode) {
case 38: // 'up':
e.preventDefault();
this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
break;
case 40: // 'down'
e.preventDefault();
this._selectedIndex = Math.min(this._selectedIndex + 1,
this._autocompleteData.length - 1);
break;
case 27: // 'esc'
e.preventDefault();
this._hideAutocomplete = true;
break;
case 13: // 'enter'
e.preventDefault();
this._sendAddRequest();
break;
}
},
_cancel: function() {
this._showInput = false;
this._selectedIndex = 0;
this._inputVal = '';
this._autocompleteData = [];
this.$.addReviewer.focus();
},
_sendAddRequest: function() {
this._clearInputRequestHandle();
var reviewerID;
var reviewer = this._autocompleteData[this._selectedIndex];
if (reviewer.account) {
reviewerID = reviewer.account._account_id;
} else if (reviewer.group) {
reviewerID = reviewer.group.id;
}
this._autocompleteData = [];
this._send('POST', this._restEndpoint(), reviewerID).then(function(req) {
this.change.reviewers.CC = this.change.reviewers.CC || [];
req.response.reviewers.forEach(function(r) {
this.push('change.removable_reviewers', r);
this.push('change.reviewers.CC', r);
}, this);
this._inputVal = '';
this.$.input.focus();
}.bind(this)).catch(function(err) {
// TODO(andybons): Use the message returned by the server.
alert('Unable to add ' + reviewerID + ' as a reviewer.');
throw err;
}.bind(this));
},
_send: function(method, url, reviewerID) {
this.disabled = true;
var request = document.createElement('gr-request');
var opts = {
method: method,
url: url,
};
if (reviewerID) {
opts.body = {reviewer: reviewerID};
}
this._xhrPromise = request.send(opts);
var enableEl = function() { this.disabled = false; }.bind(this);
this._xhrPromise.then(enableEl).catch(enableEl);
return this._xhrPromise;
},
_restEndpoint: function(id) {
var path = '/changes/' + this.change._number + '/reviewers';
if (id) {
path += '/' + id;
}
return path;
},
});
})();

View File

@@ -18,12 +18,12 @@ 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-list</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-reviewer-list.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-reviewer-list.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,54 @@
<!--
Copyright (C) 2015 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="../gr-diff-comment/gr-diff-comment.html">
<dom-module id="gr-diff-comment-thread">
<template>
<style>
:host {
display: block;
white-space: normal;
}
gr-diff-comment {
border-left: 1px solid #ddd;
}
gr-diff-comment:first-of-type {
border-top: 1px solid #ddd;
}
gr-diff-comment:last-of-type {
border-bottom: 1px solid #ddd;
}
</style>
<div id="container">
<template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment">
<gr-diff-comment
comment="{{comment}}"
change-num="[[changeNum]]"
patch-num="[[patchNum]]"
draft="[[comment.__draft]]"
show-actions="[[showActions]]"
project-config="[[projectConfig]]"
on-height-change="_handleCommentHeightChange"
on-reply="_handleCommentReply"
on-discard="_handleCommentDiscard"
on-done="_handleCommentDone"></gr-diff-comment>
</template>
</div>
</template>
<script src="gr-diff-comment-thread.js"></script>
</dom-module>

View File

@@ -0,0 +1,214 @@
// 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-diff-comment-thread',
/**
* Fired when the height of the thread changes.
*
* @event height-change
*/
/**
* Fired when the thread should be discarded.
*
* @event discard
*/
properties: {
changeNum: String,
comments: {
type: Array,
value: function() { return []; },
},
patchNum: String,
path: String,
showActions: Boolean,
projectConfig: Object,
_boundWindowResizeHandler: {
type: Function,
value: function() { return this._handleWindowResize.bind(this); }
},
_lastHeight: Number,
_orderedComments: Array,
},
get naturalHeight() {
return this.$.container.offsetHeight;
},
observers: [
'_commentsChanged(comments.splices)',
],
attached: function() {
window.addEventListener('resize', this._boundWindowResizeHandler);
},
detached: function() {
window.removeEventListener('resize', this._boundWindowResizeHandler);
},
_handleWindowResize: function(e) {
this._heightChanged();
},
_commentsChanged: function(changeRecord) {
this._orderedComments = this._sortedComments(this.comments);
},
_sortedComments: function(comments) {
comments.sort(function(c1, c2) {
var c1Date = c1.__date || util.parseDate(c1.updated);
var c2Date = c2.__date || util.parseDate(c2.updated);
return c1Date - c2Date;
});
var commentIDToReplies = {};
var topLevelComments = [];
for (var i = 0; i < comments.length; i++) {
var c = comments[i];
if (c.in_reply_to) {
if (commentIDToReplies[c.in_reply_to] == null) {
commentIDToReplies[c.in_reply_to] = [];
}
commentIDToReplies[c.in_reply_to].push(c);
} else {
topLevelComments.push(c);
}
}
var results = [];
for (var i = 0; i < topLevelComments.length; i++) {
this._visitComment(topLevelComments[i], commentIDToReplies, results);
}
return results;
},
_visitComment: function(parent, commentIDToReplies, results) {
results.push(parent);
var replies = commentIDToReplies[parent.id];
if (!replies) { return; }
for (var i = 0; i < replies.length; i++) {
this._visitComment(replies[i], commentIDToReplies, results);
}
},
_handleCommentHeightChange: function(e) {
e.stopPropagation();
this._heightChanged();
},
_handleCommentReply: function(e) {
var comment = e.detail.comment;
var quoteStr;
if (e.detail.quote) {
var msg = comment.message;
var quoteStr = msg.split('\n').map(
function(line) { return ' > ' + line; }).join('\n') + '\n\n';
}
var reply =
this._newReply(comment.id, comment.line, this.path, quoteStr);
this.push('comments', reply);
// Allow the reply to render in the dom-repeat.
this.async(function() {
var commentEl = this._commentElWithDraftID(reply.__draftID);
commentEl.editing = true;
this.async(this._heightChanged.bind(this), 1);
}.bind(this), 1);
},
_handleCommentDone: function(e) {
var comment = e.detail.comment;
var reply = this._newReply(comment.id, comment.line, this.path, 'Done');
this.push('comments', reply);
// Allow the reply to render in the dom-repeat.
this.async(function() {
var commentEl = this._commentElWithDraftID(reply.__draftID);
commentEl.save();
this.async(this._heightChanged.bind(this), 1);
}.bind(this), 1);
},
_commentElWithDraftID: function(draftID) {
var commentEls =
Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
for (var i = 0; i < commentEls.length; i++) {
if (commentEls[i].comment.__draftID == draftID) {
return commentEls[i];
}
}
return null;
},
_newReply: function(inReplyTo, line, path, opt_message) {
var c = {
__draft: true,
__draftID: Math.random().toString(36),
__date: new Date(),
line: line,
path: path,
in_reply_to: inReplyTo,
};
if (opt_message != null) {
c.message = opt_message;
}
return c;
},
_handleCommentDiscard: function(e) {
// TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady
// DOM, it respects the bubbles property.
// https://github.com/Polymer/polymer/issues/3226
e.stopPropagation();
var diffCommentEl = Polymer.dom(e).rootTarget;
var idx = this._indexOf(diffCommentEl.comment, this.comments);
if (idx == -1) {
throw Error('Cannot find comment ' +
JSON.stringify(diffCommentEl.comment));
}
this.splice('comments', idx, 1);
if (this.comments.length == 0) {
this.fire('discard', null, {bubbles: false});
return;
}
this.async(this._heightChanged.bind(this), 1);
},
_heightChanged: function() {
var height = this.$.container.offsetHeight;
if (height == this._lastHeight) { return; }
this.fire('height-change', {height: height}, {bubbles: false});
this._lastHeight = height;
},
_indexOf: function(comment, arr) {
for (var i = 0; i < arr.length; i++) {
var c = arr[i];
if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
(c.id != null && c.id == comment.id)) {
return i;
}
}
return -1;
},
});
})();

View File

@@ -18,12 +18,12 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-comment-thread</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-diff-comment-thread.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-comment-thread.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,153 @@
<!--
Copyright (C) 2015 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-autogrow-textarea/iron-autogrow-textarea.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-linked-text/gr-linked-text.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<dom-module id="gr-diff-comment">
<template>
<style>
:host {
background-color: #ffd;
display: block;
--iron-autogrow-textarea: {
padding: 2px;
};
}
:host([disabled]) {
pointer-events: none;
}
:host([disabled]) .container {
opacity: .5;
}
.header,
.message,
.actions {
padding: .5em .7em;
}
.header {
display: flex;
padding-bottom: 0;
font-family: 'Open Sans', sans-serif;
}
.headerLeft {
flex: 1;
}
.authorName,
.draftLabel {
font-weight: bold;
}
.draftLabel {
color: #999;
display: none;
}
.date {
justify-content: flex-end;
margin-left: 5px;
}
a.date:link,
a.date:visited {
color: #666;
}
.actions {
display: flex;
padding-top: 0;
}
.action {
margin-right: .5em;
}
.danger {
display: flex;
flex: 1;
justify-content: flex-end;
}
.editMessage {
display: none;
margin: .5em .7em;
width: calc(100% - 1.4em - 2px);
}
.danger .action {
margin-right: 0;
}
.container:not(.draft) .actions :not(.reply):not(.quote):not(.done) {
display: none;
}
.draft .reply,
.draft .quote,
.draft .done {
display: none;
}
.draft .draftLabel {
display: inline;
}
.draft:not(.editing) .save,
.draft:not(.editing) .cancel {
display: none;
}
.editing .message,
.editing .reply,
.editing .quote,
.editing .done,
.editing .edit {
display: none;
}
.editing .editMessage {
background-color: #fff;
display: block;
}
</style>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
<span class="authorName">[[comment.author.name]]</span>
<span class="draftLabel">DRAFT</span>
</div>
<a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
<gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
</a>
</div>
<iron-autogrow-textarea
id="editTextarea"
class="editMessage"
disabled="{{disabled}}"
rows="4"
bind-value="{{_editDraft}}"
on-keyup="_handleTextareaKeyup"
on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
<gr-linked-text class="message"
pre
content="[[comment.message]]"
config="[[projectConfig.commentlinks]]"></gr-linked-text>
<div class="actions" hidden$="[[!showActions]]">
<gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
<gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button>
<gr-button class="action done" on-tap="_handleDone">Done</gr-button>
<gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button>
<gr-button class="action save" on-tap="_handleSave"
disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button>
<gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button>
<div class="danger">
<gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button>
</div>
</div>
</div>
</template>
<script src="gr-diff-comment.js"></script>
</dom-module>

View File

@@ -0,0 +1,247 @@
// 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-diff-comment',
/**
* Fired when the height of the comment changes.
*
* @event height-change
*/
/**
* Fired when the Reply action is triggered.
*
* @event reply
*/
/**
* Fired when the Done action is triggered.
*
* @event done
*/
/**
* Fired when this comment is discarded.
*
* @event discard
*/
properties: {
changeNum: String,
comment: {
type: Object,
notify: true,
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
draft: {
type: Boolean,
value: false,
observer: '_draftChanged',
},
editing: {
type: Boolean,
value: false,
observer: '_editingChanged',
},
patchNum: String,
showActions: Boolean,
projectConfig: Object,
_xhrPromise: Object, // Used for testing.
_editDraft: String,
},
ready: function() {
this._editDraft = (this.comment && this.comment.message) || '';
this.editing = this._editDraft.length == 0;
},
attached: function() {
this._heightChanged();
},
save: function() {
this.comment.message = this._editDraft;
this.disabled = true;
var endpoint = this._restEndpoint(this.comment.id);
this._send('PUT', endpoint).then(function(req) {
this.disabled = false;
var comment = req.response;
comment.__draft = true;
// Maintain the ephemeral draft ID for identification by other
// elements.
if (this.comment.__draftID) {
comment.__draftID = this.comment.__draftID;
}
this.comment = comment;
this.editing = false;
}.bind(this)).catch(function(err) {
alert('Your draft couldnt be saved. Check the console and contact ' +
'the PolyGerrit team for assistance.');
this.disabled = false;
}.bind(this));
},
_heightChanged: function() {
this.async(function() {
this.fire('height-change', {height: this.offsetHeight},
{bubbles: false});
}.bind(this));
},
_draftChanged: function(draft) {
this.$.container.classList.toggle('draft', draft);
},
_editingChanged: function(editing) {
this.$.container.classList.toggle('editing', editing);
if (editing) {
var textarea = this.$.editTextarea.textarea;
// Put the cursor at the end always.
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.selectionStart;
this.async(function() {
textarea.focus();
}.bind(this));
}
if (this.comment && this.comment.id) {
this.$$('.cancel').hidden = !editing;
}
this._heightChanged();
},
_computeLinkToComment: function(comment) {
return '#' + comment.line;
},
_computeSaveDisabled: function(draft) {
return draft == null || draft.trim() == '';
},
_handleTextareaKeyup: function(e) {
// TODO(andybons): This isn't always true, but I can't currently think
// of a better metric.
this._heightChanged();
},
_handleTextareaKeydown: function(e) {
if (e.keyCode == 27) { // 'esc'
this._handleCancel(e);
}
},
_handleLinkTap: function(e) {
e.preventDefault();
var hash = this._computeLinkToComment(this.comment);
// Don't add the hash to the window history if it's already there.
// Otherwise you mess up expected back button behavior.
if (window.location.hash == hash) { return; }
// Change the URL but dont trigger a nav event. Otherwise it will
// reload the page.
page.show(window.location.pathname + hash, null, false);
},
_handleReply: function(e) {
this._preventDefaultAndBlur(e);
this.fire('reply', {comment: this.comment}, {bubbles: false});
},
_handleQuote: function(e) {
this._preventDefaultAndBlur(e);
this.fire('reply', {comment: this.comment, quote: true},
{bubbles: false});
},
_handleDone: function(e) {
this._preventDefaultAndBlur(e);
this.fire('done', {comment: this.comment}, {bubbles: false});
},
_handleEdit: function(e) {
this._preventDefaultAndBlur(e);
this._editDraft = this.comment.message;
this.editing = true;
},
_handleSave: function(e) {
this._preventDefaultAndBlur(e);
this.save();
},
_handleCancel: function(e) {
this._preventDefaultAndBlur(e);
if (this.comment.message == null || this.comment.message.length == 0) {
this.fire('discard', null, {bubbles: false});
return;
}
this._editDraft = this.comment.message;
this.editing = false;
},
_handleDiscard: function(e) {
this._preventDefaultAndBlur(e);
if (!this.comment.__draft) {
throw Error('Cannot discard a non-draft comment.');
}
this.disabled = true;
var commentID = this.comment.id;
if (!commentID) {
this.fire('discard', null, {bubbles: false});
return;
}
this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
this.fire('discard', null, {bubbles: false});
}.bind(this)).catch(function(err) {
alert('Your draft couldnt be deleted. Check the console and ' +
'contact the PolyGerrit team for assistance.');
this.disabled = false;
}.bind(this));
},
_preventDefaultAndBlur: function(e) {
e.preventDefault();
Polymer.dom(e).rootTarget.blur();
},
_send: function(method, url) {
var xhr = document.createElement('gr-request');
var opts = {
method: method,
url: url,
};
if (method == 'PUT' || method == 'POST') {
opts.body = this.comment;
}
this._xhrPromise = xhr.send(opts);
return this._xhrPromise;
},
_restEndpoint: function(id) {
var path = '/changes/' + this.changeNum + '/revisions/' +
this.patchNum + '/drafts';
if (id) {
path += '/' + id;
}
return path;
},
});
})();

View File

@@ -18,13 +18,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-comment</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-diff-comment.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-comment.html">
<test-fixture id="basic">
<template>

View File

@@ -14,9 +14,9 @@ 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="gr-button.html">
<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">
<dom-module id="gr-diff-preferences">
<template>
@@ -106,65 +106,5 @@ limitations under the License.
<gr-button on-tap="_handleCancel">Cancel</gr-button>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff-preferences',
/**
* Fired when the user presses the save button.
*
* @event save
*/
/**
* Fired when the user presses the cancel button.
*
* @event cancel
*/
properties: {
prefs: {
type: Object,
notify: true,
value: function() { return {}; },
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
},
observers: [
'_prefsChanged(prefs.*)',
],
_prefsChanged: function(changeRecord) {
var prefs = changeRecord.base;
this.$.contextSelect.value = prefs.context;
this.$.showTabsInput.checked = prefs.show_tabs;
},
_handleContextSelectChange: function(e) {
var selectEl = Polymer.dom(e).rootTarget;
this.set('prefs.context', parseInt(selectEl.value, 10));
},
_handleShowTabsTap: function(e) {
this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked);
},
_handleSave: function() {
this.fire('save', null, {bubbles: false});
},
_handleCancel: function() {
this.fire('cancel', null, {bubbles: false});
},
});
})();
</script>
<script src="gr-diff-preferences.js"></script>
</dom-module>

View File

@@ -0,0 +1,72 @@
// 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-diff-preferences',
/**
* Fired when the user presses the save button.
*
* @event save
*/
/**
* Fired when the user presses the cancel button.
*
* @event cancel
*/
properties: {
prefs: {
type: Object,
notify: true,
value: function() { return {}; },
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
},
observers: [
'_prefsChanged(prefs.*)',
],
_prefsChanged: function(changeRecord) {
var prefs = changeRecord.base;
this.$.contextSelect.value = prefs.context;
this.$.showTabsInput.checked = prefs.show_tabs;
},
_handleContextSelectChange: function(e) {
var selectEl = Polymer.dom(e).rootTarget;
this.set('prefs.context', parseInt(selectEl.value, 10));
},
_handleShowTabsTap: function(e) {
this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked);
},
_handleSave: function() {
this.fire('save', null, {bubbles: false});
},
_handleCancel: function() {
this.fire('cancel', null, {bubbles: false});
},
});
})();

View File

@@ -18,11 +18,11 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-preferences</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<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="../elements/gr-diff-preferences.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-preferences.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,97 @@
<!--
Copyright (C) 2015 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="../gr-diff-comment-thread/gr-diff-comment-thread.html">
<dom-module id="gr-diff-side">
<template>
<style>
:host,
.container {
display: flex;
flex: 0 0 auto;
}
.lineNum:before,
.code:before {
/* To ensure the height is non-zero in these elements, a
zero-width space is set as its content. The character
itself doesn't matter. Just that there is something
there. */
content: '\200B';
}
.lineNum {
background-color: #eee;
color: #666;
padding: 0 .75em;
text-align: right;
}
.canComment .lineNum {
cursor: pointer;
text-decoration: underline;
}
.canComment .lineNum:hover {
background-color: #ccc;
}
.lightHighlight {
background-color: var(--light-highlight-color);
}
hl,
.darkHighlight {
background-color: var(--dark-highlight-color);
}
.br:after {
/* Line feed */
content: '\A';
}
.tab {
display: inline-block;
}
.tab.withIndicator:before {
color: #C62828;
/* >> character */
content: '\00BB';
}
.numbers,
.content {
white-space: pre;
}
.numbers .filler {
background-color: #eee;
}
.contextControl {
background-color: #fef;
}
.contextControl a:link,
.contextControl a:visited {
display: block;
text-decoration: none;
}
.numbers .contextControl {
padding: 0 .75em;
text-align: right;
}
.content .contextControl {
text-align: center;
}
</style>
<div class$="[[_computeContainerClass(canComment)]]">
<div class="numbers" id="numbers"></div>
<div class="content" id="content"></div>
</div>
</template>
<script src="gr-diff-side.js"></script>
</dom-module>

View File

@@ -0,0 +1,613 @@
// 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 CharCode = {
LESS_THAN: '<'.charCodeAt(0),
GREATER_THAN: '>'.charCodeAt(0),
AMPERSAND: '&'.charCodeAt(0),
SEMICOLON: ';'.charCodeAt(0),
};
var TAB_REGEX = /\t/g;
Polymer({
is: 'gr-diff-side',
/**
* Fired when an expand context control is clicked.
*
* @event expand-context
*/
/**
* Fired when a thread's height is changed.
*
* @event thread-height-change
*/
/**
* Fired when a draft should be added.
*
* @event add-draft
*/
/**
* Fired when a thread is removed.
*
* @event remove-thread
*/
properties: {
canComment: {
type: Boolean,
value: false,
},
content: {
type: Array,
notify: true,
observer: '_contentChanged',
},
prefs: {
type: Object,
value: function() { return {}; },
},
changeNum: String,
patchNum: String,
path: String,
projectConfig: {
type: Object,
observer: '_projectConfigChanged',
},
_lineFeedHTML: {
type: String,
value: '<span class="style-scope gr-diff-side br"></span>',
readOnly: true,
},
_highlightStartTag: {
type: String,
value: '<hl class="style-scope gr-diff-side">',
readOnly: true,
},
_highlightEndTag: {
type: String,
value: '</hl>',
readOnly: true,
},
_diffChunkLineNums: {
type: Array,
value: function() { return []; },
},
_commentThreadLineNums: {
type: Array,
value: function() { return []; },
},
_focusedLineNum: {
type: Number,
value: 1,
},
},
listeners: {
'tap': '_tapHandler',
},
observers: [
'_prefsChanged(prefs.*)',
],
rowInserted: function(index) {
this.renderLineIndexRange(index, index);
this._updateDOMIndices();
this._updateJumpIndices();
},
rowRemoved: function(index) {
var removedEls = Polymer.dom(this.root).querySelectorAll(
'[data-index="' + index + '"]');
for (var i = 0; i < removedEls.length; i++) {
removedEls[i].parentNode.removeChild(removedEls[i]);
}
this._updateDOMIndices();
this._updateJumpIndices();
},
rowUpdated: function(index) {
var removedEls = Polymer.dom(this.root).querySelectorAll(
'[data-index="' + index + '"]');
for (var i = 0; i < removedEls.length; i++) {
removedEls[i].parentNode.removeChild(removedEls[i]);
}
this.renderLineIndexRange(index, index);
},
scrollToLine: function(lineNum) {
if (isNaN(lineNum) || lineNum < 1) { return; }
var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
if (!el) { return; }
// Calculate where the line is relative to the window.
var top = el.offsetTop;
for (var offsetParent = el.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
// Scroll the element to the middle of the window. Dividing by a third
// instead of half the inner height feels a bit better otherwise the
// element appears to be below the center of the window even when it
// isn't.
window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
},
scrollToNextDiffChunk: function() {
this._scrollToNextChunkOrThread(this._diffChunkLineNums);
},
scrollToPreviousDiffChunk: function() {
this._scrollToPreviousChunkOrThread(this._diffChunkLineNums);
},
scrollToNextCommentThread: function() {
this._scrollToNextChunkOrThread(this._commentThreadLineNums);
},
scrollToPreviousCommentThread: function() {
this._scrollToPreviousChunkOrThread(this._commentThreadLineNums);
},
renderLineIndexRange: function(startIndex, endIndex) {
this._render(this.content, startIndex, endIndex);
},
hideElementsWithIndex: function(index) {
var els = Polymer.dom(this.root).querySelectorAll(
'[data-index="' + index + '"]');
for (var i = 0; i < els.length; i++) {
els[i].setAttribute('hidden', true);
}
},
getRowHeight: function(index) {
var row = this.content[index];
// Filler elements should not be taken into account when determining
// height calculations.
if (row.type == 'FILLER') {
return 0;
}
if (row.height != null) {
return row.height;
}
var selector = '[data-index="' + index + '"]';
var els = Polymer.dom(this.root).querySelectorAll(selector);
if (els.length != 2) {
throw Error('Rows should only consist of two elements');
}
return Math.max(els[0].offsetHeight, els[1].offsetHeight);
},
getRowNaturalHeight: function(index) {
var contentEl = this.$$('.content [data-index="' + index + '"]');
return contentEl.naturalHeight || contentEl.offsetHeight;
},
setRowNaturalHeight: function(index) {
var lineEl = this.$$('.numbers [data-index="' + index + '"]');
var contentEl = this.$$('.content [data-index="' + index + '"]');
contentEl.style.height = null;
var height = contentEl.offsetHeight;
lineEl.style.height = height + 'px';
this.content[index].height = height;
return height;
},
setRowHeight: function(index, height) {
var selector = '[data-index="' + index + '"]';
var els = Polymer.dom(this.root).querySelectorAll(selector);
for (var i = 0; i < els.length; i++) {
els[i].style.height = height + 'px';
}
this.content[index].height = height;
},
_scrollToNextChunkOrThread: function(lineNums) {
for (var i = 0; i < lineNums.length; i++) {
if (lineNums[i] > this._focusedLineNum) {
this._focusedLineNum = lineNums[i];
this.scrollToLine(this._focusedLineNum);
return;
}
}
},
_scrollToPreviousChunkOrThread: function(lineNums) {
for (var i = lineNums.length - 1; i >= 0; i--) {
if (this._focusedLineNum > lineNums[i]) {
this._focusedLineNum = lineNums[i];
this.scrollToLine(this._focusedLineNum);
return;
}
}
},
_updateJumpIndices: function() {
this._commentThreadLineNums = [];
this._diffChunkLineNums = [];
var inHighlight = false;
for (var i = 0; i < this.content.length; i++) {
switch (this.content[i].type) {
case 'COMMENT_THREAD':
this._commentThreadLineNums.push(
this.content[i].comments[0].line);
break;
case 'CODE':
// Only grab the first line of the highlighted chunk.
if (!inHighlight && this.content[i].highlight) {
this._diffChunkLineNums.push(this.content[i].lineNum);
inHighlight = true;
} else if (!this.content[i].highlight) {
inHighlight = false;
}
break;
}
}
},
_updateDOMIndices: function() {
// There is no way to select elements with a data-index greater than a
// given value. For now, just update all DOM elements.
var lineEls = Polymer.dom(this.root).querySelectorAll(
'.numbers [data-index]');
var contentEls = Polymer.dom(this.root).querySelectorAll(
'.content [data-index]');
if (lineEls.length != contentEls.length) {
throw Error(
'There must be the same number of line and content elements');
}
var index = 0;
for (var i = 0; i < this.content.length; i++) {
if (this.content[i].hidden) { continue; }
lineEls[index].setAttribute('data-index', i);
contentEls[index].setAttribute('data-index', i);
index++;
}
},
_prefsChanged: function(changeRecord) {
var prefs = changeRecord.base;
this.$.content.style.width = prefs.line_length + 'ch';
},
_projectConfigChanged: function(projectConfig) {
var threadEls =
Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
for (var i = 0; i < threadEls.length; i++) {
threadEls[i].projectConfig = projectConfig;
}
},
_contentChanged: function(diff) {
this._clearChildren(this.$.numbers);
this._clearChildren(this.$.content);
this._render(diff, 0, diff.length - 1);
this._updateJumpIndices();
},
_computeContainerClass: function(canComment) {
return 'container' + (canComment ? ' canComment' : '');
},
_tapHandler: function(e) {
var lineEl = Polymer.dom(e).rootTarget;
if (!this.canComment || !lineEl.classList.contains('lineNum')) {
return;
}
e.preventDefault();
var index = parseInt(lineEl.getAttribute('data-index'), 10);
var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
this.fire('add-draft', {
index: index,
line: line
}, {bubbles: false});
},
_clearChildren: function(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
},
_handleContextControlClick: function(context, e) {
e.preventDefault();
this.fire('expand-context', {context: context}, {bubbles: false});
},
_render: function(diff, startIndex, endIndex) {
var beforeLineEl;
var beforeContentEl;
if (endIndex != diff.length - 1) {
beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
if (!beforeLineEl && !beforeContentEl) {
// `endIndex` may be present within the model, but not in the DOM.
// Insert it before its successive element.
beforeLineEl = this.$$(
'.numbers [data-index="' + (endIndex + 1) + '"]');
beforeContentEl = this.$$(
'.content [data-index="' + (endIndex + 1) + '"]');
}
}
for (var i = startIndex; i <= endIndex; i++) {
if (diff[i].hidden) { continue; }
switch (diff[i].type) {
case 'CODE':
this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
break;
case 'FILLER':
this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
break;
case 'CONTEXT_CONTROL':
this._renderContextControl(diff[i], i, beforeLineEl,
beforeContentEl);
break;
case 'COMMENT_THREAD':
this._renderCommentThread(diff[i], i, beforeLineEl,
beforeContentEl);
break;
}
}
},
_handleCommentThreadHeightChange: function(e) {
var threadEl = Polymer.dom(e).rootTarget;
var index = parseInt(threadEl.getAttribute('data-index'), 10);
this.content[index].height = e.detail.height;
var lineEl = this.$$('.numbers [data-index="' + index + '"]');
lineEl.style.height = e.detail.height + 'px';
this.fire('thread-height-change', {
index: index,
height: e.detail.height,
}, {bubbles: false});
},
_handleCommentThreadDiscard: function(e) {
var threadEl = Polymer.dom(e).rootTarget;
var index = parseInt(threadEl.getAttribute('data-index'), 10);
this.fire('remove-thread', {index: index}, {bubbles: false});
},
_renderCommentThread: function(thread, index, beforeLineEl,
beforeContentEl) {
var lineEl = this._createElement('div', 'commentThread');
lineEl.classList.add('filler');
lineEl.setAttribute('data-index', index);
var threadEl = document.createElement('gr-diff-comment-thread');
threadEl.addEventListener('height-change',
this._handleCommentThreadHeightChange.bind(this));
threadEl.addEventListener('discard',
this._handleCommentThreadDiscard.bind(this));
threadEl.setAttribute('data-index', index);
threadEl.changeNum = this.changeNum;
threadEl.patchNum = thread.patchNum || this.patchNum;
threadEl.path = this.path;
threadEl.comments = thread.comments;
threadEl.showActions = this.canComment;
threadEl.projectConfig = this.projectConfig;
this.$.numbers.insertBefore(lineEl, beforeLineEl);
this.$.content.insertBefore(threadEl, beforeContentEl);
},
_renderContextControl: function(control, index, beforeLineEl,
beforeContentEl) {
var lineEl = this._createElement('div', 'contextControl');
lineEl.setAttribute('data-index', index);
lineEl.textContent = '@@';
var contentEl = this._createElement('div', 'contextControl');
contentEl.setAttribute('data-index', index);
var a = this._createElement('a');
a.href = '#';
a.textContent = 'Show ' + control.numLines + ' common ' +
(control.numLines == 1 ? 'line' : 'lines') + '...';
a.addEventListener('click',
this._handleContextControlClick.bind(this, control));
contentEl.appendChild(a);
this.$.numbers.insertBefore(lineEl, beforeLineEl);
this.$.content.insertBefore(contentEl, beforeContentEl);
},
_renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
var lineFillerEl = this._createElement('div', 'filler');
lineFillerEl.setAttribute('data-index', index);
var fillerEl = this._createElement('div', 'filler');
fillerEl.setAttribute('data-index', index);
var numLines = filler.numLines || 1;
lineFillerEl.textContent = '\n'.repeat(numLines);
for (var i = 0; i < numLines; i++) {
var newlineEl = this._createElement('span', 'br');
fillerEl.appendChild(newlineEl);
}
this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
this.$.content.insertBefore(fillerEl, beforeContentEl);
},
_renderCode: function(code, index, beforeLineEl, beforeContentEl) {
var lineNumEl = this._createElement('div', 'lineNum');
lineNumEl.setAttribute('data-line-num', code.lineNum);
lineNumEl.setAttribute('data-index', index);
var numLines = code.numLines || 1;
lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
var contentEl = this._createElement('div', 'code');
contentEl.setAttribute('data-line-num', code.lineNum);
contentEl.setAttribute('data-index', index);
if (code.highlight) {
contentEl.classList.add(code.intraline.length > 0 ?
'lightHighlight' : 'darkHighlight');
}
var html = util.escapeHTML(code.content);
if (code.highlight && code.intraline.length > 0) {
html = this._addIntralineHighlights(code.content, html,
code.intraline);
}
if (numLines > 1) {
html = this._addNewLines(code.content, html, numLines);
}
html = this._addTabWrappers(code.content, html);
// If the html is equivalent to the text then it didn't get highlighted
// or escaped. Use textContent which is faster than innerHTML.
if (code.content == html) {
contentEl.textContent = code.content;
} else {
contentEl.innerHTML = html;
}
this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
this.$.content.insertBefore(contentEl, beforeContentEl);
},
// Advance `index` by the appropriate number of characters that would
// represent one source code character and return that index. For
// example, for source code '<span>' the escaped html string is
// '&lt;span&gt;'. Advancing from index 0 on the prior html string would
// return 4, since &lt; maps to one source code character ('<').
_advanceChar: function(html, index) {
// Any tags don't count as characters
while (index < html.length &&
html.charCodeAt(index) == CharCode.LESS_THAN) {
while (index < html.length &&
html.charCodeAt(index) != CharCode.GREATER_THAN) {
index++;
}
index++; // skip the ">" itself
}
// An HTML entity (e.g., &lt;) counts as one character.
if (index < html.length &&
html.charCodeAt(index) == CharCode.AMPERSAND) {
while (index < html.length &&
html.charCodeAt(index) != CharCode.SEMICOLON) {
index++;
}
}
return index + 1;
},
_addIntralineHighlights: function(content, html, highlights) {
var startTag = this._highlightStartTag;
var endTag = this._highlightEndTag;
for (var i = 0; i < highlights.length; i++) {
var hl = highlights[i];
var htmlStartIndex = 0;
for (var j = 0; j < hl.startIndex; j++) {
htmlStartIndex = this._advanceChar(html, htmlStartIndex);
}
var htmlEndIndex = 0;
if (hl.endIndex != null) {
for (var j = 0; j < hl.endIndex; j++) {
htmlEndIndex = this._advanceChar(html, htmlEndIndex);
}
} else {
// If endIndex isn't present, continue to the end of the line.
htmlEndIndex = html.length;
}
// The start and end indices could be the same if a highlight is meant
// to start at the end of a line and continue onto the next one.
// Ignore it.
if (htmlStartIndex != htmlEndIndex) {
html = html.slice(0, htmlStartIndex) + startTag +
html.slice(htmlStartIndex, htmlEndIndex) + endTag +
html.slice(htmlEndIndex);
}
}
return html;
},
_addNewLines: function(content, html, numLines) {
var htmlIndex = 0;
var indices = [];
var numChars = 0;
for (var i = 0; i < content.length; i++) {
if (numChars > 0 && numChars % this.prefs.line_length == 0) {
indices.push(htmlIndex);
}
htmlIndex = this._advanceChar(html, htmlIndex);
if (content[i] == '\t') {
numChars += this.prefs.tab_size;
} else {
numChars++;
}
}
var result = html;
var linesLeft = numLines;
// Since the result string is being altered in place, start from the end
// of the string so that the insertion indices are not affected as the
// result string changes.
for (var i = indices.length - 1; i >= 0; i--) {
result = result.slice(0, indices[i]) + this._lineFeedHTML +
result.slice(indices[i]);
linesLeft--;
}
// numLines is the total number of lines this code block should take up.
// Fill in the remaining ones.
for (var i = 0; i < linesLeft; i++) {
result += this._lineFeedHTML;
}
return result;
},
_addTabWrappers: function(content, html) {
// TODO(andybons): CSS tab-size is not supported in IE.
// Force this to be a number to prevent arbitrary injection.
var tabSize = +this.prefs.tab_size;
var htmlStr = '<span class="style-scope gr-diff-side tab ' +
(this.prefs.show_tabs ? 'withIndicator" ' : '" ') +
'style="tab-size:' + tabSize + ';' +
'-moz-tab-size:' + tabSize + ';">\t</span>';
return html.replace(TAB_REGEX, htmlStr);
},
_createElement: function(tagName, className) {
var el = document.createElement(tagName);
// When Shady DOM is being used, these classes are added to account for
// Polymer's polyfill behavior. In order to guarantee sufficient
// specificity within the CSS rules, these are added to every element.
// Since the Polymer DOM utility functions (which would do this
// automatically) are not being used for performance reasons, this is
// done manually.
el.classList.add('style-scope', 'gr-diff-side');
if (!!className) {
el.classList.add(className);
}
return el;
},
});
})();

View File

@@ -18,12 +18,12 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-side</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-diff-side.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-side.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,174 @@
<!--
Copyright (C) 2015 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-dropdown/iron-dropdown.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<link rel="import" href="../gr-diff/gr-diff.html">
<dom-module id="gr-diff-view">
<template>
<style>
:host {
background-color: var(--view-background-color);
display: block;
}
h3 {
margin-top: 1em;
padding: .75em var(--default-horizontal-margin);
}
.reviewed {
display: inline-block;
margin: 0 .25em;
vertical-align: .15em;
}
.jumpToFileContainer {
display: inline-block;
}
.mobileJumpToFileContainer {
display: none;
}
.downArrow {
display: inline-block;
font-size: .6em;
vertical-align: middle;
}
.dropdown-trigger {
color: #00e;
cursor: pointer;
padding: 0;
}
.dropdown-content {
background-color: #fff;
box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
}
.dropdown-content a {
cursor: pointer;
display: block;
font-weight: normal;
padding: .3em .5em;
}
.dropdown-content a:before {
color: #ccc;
content: attr(data-key-nav);
display: inline-block;
margin-right: .5em;
width: .3em;
}
.dropdown-content a:hover {
background-color: #00e;
color: #fff;
}
.dropdown-content a[selected] {
color: #000;
font-weight: bold;
pointer-events: none;
text-decoration: none;
}
.dropdown-content a[selected]:hover {
background-color: #fff;
color: #000;
}
gr-button {
font: inherit;
padding: .3em 0;
text-decoration: none;
}
@media screen and (max-width: 50em) {
.dash {
display: none;
}
.reviewed {
vertical-align: -.1em;
}
.jumpToFileContainer {
display: none;
}
.mobileJumpToFileContainer {
display: block;
width: 100%;
}
}
</style>
<gr-ajax id="changeDetailXHR"
auto
url="[[_computeChangeDetailPath(_changeNum)]]"
params="[[_computeChangeDetailQueryParams()]]"
last-response="{{_change}}"></gr-ajax>
<gr-ajax id="filesXHR"
auto
url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
on-response="_handleFilesResponse"></gr-ajax>
<gr-ajax id="configXHR"
auto
url="[[_computeProjectConfigPath(_change.project)]]"
last-response="{{_projectConfig}}"></gr-ajax>
<h3>
<a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]">
[[_changeNum]]</a><span>:</span>
<span>[[_change.subject]]</span>
<span class="dash"></span>
<input id="reviewed"
class="reviewed"
type="checkbox"
on-change="_handleReviewedChange"
hidden$="[[!_loggedIn]]" hidden>
<div class="jumpToFileContainer">
<gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
<span>[[_computeFileDisplayName(_path)]]</span>
<span class="downArrow">&#9660;</span>
</gr-button>
<iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
<div class="dropdown-content">
<template is="dom-repeat" items="[[_fileList]]" as="path">
<a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]"
selected$="[[_computeFileSelected(path, _path)]]"
data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
on-tap="_handleFileTap">
[[_computeFileDisplayName(path)]]
</a>
</template>
</div>
</iron-dropdown>
</div>
<div class="mobileJumpToFileContainer">
<select on-change="_handleMobileSelectChange">
<template is="dom-repeat" items="[[_fileList]]" as="path">
<option
value$="[[path]]"
selected$="[[_computeFileSelected(path, _path)]]">
[[_computeFileDisplayName(path)]]
</option>
</template>
</select>
</div>
</h3>
<gr-diff id="diff"
change-num="[[_changeNum]]"
prefs="{{prefs}}"
patch-range="[[_patchRange]]"
path="[[_path]]"
project-config="[[_projectConfig]]"
available-patches="[[_computeAvailablePatches(_change.revisions)]]"
on-render="_handleDiffRender">
</gr-diff>
</template>
<script src="gr-diff-view.js"></script>
</dom-module>

View File

@@ -0,0 +1,315 @@
// 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 COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
Polymer({
is: 'gr-diff-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
prefs: {
type: Object,
notify: true,
},
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
changeViewState: {
type: Object,
notify: true,
value: function() { return {}; },
},
_patchRange: Object,
_change: Object,
_changeNum: String,
_diff: Object,
_fileList: {
type: Array,
value: function() { return []; },
},
_path: {
type: String,
observer: '_pathChanged',
},
_loggedIn: {
type: Boolean,
value: false,
},
_xhrPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
if (this._loggedIn) {
this._setReviewed(true);
}
}.bind(this));
},
attached: function() {
if (this._path) {
this.fire('title-change',
{title: this._computeFileDisplayName(this._path)});
}
window.addEventListener('resize', this._boundWindowResizeHandler);
},
detached: function() {
window.removeEventListener('resize', this._boundWindowResizeHandler);
},
_handleReviewedChange: function(e) {
this._setReviewed(Polymer.dom(e).rootTarget.checked);
},
_setReviewed: function(reviewed) {
this.$.reviewed.checked = reviewed;
var method = reviewed ? 'PUT' : 'DELETE';
var url = this.changeBaseURL(this._changeNum,
this._patchRange.patchNum) + '/files/' +
encodeURIComponent(this._path) + '/reviewed';
this._send(method, url).catch(function(err) {
alert('Couldnt change file review status. Check the console ' +
'and contact the PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 219: // '['
e.preventDefault();
this._navToFile(this._fileList, -1);
break;
case 221: // ']'
e.preventDefault();
this._navToFile(this._fileList, 1);
break;
case 78: // 'n'
if (e.shiftKey) {
this.$.diff.scrollToNextCommentThread();
} else {
this.$.diff.scrollToNextDiffChunk();
}
break;
case 80: // 'p'
if (e.shiftKey) {
this.$.diff.scrollToPreviousCommentThread();
} else {
this.$.diff.scrollToPreviousDiffChunk();
}
break;
case 65: // 'a'
if (!this._loggedIn) { return; }
this.set('changeViewState.showReplyDialog', true);
/* falls through */ // required by JSHint
case 85: // 'u'
if (this._changeNum && this._patchRange.patchNum) {
e.preventDefault();
page.show(this._computeChangePath(
this._changeNum,
this._patchRange.patchNum,
this._change && this._change.revisions));
}
break;
case 188: // ','
this.$.diff.showDiffPreferences();
break;
}
},
_handleDiffRender: function() {
if (window.location.hash.length > 0) {
this.$.diff.scrollToLine(
parseInt(window.location.hash.substring(1), 10));
}
},
_navToFile: function(fileList, direction) {
if (fileList.length == 0) { return; }
var idx = fileList.indexOf(this._path) + direction;
if (idx < 0 || idx > fileList.length - 1) {
page.show(this._computeChangePath(
this._changeNum,
this._patchRange.patchNum,
this._change && this._change.revisions));
return;
}
page.show(this._computeDiffURL(this._changeNum,
this._patchRange,
fileList[idx]));
},
_paramsChanged: function(value) {
if (value.view != this.tagName.toLowerCase()) { return; }
this._changeNum = value.changeNum;
this._patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum || 'PARENT',
};
this._path = value.path;
this.fire('title-change',
{title: this._computeFileDisplayName(this._path)});
// When navigating away from the page, there is a possibility that the
// patch number is no longer a part of the URL (say when navigating to
// the top-level change info view) and therefore undefined in `params`.
if (!this._patchRange.patchNum) {
return;
}
this.$.diff.reload();
},
_pathChanged: function(path) {
if (this._fileList.length == 0) { return; }
this.set('changeViewState.selectedFileIndex',
this._fileList.indexOf(path));
if (this._loggedIn) {
this._setReviewed(true);
}
},
_computeDiffURL: function(changeNum, patchRange, path) {
var patchStr = patchRange.patchNum;
if (patchRange.basePatchNum != null &&
patchRange.basePatchNum != 'PARENT') {
patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
}
return '/c/' + changeNum + '/' + patchStr + '/' + path;
},
_computeAvailablePatches: function(revisions) {
var patchNums = [];
for (var rev in revisions) {
patchNums.push(revisions[rev]._number);
}
return patchNums.sort(function(a, b) { return a - b; });
},
_computeChangePath: function(changeNum, patchNum, revisions) {
var base = '/c/' + changeNum + '/';
// The change may not have loaded yet, making revisions unavailable.
if (!revisions) {
return base + patchNum;
}
var latestPatchNum = -1;
for (var rev in revisions) {
latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number);
}
if (parseInt(patchNum, 10) != latestPatchNum) {
return base + patchNum;
}
return base;
},
_computeFileDisplayName: function(path) {
return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
},
_computeChangeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeChangeDetailQueryParams: function() {
return {O: this.listChangesOptionsToHex(
this.ListChangesOption.ALL_REVISIONS
)};
},
_computeFilesPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/files';
},
_computeProjectConfigPath: function(project) {
return '/projects/' + encodeURIComponent(project) + '/config';
},
_computeFileSelected: function(path, currentPath) {
return path == currentPath;
},
_computeKeyNav: function(path, selectedPath, fileList) {
var selectedIndex = fileList.indexOf(selectedPath);
if (fileList.indexOf(path) == selectedIndex - 1) {
return '[';
}
if (fileList.indexOf(path) == selectedIndex + 1) {
return ']';
}
return '';
},
_handleFileTap: function(e) {
this.$.dropdown.close();
},
_handleMobileSelectChange: function(e) {
var path = Polymer.dom(e).rootTarget.value;
page.show(
this._computeDiffURL(this._changeNum, this._patchRange, path));
},
_handleFilesResponse: function(e, req) {
this._fileList = Object.keys(e.detail.response).sort();
},
_showDropdownTapHandler: function(e) {
this.$.dropdown.open();
},
_send: function(method, url) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: url,
});
return this._xhrPromise;
},
});
})();

View File

@@ -18,14 +18,14 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-view</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-diff-view.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-view.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,123 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../gr-diff-side/gr-diff-side.html">
<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
<dom-module id="gr-diff">
<template>
<style>
.loading {
padding: 0 var(--default-horizontal-margin) 1em;
color: #666;
}
.header {
display: flex;
justify-content: space-between;
margin: 0 var(--default-horizontal-margin) .75em;
}
.prefsButton {
text-align: right;
}
.diffContainer {
border-bottom: 1px solid #eee;
border-top: 1px solid #eee;
display: flex;
font: 12px var(--monospace-font-family);
overflow-x: auto;
}
gr-diff-side:first-of-type {
--light-highlight-color: #fee;
--dark-highlight-color: #ffd4d4;
}
gr-diff-side:last-of-type {
--light-highlight-color: #efe;
--dark-highlight-color: #d4ffd4;
border-right: 1px solid #ddd;
}
</style>
<gr-ajax id="diffXHR"
url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
last-response="{{_diffResponse}}"
loading="{{_loading}}"></gr-ajax>
<gr-ajax id="baseCommentsXHR"
url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
<gr-ajax id="commentsXHR"
url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
<gr-ajax id="baseDraftsXHR"
url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
<div class="loading" hidden$="[[!_loading]]">Loading...</div>
<div hidden$="[[_loading]]" hidden>
<div class="header">
<gr-patch-range-select
path="[[path]]"
change-num="[[changeNum]]"
patch-range="[[patchRange]]"
available-patches="[[availablePatches]]"></gr-patch-range-select>
<gr-button link
class="prefsButton"
on-tap="_handlePrefsTap"
hidden$="[[!prefs]]"
hidden>Diff View Preferences</gr-button>
</div>
<gr-overlay id="prefsOverlay" with-backdrop>
<gr-diff-preferences
prefs="{{prefs}}"
on-save="_handlePrefsSave"
on-cancel="_handlePrefsCancel"></gr-diff-preferences>
</gr-overlay>
<div class="diffContainer">
<gr-diff-side id="leftDiff"
change-num="[[changeNum]]"
patch-num="[[patchRange.basePatchNum]]"
path="[[path]]"
content="{{_diff.leftSide}}"
prefs="[[prefs]]"
can-comment="[[_loggedIn]]"
project-config="[[projectConfig]]"
on-expand-context="_handleExpandContext"
on-thread-height-change="_handleThreadHeightChange"
on-add-draft="_handleAddDraft"
on-remove-thread="_handleRemoveThread"></gr-diff-side>
<gr-diff-side id="rightDiff"
change-num="[[changeNum]]"
patch-num="[[patchRange.patchNum]]"
path="[[path]]"
content="{{_diff.rightSide}}"
prefs="[[prefs]]"
can-comment="[[_loggedIn]]"
project-config="[[projectConfig]]"
on-expand-context="_handleExpandContext"
on-thread-height-change="_handleThreadHeightChange"
on-add-draft="_handleAddDraft"
on-remove-thread="_handleRemoveThread"></gr-diff-side>
</div>
</div>
</template>
<script src="gr-diff.js"></script>
</dom-module>

View File

@@ -0,0 +1,746 @@
// 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-diff',
/**
* Fired when the diff is rendered.
*
* @event render
*/
properties: {
availablePatches: Array,
changeNum: String,
/*
* A single object to encompass basePatchNum and patchNum is used
* so that both can be set at once without incremental observers
* firing after each property changes.
*/
patchRange: Object,
path: String,
prefs: {
type: Object,
notify: true,
},
projectConfig: Object,
_prefsReady: {
type: Object,
readOnly: true,
value: function() {
return new Promise(function(resolve) {
this._resolvePrefsReady = resolve;
}.bind(this));
},
},
_baseComments: Array,
_comments: Array,
_drafts: Array,
_baseDrafts: Array,
/**
* Base (left side) comments and drafts grouped by line number.
* Only used for initial rendering.
*/
_groupedBaseComments: {
type: Object,
value: function() { return {}; },
},
/**
* Comments and drafts (right side) grouped by line number.
* Only used for initial rendering.
*/
_groupedComments: {
type: Object,
value: function() { return {}; },
},
_diffResponse: Object,
_diff: {
type: Object,
value: function() { return {}; },
},
_loggedIn: {
type: Boolean,
value: false,
},
_initialRenderComplete: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: true,
},
_savedPrefs: Object,
_diffRequestsPromise: Object, // Used for testing.
_diffPreferencesPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_prefsChanged(prefs.*)',
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
}.bind(this));
},
scrollToLine: function(lineNum) {
// TODO(andybons): Should this always be the right side?
this.$.rightDiff.scrollToLine(lineNum);
},
scrollToNextDiffChunk: function() {
this.$.rightDiff.scrollToNextDiffChunk();
},
scrollToPreviousDiffChunk: function() {
this.$.rightDiff.scrollToPreviousDiffChunk();
},
scrollToNextCommentThread: function() {
this.$.rightDiff.scrollToNextCommentThread();
},
scrollToPreviousCommentThread: function() {
this.$.rightDiff.scrollToPreviousCommentThread();
},
reload: function(changeNum, patchRange, path) {
// If a diff takes a considerable amount of time to render, the previous
// diff can end up showing up while the DOM is constructed. Clear the
// content on a reload to prevent this.
this._diff = {
leftSide: [],
rightSide: [],
};
var promises = [
this._prefsReady,
this.$.diffXHR.generateRequest().completes
];
var basePatchNum = this.patchRange.basePatchNum;
return app.accountReady.then(function() {
promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
this._diffRequestsPromise = Promise.all(promises).then(function() {
this._render();
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
}.bind(this));
},
showDiffPreferences: function() {
this.$.prefsOverlay.open();
},
_prefsChanged: function(changeRecord) {
if (this._initialRenderComplete) {
this._render();
}
this._resolvePrefsReady(changeRecord.base);
},
_render: function() {
this._groupCommentsAndDrafts();
this._processContent();
// Allow for the initial rendering to complete before firing the event.
this.async(function() {
this.fire('render', null, {bubbles: false});
}.bind(this), 1);
this._initialRenderComplete = true;
},
_getCommentsAndDrafts: function(basePatchNum, loggedIn) {
function onlyParent(c) { return c.side == 'PARENT'; }
function withoutParent(c) { return c.side != 'PARENT'; }
var promises = [];
var commentsPromise = this.$.commentsXHR.generateRequest().completes;
promises.push(commentsPromise.then(function(req) {
var comments = req.response[this.path] || [];
if (basePatchNum == 'PARENT') {
this._baseComments = comments.filter(onlyParent);
}
this._comments = comments.filter(withoutParent);
}.bind(this)));
if (basePatchNum != 'PARENT') {
commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
promises.push(commentsPromise.then(function(req) {
this._baseComments =
(req.response[this.path] || []).filter(withoutParent);
}.bind(this)));
}
if (!loggedIn) {
this._baseDrafts = [];
this._drafts = [];
return Promise.all(promises);
}
var draftsPromise = this.$.draftsXHR.generateRequest().completes;
promises.push(draftsPromise.then(function(req) {
var drafts = req.response[this.path] || [];
if (basePatchNum == 'PARENT') {
this._baseDrafts = drafts.filter(onlyParent);
}
this._drafts = drafts.filter(withoutParent);
}.bind(this)));
if (basePatchNum != 'PARENT') {
draftsPromise = this.$.baseDraftsXHR.generateRequest().completes;
promises.push(draftsPromise.then(function(req) {
this._baseDrafts =
(req.response[this.path] || []).filter(withoutParent);
}.bind(this)));
}
return Promise.all(promises);
},
_computeDiffPath: function(changeNum, patchNum, path) {
return this.changeBaseURL(changeNum, patchNum) + '/files/' +
encodeURIComponent(path) + '/diff';
},
_computeCommentsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/comments';
},
_computeDraftsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/drafts';
},
_computeDiffQueryParams: function(basePatchNum) {
var params = {
context: 'ALL',
intraline: null
};
if (basePatchNum != 'PARENT') {
params.base = basePatchNum;
}
return params;
},
_handlePrefsTap: function(e) {
e.preventDefault();
// TODO(andybons): This is not supported in IE. Implement a polyfill.
// NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
// an object as a value, it must be marked enumerable.
this._savedPrefs = Object.assign({}, this.prefs);
this.$.prefsOverlay.open();
},
_handlePrefsSave: function(e) {
e.stopPropagation();
var el = Polymer.dom(e).rootTarget;
el.disabled = true;
app.accountReady.then(function() {
if (!this._loggedIn) {
el.disabled = false;
this.$.prefsOverlay.close();
return;
}
this._saveDiffPreferences().then(function() {
this.$.prefsOverlay.close();
el.disabled = false;
}.bind(this)).catch(function(err) {
el.disabled = false;
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
}.bind(this));
},
_saveDiffPreferences: function() {
var xhr = document.createElement('gr-request');
this._diffPreferencesPromise = xhr.send({
method: 'PUT',
url: '/accounts/self/preferences.diff',
body: this.prefs,
});
return this._diffPreferencesPromise;
},
_handlePrefsCancel: function(e) {
e.stopPropagation();
this.prefs = this._savedPrefs;
this.$.prefsOverlay.close();
},
_handleExpandContext: function(e) {
var ctx = e.detail.context;
var contextControlIndex = -1;
for (var i = ctx.start; i <= ctx.end; i++) {
this._diff.leftSide[i].hidden = false;
this._diff.rightSide[i].hidden = false;
if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
contextControlIndex = i;
}
}
this._diff.leftSide[contextControlIndex].hidden = true;
this._diff.rightSide[contextControlIndex].hidden = true;
this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
},
_handleThreadHeightChange: function(e) {
var index = e.detail.index;
var diffEl = Polymer.dom(e).rootTarget;
var otherSide = diffEl == this.$.leftDiff ?
this.$.rightDiff : this.$.leftDiff;
var threadHeight = e.detail.height;
var otherSideHeight;
if (otherSide.content[index].type == 'COMMENT_THREAD') {
otherSideHeight = otherSide.getRowNaturalHeight(index);
} else {
otherSideHeight = otherSide.getRowHeight(index);
}
var maxHeight = Math.max(threadHeight, otherSideHeight);
this.$.leftDiff.setRowHeight(index, maxHeight);
this.$.rightDiff.setRowHeight(index, maxHeight);
},
_handleAddDraft: function(e) {
var insertIndex = e.detail.index + 1;
var diffEl = Polymer.dom(e).rootTarget;
var content = diffEl.content;
if (content[insertIndex] &&
content[insertIndex].type == 'COMMENT_THREAD') {
// A thread is already here. Do nothing.
return;
}
var comment = {
type: 'COMMENT_THREAD',
comments: [{
__draft: true,
__draftID: Math.random().toString(36),
line: e.detail.line,
path: this.path,
}]
};
if (diffEl == this.$.leftDiff &&
this.patchRange.basePatchNum == 'PARENT') {
comment.comments[0].side = 'PARENT';
comment.patchNum = this.patchRange.patchNum;
}
if (content[insertIndex] &&
content[insertIndex].type == 'FILLER') {
content[insertIndex] = comment;
diffEl.rowUpdated(insertIndex);
} else {
content.splice(insertIndex, 0, comment);
diffEl.rowInserted(insertIndex);
}
var otherSide = diffEl == this.$.leftDiff ?
this.$.rightDiff : this.$.leftDiff;
if (otherSide.content[insertIndex] == null ||
otherSide.content[insertIndex].type != 'COMMENT_THREAD') {
otherSide.content.splice(insertIndex, 0, {
type: 'FILLER',
});
otherSide.rowInserted(insertIndex);
}
},
_handleRemoveThread: function(e) {
var diffEl = Polymer.dom(e).rootTarget;
var otherSide = diffEl == this.$.leftDiff ?
this.$.rightDiff : this.$.leftDiff;
var index = e.detail.index;
if (otherSide.content[index].type == 'FILLER') {
otherSide.content.splice(index, 1);
otherSide.rowRemoved(index);
diffEl.content.splice(index, 1);
diffEl.rowRemoved(index);
} else if (otherSide.content[index].type == 'COMMENT_THREAD') {
diffEl.content[index] = {type: 'FILLER'};
diffEl.rowUpdated(index);
var height = otherSide.setRowNaturalHeight(index);
diffEl.setRowHeight(index, height);
} else {
throw Error('A thread cannot be opposite anything but filler or ' +
'another thread');
}
},
_processContent: function() {
var leftSide = [];
var rightSide = [];
var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
var ctx = {
hidingLines: false,
lastNumLinesHidden: 0,
left: {
lineNum: initialLineNum,
},
right: {
lineNum: initialLineNum,
}
};
var content = this._breakUpCommonChunksWithComments(ctx,
this._diffResponse.content);
var context = this.prefs.context;
if (context == -1) {
// Show the entire file.
context = Infinity;
}
for (var i = 0; i < content.length; i++) {
if (i == 0) {
ctx.skipRange = [0, context];
} else if (i == content.length - 1) {
ctx.skipRange = [context, 0];
} else {
ctx.skipRange = [context, context];
}
ctx.diffChunkIndex = i;
this._addDiffChunk(ctx, content[i], leftSide, rightSide);
}
this._diff = {
leftSide: leftSide,
rightSide: rightSide,
};
},
// In order to show comments out of the bounds of the selected context,
// treat them as diffs within the model so that the content (and context
// surrounding it) renders correctly.
_breakUpCommonChunksWithComments: function(ctx, content) {
var result = [];
var leftLineNum = ctx.left.lineNum;
var rightLineNum = ctx.right.lineNum;
for (var i = 0; i < content.length; i++) {
if (!content[i].ab) {
result.push(content[i]);
if (content[i].a) {
leftLineNum += content[i].a.length;
}
if (content[i].b) {
rightLineNum += content[i].b.length;
}
continue;
}
var chunk = content[i].ab;
var currentChunk = {ab: []};
for (var j = 0; j < chunk.length; j++) {
leftLineNum++;
rightLineNum++;
if (this._groupedBaseComments[leftLineNum] == null &&
this._groupedComments[rightLineNum] == null) {
currentChunk.ab.push(chunk[j]);
} else {
if (currentChunk.ab && currentChunk.ab.length > 0) {
result.push(currentChunk);
currentChunk = {ab: []};
}
// Append an annotation to indicate that this line should not be
// highlighted even though it's implied with both `a` and `b`
// defined. This is needed since there may be two lines that
// should be highlighted but are equal (blank lines, for example).
result.push({
__noHighlight: true,
a: [chunk[j]],
b: [chunk[j]],
});
}
}
if (currentChunk.ab != null && currentChunk.ab.length > 0) {
result.push(currentChunk);
}
}
return result;
},
_groupCommentsAndDrafts: function() {
this._baseDrafts.forEach(function(d) { d.__draft = true; });
this._drafts.forEach(function(d) { d.__draft = true; });
var allLeft = this._baseComments.concat(this._baseDrafts);
var allRight = this._comments.concat(this._drafts);
var leftByLine = {};
var rightByLine = {};
var mapFunc = function(byLine) {
return function(c) {
// File comments/drafts are grouped with line 1 for now.
var line = c.line || 1;
if (byLine[line] == null) {
byLine[line] = [];
}
byLine[line].push(c);
};
};
allLeft.forEach(mapFunc(leftByLine));
allRight.forEach(mapFunc(rightByLine));
this._groupedBaseComments = leftByLine;
this._groupedComments = rightByLine;
},
_addContextControl: function(ctx, leftSide, rightSide) {
var numLinesHidden = ctx.lastNumLinesHidden;
var leftStart = leftSide.length - numLinesHidden;
var leftEnd = leftSide.length;
var rightStart = rightSide.length - numLinesHidden;
var rightEnd = rightSide.length;
if (leftStart != rightStart || leftEnd != rightEnd) {
throw Error(
'Left and right ranges for context control should be equal:' +
'Left: [' + leftStart + ', ' + leftEnd + '] ' +
'Right: [' + rightStart + ', ' + rightEnd + ']');
}
var obj = {
type: 'CONTEXT_CONTROL',
numLines: numLinesHidden,
start: leftStart,
end: leftEnd,
};
// NOTE: Be careful, here. This object is meant to be immutable. If the
// object is altered within one side's array it will reflect the
// alterations in another.
leftSide.push(obj);
rightSide.push(obj);
},
_addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
for (var i = 0; i < chunk.ab.length; i++) {
var numLines = Math.ceil(
this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
var hidden = i >= ctx.skipRange[0] &&
i < chunk.ab.length - ctx.skipRange[1];
if (ctx.hidingLines && hidden == false) {
// No longer hiding lines. Add a context control.
this._addContextControl(ctx, leftSide, rightSide);
ctx.lastNumLinesHidden = 0;
}
ctx.hidingLines = hidden;
if (hidden) {
ctx.lastNumLinesHidden++;
}
// Blank lines within a diff content array indicate a newline.
leftSide.push({
type: 'CODE',
hidden: hidden,
content: chunk.ab[i] || '\n',
numLines: numLines,
lineNum: ++ctx.left.lineNum,
});
rightSide.push({
type: 'CODE',
hidden: hidden,
content: chunk.ab[i] || '\n',
numLines: numLines,
lineNum: ++ctx.right.lineNum,
});
this._addCommentsIfPresent(ctx, leftSide, rightSide);
}
if (ctx.lastNumLinesHidden > 0) {
this._addContextControl(ctx, leftSide, rightSide);
}
},
_addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
if (chunk.ab) {
this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
return;
}
var leftHighlights = [];
if (chunk.edit_a) {
leftHighlights =
this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
}
var rightHighlights = [];
if (chunk.edit_b) {
rightHighlights =
this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
}
var aLen = (chunk.a && chunk.a.length) || 0;
var bLen = (chunk.b && chunk.b.length) || 0;
var maxLen = Math.max(aLen, bLen);
for (var i = 0; i < maxLen; i++) {
var hasLeftContent = chunk.a && i < chunk.a.length;
var hasRightContent = chunk.b && i < chunk.b.length;
var leftContent = hasLeftContent ? chunk.a[i] : '';
var rightContent = hasRightContent ? chunk.b[i] : '';
var highlight = !chunk.__noHighlight;
var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
if (hasLeftContent) {
leftSide.push({
type: 'CODE',
content: leftContent || '\n',
numLines: maxNumLines,
lineNum: ++ctx.left.lineNum,
highlight: highlight,
intraline: highlight && leftHighlights.filter(function(hl) {
return hl.contentIndex == i;
}),
});
} else {
leftSide.push({
type: 'FILLER',
numLines: maxNumLines,
});
}
if (hasRightContent) {
rightSide.push({
type: 'CODE',
content: rightContent || '\n',
numLines: maxNumLines,
lineNum: ++ctx.right.lineNum,
highlight: highlight,
intraline: highlight && rightHighlights.filter(function(hl) {
return hl.contentIndex == i;
}),
});
} else {
rightSide.push({
type: 'FILLER',
numLines: maxNumLines,
});
}
this._addCommentsIfPresent(ctx, leftSide, rightSide);
}
},
_addCommentsIfPresent: function(ctx, leftSide, rightSide) {
var leftComments = this._groupedBaseComments[ctx.left.lineNum];
var rightComments = this._groupedComments[ctx.right.lineNum];
if (leftComments) {
var thread = {
type: 'COMMENT_THREAD',
comments: leftComments,
};
if (this.patchRange.basePatchNum == 'PARENT') {
thread.patchNum = this.patchRange.patchNum;
}
leftSide.push(thread);
}
if (rightComments) {
rightSide.push({
type: 'COMMENT_THREAD',
comments: rightComments,
});
}
if (leftComments && !rightComments) {
rightSide.push({type: 'FILLER'});
} else if (!leftComments && rightComments) {
leftSide.push({type: 'FILLER'});
}
this._groupedBaseComments[ctx.left.lineNum] = null;
this._groupedComments[ctx.right.lineNum] = null;
},
// The `highlights` array consists of a list of <skip length, mark length>
// pairs, where the skip length is the number of characters between the
// end of the previous edit and the start of this edit, and the mark
// length is the number of edited characters following the skip. The start
// of the edits is from the beginning of the related diff content lines.
//
// Note that the implied newline character at the end of each line is
// included in the length calculation, and thus it is possible for the
// edits to span newlines.
//
// A line highlight object consists of three fields:
// - contentIndex: The index of the diffChunk `content` field (the line
// being referred to).
// - startIndex: Where the highlight should begin.
// - endIndex: (optional) Where the highlight should end. If omitted, the
// highlight is meant to be a continuation onto the next line.
_normalizeIntralineHighlights: function(content, highlights) {
var contentIndex = 0;
var idx = 0;
var normalized = [];
for (var i = 0; i < highlights.length; i++) {
var line = content[contentIndex] + '\n';
var hl = highlights[i];
var j = 0;
while (j < hl[0]) {
if (idx == line.length) {
idx = 0;
line = content[++contentIndex] + '\n';
continue;
}
idx++;
j++;
}
var lineHighlight = {
contentIndex: contentIndex,
startIndex: idx,
};
j = 0;
while (line && j < hl[1]) {
if (idx == line.length) {
idx = 0;
line = content[++contentIndex] + '\n';
normalized.push(lineHighlight);
lineHighlight = {
contentIndex: contentIndex,
startIndex: idx,
};
continue;
}
idx++;
j++;
}
lineHighlight.endIndex = idx;
normalized.push(lineHighlight);
}
return normalized;
},
_visibleLineLength: function(contents) {
// http://jsperf.com/performance-of-match-vs-split
var numTabs = contents.split('\t').length - 1;
return contents.length - numTabs + (this.prefs.tab_size * numTabs);
},
_maxLinesSpanned: function(left, right) {
return Math.max(
Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
},
});
})();

View File

@@ -18,13 +18,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-diff.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff.html">
<test-fixture id="basic">
<template>

View File

@@ -14,7 +14,7 @@ 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/polymer/polymer.html">
<dom-module id="gr-patch-range-select">
<template>
@@ -49,47 +49,5 @@ limitations under the License.
</select>
</span>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-patch-range-select',
properties: {
availablePatches: Array,
changeNum: String,
patchRange: Object,
path: String,
},
_handlePatchChange: function(e) {
var leftPatch = this.$.leftPatchSelect.value;
var rightPatch = this.$.rightPatchSelect.value;
var rangeStr = rightPatch;
if (leftPatch != 'PARENT') {
rangeStr = leftPatch + '..' + rangeStr;
}
page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
},
_computeLeftSelected: function(patchNum, patchRange) {
return patchNum == patchRange.basePatchNum;
},
_computeRightSelected: function(patchNum, patchRange) {
return patchNum == patchRange.patchNum;
},
_computeLeftDisabled: function(patchNum, patchRange) {
return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10);
},
_computeRightDisabled: function(patchNum, patchRange) {
if (patchRange.basePatchNum == 'PARENT') { return false; }
return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10);
},
});
})();
</script>
<script src="gr-patch-range-select.js"></script>
</dom-module>

View File

@@ -0,0 +1,54 @@
// 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-patch-range-select',
properties: {
availablePatches: Array,
changeNum: String,
patchRange: Object,
path: String,
},
_handlePatchChange: function(e) {
var leftPatch = this.$.leftPatchSelect.value;
var rightPatch = this.$.rightPatchSelect.value;
var rangeStr = rightPatch;
if (leftPatch != 'PARENT') {
rangeStr = leftPatch + '..' + rangeStr;
}
page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
},
_computeLeftSelected: function(patchNum, patchRange) {
return patchNum == patchRange.basePatchNum;
},
_computeRightSelected: function(patchNum, patchRange) {
return patchNum == patchRange.patchNum;
},
_computeLeftDisabled: function(patchNum, patchRange) {
return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10);
},
_computeRightDisabled: function(patchNum, patchRange) {
if (patchRange.basePatchNum == 'PARENT') { return false; }
return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10);
},
});
})();

View File

@@ -18,12 +18,12 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-patch-range-select</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../bower_components/page/page.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../elements/gr-patch-range-select.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-patch-range-select.html">
<test-fixture id="basic">
<template>

View File

@@ -1,105 +0,0 @@
<!--
Copyright (C) 2015 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-ajax/iron-ajax.html">
<dom-module id="gr-ajax">
<template>
<iron-ajax id="xhr"
auto="[[auto]]"
url="[[url]]"
params="[[params]]"
json-prefix=")]}'"
last-error="{{lastError}}"
last-response="{{lastResponse}}"
loading="{{loading}}"
on-response="_handleResponse"
on-error="_handleError"
debounce-duration="300"></iron-ajax>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-ajax',
/**
* Fired when a response is received.
*
* @event response
*/
/**
* Fired when an error is received.
*
* @event error
*/
hostAttributes: {
hidden: true
},
properties: {
auto: {
type: Boolean,
value: false,
},
url: String,
params: {
type: Object,
value: function() {
return {};
},
},
lastError: {
type: Object,
notify: true,
},
lastResponse: {
type: Object,
notify: true,
},
loading: {
type: Boolean,
notify: true,
},
},
ready: function() {
// Used for debugging which element a request came from.
var headers = this.$.xhr.headers;
headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)';
this.$.xhr.headers = headers;
},
generateRequest: function() {
return this.$.xhr.generateRequest();
},
_handleResponse: function(e, req) {
this.fire('response', req, {bubbles: false});
},
_handleError: function(e, req) {
this.fire('error', req, {bubbles: false});
},
});
})();
</script>
</dom-module>

View File

@@ -17,15 +17,17 @@ limitations under the License.
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../styles/app-theme.html">
<link rel="import" href="gr-account-dropdown.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-change-list-view.html">
<link rel="import" href="gr-change-view.html">
<link rel="import" href="gr-dashboard-view.html">
<link rel="import" href="gr-diff-view.html">
<link rel="import" href="gr-keyboard-shortcuts-dialog.html">
<link rel="import" href="gr-overlay.html">
<link rel="import" href="gr-search-bar.html">
<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
<link rel="import" href="./change/gr-change-view/gr-change-view.html">
<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
<link rel="import" href="./shared/gr-account-dropdown/gr-account-dropdown.html">
<link rel="import" href="./shared/gr-ajax/gr-ajax.html">
<link rel="import" href="./shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
<link rel="import" href="./shared/gr-overlay/gr-overlay.html">
<link rel="import" href="./shared/gr-search-bar/gr-search-bar.html">
<script src="../bower_components/page/page.js"></script>
<script src="../scripts/app.js"></script>
@@ -166,164 +168,5 @@ limitations under the License.
on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
</gr-overlay>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-app',
properties: {
account: {
type: Object,
observer: '_accountChanged',
},
accountReady: {
type: Object,
readOnly: true,
notify: true,
value: function() {
return new Promise(function(resolve) {
this._resolveAccountReady = resolve;
}.bind(this));
},
},
config: {
type: Object,
observer: '_configChanged',
},
configReady: {
type: Object,
readOnly: true,
notify: true,
value: function() {
return new Promise(function(resolve) {
this._resolveConfigReady = resolve;
}.bind(this));
},
},
version: String,
params: Object,
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_diffPreferences: Object,
_showChangeListView: Boolean,
_showDashboardView: Boolean,
_showChangeView: Boolean,
_showDiffView: Boolean,
_viewState: Object,
},
listeners: {
'title-change': '_handleTitleChange',
},
observers: [
'_viewChanged(params.view)',
],
behaviors: [
Gerrit.KeyboardShortcutBehavior,
],
get loggedIn() {
return !!(this.account && Object.keys(this.account).length > 0);
},
ready: function() {
this._viewState = {
changeView: {
changeNum: null,
patchNum: null,
selectedFileIndex: 0,
showReplyDialog: false,
},
changeListView: {
query: null,
offset: 0,
selectedChangeIndex: 0,
},
dashboardView: {
selectedChangeIndex: 0,
},
};
},
_accountChanged: function() {
this._resolveAccountReady();
this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn);
this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn);
if (this.loggedIn) {
this.$.diffPreferencesXHR.generateRequest();
} else {
// These defaults should match the defaults in
// gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
// NOTE: There are some settings that don't apply to PolyGerrit
// (Render mode being at least one of them).
this._diffPreferences = {
auto_hide_diff_table_header: true,
context: 10,
cursor_blink_rate: 0,
ignore_whitespace: 'IGNORE_NONE',
intraline_difference: true,
line_length: 100,
show_line_endings: true,
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
theme: 'DEFAULT',
};
}
},
_configChanged: function(config) {
this._resolveConfigReady(config);
},
_viewChanged: function(view) {
this.set('_showChangeListView', view == 'gr-change-list-view');
this.set('_showDashboardView', view == 'gr-dashboard-view');
this.set('_showChangeView', view == 'gr-change-view');
this.set('_showDiffView', view == 'gr-diff-view');
},
_loginTapHandler: function(e) {
e.preventDefault();
page.show('/login/' + encodeURIComponent(
window.location.pathname + window.location.hash));
},
_computeLoggedIn: function(account) { // argument used for binding update only
return this.loggedIn;
},
_handleTitleChange: function(e) {
if (e.detail.title) {
document.title = e.detail.title + ' · Gerrit Code Review';
} else {
document.title = '';
}
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 191: // '/' or '?' with shift key.
// TODO(andybons): Localization using e.key/keypress event.
if (!e.shiftKey) { break; }
this.$.keyboardShortcuts.open();
}
},
_handleKeyboardShortcutDialogClose: function() {
this.$.keyboardShortcuts.close();
},
});
})();
</script>
<script src="gr-app.js"></script>
</dom-module>

View File

@@ -0,0 +1,171 @@
// 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-app',
properties: {
account: {
type: Object,
observer: '_accountChanged',
},
accountReady: {
type: Object,
readOnly: true,
notify: true,
value: function() {
return new Promise(function(resolve) {
this._resolveAccountReady = resolve;
}.bind(this));
},
},
config: {
type: Object,
observer: '_configChanged',
},
configReady: {
type: Object,
readOnly: true,
notify: true,
value: function() {
return new Promise(function(resolve) {
this._resolveConfigReady = resolve;
}.bind(this));
},
},
version: String,
params: Object,
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_diffPreferences: Object,
_showChangeListView: Boolean,
_showDashboardView: Boolean,
_showChangeView: Boolean,
_showDiffView: Boolean,
_viewState: Object,
},
listeners: {
'title-change': '_handleTitleChange',
},
observers: [
'_viewChanged(params.view)',
],
behaviors: [
Gerrit.KeyboardShortcutBehavior,
],
get loggedIn() {
return !!(this.account && Object.keys(this.account).length > 0);
},
ready: function() {
this._viewState = {
changeView: {
changeNum: null,
patchNum: null,
selectedFileIndex: 0,
showReplyDialog: false,
},
changeListView: {
query: null,
offset: 0,
selectedChangeIndex: 0,
},
dashboardView: {
selectedChangeIndex: 0,
},
};
},
_accountChanged: function() {
this._resolveAccountReady();
this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn);
this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn);
if (this.loggedIn) {
this.$.diffPreferencesXHR.generateRequest();
} else {
// These defaults should match the defaults in
// gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
// NOTE: There are some settings that don't apply to PolyGerrit
// (Render mode being at least one of them).
this._diffPreferences = {
auto_hide_diff_table_header: true,
context: 10,
cursor_blink_rate: 0,
ignore_whitespace: 'IGNORE_NONE',
intraline_difference: true,
line_length: 100,
show_line_endings: true,
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
theme: 'DEFAULT',
};
}
},
_configChanged: function(config) {
this._resolveConfigReady(config);
},
_viewChanged: function(view) {
this.set('_showChangeListView', view == 'gr-change-list-view');
this.set('_showDashboardView', view == 'gr-dashboard-view');
this.set('_showChangeView', view == 'gr-change-view');
this.set('_showDiffView', view == 'gr-diff-view');
},
_loginTapHandler: function(e) {
e.preventDefault();
page.show('/login/' + encodeURIComponent(
window.location.pathname + window.location.hash));
},
_computeLoggedIn: function(account) { // argument used for binding update only
return this.loggedIn;
},
_handleTitleChange: function(e) {
if (e.detail.title) {
document.title = e.detail.title + ' · Gerrit Code Review';
} else {
document.title = '';
}
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 191: // '/' or '?' with shift key.
// TODO(andybons): Localization using e.key/keypress event.
if (!e.shiftKey) { break; }
this.$.keyboardShortcuts.open();
}
},
_handleKeyboardShortcutDialogClose: function() {
this.$.keyboardShortcuts.close();
},
});
})();

View File

@@ -1,83 +0,0 @@
<!--
Copyright (C) 2015 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">
<dom-module id="gr-avatar">
<template>
<style>
:host {
display: inline-block;
border-radius: 50%;
background-size: cover;
background-color: var(--background-color, #f1f2f3);
}
</style>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-avatar',
properties: {
account: {
type: Object,
observer: '_accountChanged',
},
imageSize: {
type: Number,
value: 16,
},
},
created: function() {
this.hidden = true;
},
ready: function() {
app.configReady.then(function(cfg) {
var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
if (hasAvatars) {
this.hidden = false;
this._updateAvatarURL(this.account); // src needs to be set if avatar becomes visible
}
}.bind(this));
},
_accountChanged: function(account) {
this._updateAvatarURL(account);
},
_updateAvatarURL: function(account) {
if (!this.hidden && account) {
var url = this._buildAvatarURL(this.account);
if (url) {
this.style.backgroundImage = 'url("' + url + '")';
}
}
},
_buildAvatarURL: function(account) {
if (!account) { return ''; }
return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize;
},
});
})();
</script>
</dom-module>

View File

@@ -1,295 +0,0 @@
<!--
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="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-confirm-rebase-dialog.html">
<link rel="import" href="gr-overlay.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-change-actions">
<template>
<style>
:host {
display: block;
}
gr-button {
display: block;
margin-bottom: .5em;
}
gr-button:before {
content: attr(data-label);
}
gr-button[loading]:before {
content: attr(data-loading-label);
}
@media screen and (max-width: 50em) {
.confirmDialog {
width: 90vw;
}
}
</style>
<gr-ajax id="actionsXHR"
url="[[_computeRevisionActionsPath(changeNum, patchNum)]]"
last-response="{{_revisionActions}}"
loading="{{_loading}}"></gr-ajax>
<div>
<template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
hidden$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
on-tap="_handleActionTap"></gr-button>
</template>
<template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
disabled$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
on-tap="_handleActionTap"></gr-button>
</template>
</div>
<gr-overlay id="overlay" with-backdrop>
<gr-confirm-rebase-dialog id="confirmRebase"
class="confirmDialog"
on-confirm="_handleRebaseConfirm"
on-cancel="_handleConfirmDialogCancel"
hidden></gr-confirm-rebase-dialog>
</gr-overlay>
</template>
<script>
(function() {
'use strict';
// TODO(davido): Add the rest of the change actions.
var ChangeActions = {
ABANDON: 'abandon',
DELETE: '/',
RESTORE: 'restore',
};
// TODO(andybons): Add the rest of the revision actions.
var RevisionActions = {
DELETE: '/',
PUBLISH: 'publish',
REBASE: 'rebase',
SUBMIT: 'submit',
};
Polymer({
is: 'gr-change-actions',
/**
* Fired when the change should be reloaded.
*
* @event reload-change
*/
properties: {
actions: {
type: Object,
},
changeNum: String,
patchNum: String,
_loading: {
type: Boolean,
value: true,
},
_revisionActions: Object,
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_actionsChanged(actions, _revisionActions)',
],
reload: function() {
if (!this.changeNum || !this.patchNum) {
return Promise.resolve();
}
return this.$.actionsXHR.generateRequest().completes;
},
_actionsChanged: function(actions, revisionActions) {
this.hidden =
revisionActions.rebase == null &&
revisionActions.submit == null &&
revisionActions.publish == null &&
actions.abandon == null &&
actions.restore == null;
},
_computeRevisionActionsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/actions';
},
_getValuesFor: function(obj) {
return Object.keys(obj).map(function(key) {
return obj[key];
});
},
_computeActionValues: function(actions, type) {
var result = [];
var values = this._getValuesFor(
type == 'change' ? ChangeActions : RevisionActions);
for (var a in actions) {
if (values.indexOf(a) == -1) { continue; }
actions[a].__key = a;
actions[a].__type = type;
result.push(actions[a]);
}
return result;
},
_computeLoadingLabel: function(action) {
return {
'rebase': 'Rebasing...',
'submit': 'Submitting...',
}[action];
},
_computePrimary: function(actionKey) {
return actionKey == 'submit';
},
_computeButtonClass: function(action) {
if ([RevisionActions.SUBMIT,
RevisionActions.PUBLISH].indexOf(action) != -1) {
return 'primary';
}
return '';
},
_handleActionTap: function(e) {
e.preventDefault();
var el = Polymer.dom(e).rootTarget;
var key = el.getAttribute('data-action-key');
var type = el.getAttribute('data-action-type');
if (type == 'revision') {
if (key == RevisionActions.REBASE) {
this._showRebaseDialog();
return;
}
this._fireRevisionAction(this._prependSlash(key),
this._revisionActions[key]);
} else {
this._fireChangeAction(this._prependSlash(key), this.actions[key]);
}
},
_prependSlash: function(key) {
return key == '/' ? key : '/' + key;
},
_handleConfirmDialogCancel: function() {
var dialogEls =
Polymer.dom(this.root).querySelectorAll('.confirmDialog');
for (var i = 0; i < dialogEls.length; i++) {
dialogEls[i].hidden = true;
}
this.$.overlay.close();
},
_handleRebaseConfirm: function() {
var payload = {};
var el = this.$.confirmRebase;
if (el.clearParent) {
// There is a subtle but important difference between setting the base
// to an empty string and omitting it entirely from the payload. An
// empty string implies that the parent should be cleared and the
// change should be rebased on top of the target branch. Leaving out
// the base implies that it should be rebased on top of its current
// parent.
payload.base = '';
} else if (el.base && el.base.length > 0) {
payload.base = el.base;
}
this.$.overlay.close();
el.hidden = false;
this._fireRevisionAction('/rebase', this._revisionActions.rebase,
payload);
},
_fireChangeAction: function(endpoint, action) {
this._send(action.method, {}, endpoint).then(
function() {
// We cant reload a change that was deleted.
if (endpoint == ChangeActions.DELETE) {
page.show('/');
} else {
this.fire('reload-change', null, {bubbles: false});
}
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
},
_fireRevisionAction: function(endpoint, action, opt_payload) {
var buttonEl = this.$$('[data-action-key="' + action.__key + '"]');
buttonEl.setAttribute('loading', true);
buttonEl.disabled = true;
function enableButton() {
buttonEl.removeAttribute('loading');
buttonEl.disabled = false;
}
this._send(action.method, opt_payload, endpoint, true).then(
function() {
this.fire('reload-change', null, {bubbles: false});
enableButton();
}.bind(this)).catch(function(err) {
// TODO(andybons): Handle merge conflict (409 status);
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
enableButton();
throw err;
});
},
_showRebaseDialog: function() {
this.$.confirmRebase.hidden = false;
this.$.overlay.open();
},
_send: function(method, payload, actionEndpoint, revisionAction) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: this.changeBaseURL(this.changeNum,
revisionAction ? this.patchNum : null) + actionEndpoint,
body: payload,
});
return this._xhrPromise;
},
});
})();
</script>
</dom-module>

View File

@@ -1,210 +0,0 @@
<!--
Copyright (C) 2015 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="../styles/gr-change-list-styles.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-account-link.html">
<link rel="import" href="gr-change-star.html">
<link rel="import" href="gr-date-formatter.html">
<dom-module id="gr-change-list-item">
<template>
<style>
:host {
display: flex;
border-bottom: 1px solid #eee;
}
:host([selected]) {
background-color: #ebf5fb;
}
:host([needs-review]) {
font-weight: bold;
}
.cell {
flex-shrink: 0;
padding: .3em .5em;
}
a {
color: var(--default-text-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.positionIndicator {
visibility: hidden;
}
:host([selected]) .positionIndicator {
visibility: visible;
}
.u-monospace {
font-family: var(--monospace-font-family);
}
.u-green {
color: #388E3C;
}
.u-red {
color: #D32F2F;
}
</style>
<style include="gr-change-list-styles"></style>
<span class="cell keyboard">
<span class="positionIndicator">&#x25b6;</span>
</span>
<span class="cell star" hidden$="[[!showStar]]">
<gr-change-star change="{{change}}"></gr-change-star>
</span>
<a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
<span class="cell status">[[_computeChangeStatusString(change)]]</span>
<span class="cell owner">
<gr-account-link account="[[change.owner]]"></gr-account-link>
</span>
<a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
<a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
<gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter>
<span class="cell size u-monospace">
<span class="u-green"><span>+</span>[[change.insertions]]</span>,
<span class="u-red"><span>-</span>[[change.deletions]]</span>
</span>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
<span title$="[[_computeLabelTitle(change, labelName)]]"
class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-change-list-item',
properties: {
selected: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
needsReview: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
labelNames: {
type: Array,
},
change: Object,
changeURL: {
type: String,
computed: '_computeChangeURL(change._number)',
},
showStar: {
type: Boolean,
value: false,
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
_computeChangeURL: function(changeNum) {
if (!changeNum) { return ''; }
return '/c/' + changeNum + '/';
},
_computeChangeStatusString: function(change) {
if (change.status == this.ChangeStatus.MERGED) {
return 'Merged';
}
if (change.mergeable != null && change.mergeable == false) {
return 'Merge Conflict';
}
if (change.status == this.ChangeStatus.DRAFT) {
return 'Draft';
}
if (change.status == this.ChangeStatus.ABANDONED) {
return 'Abandoned';
}
return '';
},
_computeLabelTitle: function(change, labelName) {
var label = change.labels[labelName];
if (!label) { return labelName; }
var significantLabel = label.rejected || label.approved ||
label.disliked || label.recommended;
if (significantLabel && significantLabel.name) {
return labelName + '\nby ' + significantLabel.name;
}
return labelName;
},
_computeLabelClass: function(change, labelName) {
var label = change.labels[labelName];
// Mimic a Set.
var classes = {
'cell': true,
'label': true,
};
if (label) {
if (label.approved) {
classes['u-green'] = true;
}
if (label.value == 1) {
classes['u-monospace'] = true;
classes['u-green'] = true;
} else if (label.value == -1) {
classes['u-monospace'] = true;
classes['u-red'] = true;
}
if (label.rejected) {
classes['u-red'] = true;
}
}
return Object.keys(classes).sort().join(' ');
},
_computeLabelValue: function(change, labelName) {
var label = change.labels[labelName];
if (!label) { return ''; }
if (label.approved) {
return '✓';
}
if (label.rejected) {
return '✕';
}
if (label.value > 0) {
return '+' + label.value;
}
if (label.value < 0) {
return label.value;
}
return '';
},
_computeProjectURL: function(project) {
return '/projects/' + project + ',dashboards/default';
},
_computeProjectBranchURL: function(project, branch) {
return '/q/status:open+project:' + project + '+branch:' + branch;
},
});
})();
</script>
</dom-module>

View File

@@ -1,229 +0,0 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-change-list.html">
<dom-module id="gr-change-list-view">
<template>
<style>
:host {
background-color: var(--view-background-color);
display: block;
margin: 0 var(--default-horizontal-margin);
}
.loading,
.error {
margin-top: 1em;
background-color: #f1f2f3;
}
.loading {
color: #666;
}
.error {
color: #D32F2F;
}
gr-change-list {
margin-top: 1em;
width: 100%;
}
nav {
margin-bottom: 1em;
padding: .5em 0;
text-align: center;
}
nav a {
display: inline-block;
}
nav a:first-of-type {
margin-right: .5em;
}
@media only screen and (max-width: 50em) {
:host {
margin: 0;
}
.loading,
.error {
padding: 0 var(--default-horizontal-margin);
}
}
</style>
<gr-ajax
auto
url="/changes/"
params="[[_computeQueryParams(_query, _offset)]]"
last-response="{{_changes}}"
last-error="{{_lastError}}"
loading="{{_loading}}"></gr-ajax>
<div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
<div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden>
[[_lastError.request.xhr.responseText]]
</div>
<div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden>
<gr-change-list
changes="{{_changes}}"
selected-index="{{viewState.selectedChangeIndex}}"
show-star="[[loggedIn]]"></gr-change-list>
<nav>
<a href$="[[_computeNavLink(_query, _offset, -1)]]"
hidden$="[[_hidePrevArrow(_offset)]]">&larr; Prev</a>
<a href$="[[_computeNavLink(_query, _offset, 1)]]"
hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</a>
</nav>
</div>
</template>
<script>
(function() {
'use strict';
var DEFAULT_NUM_CHANGES = 25;
Polymer({
is: 'gr-change-list-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* True when user is logged in.
*/
loggedIn: {
type: Boolean,
value: false,
},
/**
* State persisted across restamps of the element.
*/
viewState: {
type: Object,
notify: true,
value: function() { return {}; },
},
/**
* Currently active query.
*/
_query: String,
/**
* Offset of currently visible query results.
*/
_offset: Number,
/**
* Change objects loaded from the server.
*/
_changes: Array,
/**
* Contains error of last request (in case of change loading error).
*/
_lastError: Object,
/**
* For showing a "loading..." string during ajax requests.
*/
_loading: {
type: Boolean,
value: true,
},
},
behaviors: [
Gerrit.RESTClientBehavior,
],
attached: function() {
this.fire('title-change', {title: this._query});
},
_paramsChanged: function(value) {
if (value.view != this.tagName.toLowerCase()) { return; }
this._query = value.query;
this._offset = value.offset || 0;
if (this.viewState.query != this._query ||
this.viewState.offset != this._offset) {
this.set('viewState.selectedChangeIndex', 0);
this.set('viewState.query', this._query);
this.set('viewState.offset', this._offset);
}
this.fire('title-change', {title: this._query});
},
_computeQueryParams: function(query, offset) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.DETAILED_ACCOUNTS
);
var obj = {
n: DEFAULT_NUM_CHANGES, // Number of results to return.
O: options,
S: offset || 0,
};
if (query && query.length > 0) {
obj.q = query;
}
return obj;
},
_computeNavLink: function(query, offset, direction) {
// Offset could be a string when passed from the router.
offset = +(offset || 0);
var newOffset = Math.max(0, offset + (25 * direction));
var href = '/q/' + query;
if (newOffset > 0) {
href += ',' + newOffset;
}
return href;
},
_computeErrorHidden: function(loading, lastError) {
return loading || lastError == null;
},
_computeListHidden: function(loading, lastError) {
return loading || lastError != null;
},
_hidePrevArrow: function(offset) {
return offset == 0;
},
_hideNextArrow: function(changesLen) {
return changesLen < DEFAULT_NUM_CHANGES;
},
});
})();
</script>
</dom-module>

View File

@@ -1,221 +0,0 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="../styles/gr-change-list-styles.html">
<link rel="import" href="gr-change-list-item.html">
<dom-module id="gr-change-list">
<template>
<style>
:host {
display: flex;
flex-direction: column;
}
</style>
<style include="gr-change-list-styles"></style>
<div class="headerRow">
<span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
<span class="topHeader star" hidden$="[[!showStar]]"></span>
<span class="topHeader subject">Subject</span>
<span class="topHeader status">Status</span>
<span class="topHeader owner">Owner</span>
<span class="topHeader project">Project</span>
<span class="topHeader branch">Branch</span>
<span class="topHeader updated">Updated</span>
<span class="topHeader size">Size</span>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
<span class="topHeader label" title$="[[labelName]]">
[[_computeLabelShortcut(labelName)]]
</span>
</template>
</div>
<template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex">
<template is="dom-if" if="[[_groupTitle(groupIndex)]]">
<div class="groupHeader">[[_groupTitle(groupIndex)]]</div>
</template>
<template is="dom-if" if="[[!changeGroup.length]]">
<div class="noChanges">No changes</div>
</template>
<template is="dom-repeat" items="[[changeGroup]]" as="change">
<gr-change-list-item
selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]"
needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
change="[[change]]"
show-star="[[showStar]]"
label-names="[[labelNames]]"></gr-change-list-item>
</template>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-change-list',
hostAttributes: {
tabindex: 0,
},
properties: {
/**
* The logged-in user's account, or an empty object if no user is logged
* in.
*/
account: {
type: Object,
value: function() { return {}; },
},
/**
* An array of ChangeInfo objects to render.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
*/
changes: {
type: Array,
observer: '_changesChanged',
},
/**
* ChangeInfo objects grouped into arrays. The groups and changes
* properties should not be used together.
*/
groups: {
type: Array,
value: function() { return []; },
},
groupTitles: {
type: Array,
value: function() { return []; },
},
labelNames: {
type: Array,
computed: '_computeLabelNames(groups)',
},
selectedIndex: {
type: Number,
notify: true,
},
showStar: {
type: Boolean,
value: false,
},
showReviewedState: {
type: Boolean,
value: false,
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
_computeLabelNames: function(groups) {
if (!groups) { return []; }
var labels = [];
var nonExistingLabel = function(item) {
return labels.indexOf(item) < 0;
};
for (var i = 0; i < groups.length; i++) {
var group = groups[i];
for (var j = 0; j < group.length; j++) {
var change = group[j];
if (!change.labels) { continue; }
var currentLabels = Object.keys(change.labels);
labels = labels.concat(currentLabels.filter(nonExistingLabel));
}
}
return labels.sort();
},
_computeLabelShortcut: function(labelName) {
return labelName.replace(/[a-z-]/g, '');
},
_changesChanged: function(changes) {
this.groups = changes ? [changes] : [];
},
_groupTitle: function(groupIndex) {
if (groupIndex > this.groupTitles.length - 1) { return null; }
return this.groupTitles[groupIndex];
},
_computeItemSelected: function(index, groupIndex, selectedIndex) {
var idx = 0;
for (var i = 0; i < groupIndex; i++) {
idx += this.groups[i].length;
}
idx += index;
return idx == selectedIndex;
},
_computeItemNeedsReview: function(account, change, showReviewedState) {
return showReviewedState && !change.reviewed &&
change.status != this.ChangeStatus.MERGED &&
account._account_id != change.owner._account_id;
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
if (this.groups == null) { return; }
var len = 0;
this.groups.forEach(function(group) {
len += group.length;
});
switch (e.keyCode) {
case 74: // 'j'
e.preventDefault();
if (this.selectedIndex == len - 1) { return; }
this.selectedIndex += 1;
break;
case 75: // 'k'
e.preventDefault();
if (this.selectedIndex == 0) { return; }
this.selectedIndex -= 1;
break;
case 79: // 'o'
case 13: // 'enter'
e.preventDefault();
page.show(this._changeURLForIndex(this.selectedIndex));
break;
}
},
_changeURLForIndex: function(index) {
var changeEls = this._getListItems();
if (index < changeEls.length && changeEls[index]) {
return changeEls[index].changeURL;
}
return '';
},
_getListItems: function() {
return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
},
});
})();
</script>
</dom-module>

View File

@@ -1,661 +0,0 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-account-link.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-change-actions.html">
<link rel="import" href="gr-change-metadata.html">
<link rel="import" href="gr-change-star.html">
<link rel="import" href="gr-date-formatter.html">
<link rel="import" href="gr-download-dialog.html">
<link rel="import" href="gr-file-list.html">
<link rel="import" href="gr-linked-text.html">
<link rel="import" href="gr-messages-list.html">
<link rel="import" href="gr-overlay.html">
<link rel="import" href="gr-related-changes-list.html">
<link rel="import" href="gr-reply-dialog.html">
<dom-module id="gr-change-view">
<template>
<style>
.container {
margin: 1em var(--default-horizontal-margin);
}
.container:not(.loading) {
background-color: var(--view-background-color);
}
.container.loading {
color: #666;
}
.headerContainer {
height: 4.1em;
margin-bottom: .5em;
}
.header {
align-items: center;
background-color: var(--view-background-color);
border-bottom: 1px solid #ddd;
display: flex;
padding: 1em var(--default-horizontal-margin);
z-index: 99; /* Less than gr-overlay's backdrop */
}
.header.pinned {
border-bottom-color: transparent;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
position: fixed;
top: 0;
transition: box-shadow 250ms linear;
width: calc(100% - (2 * var(--default-horizontal-margin)));
}
.header-title {
flex: 1;
font-size: 1.2em;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
gr-change-star {
margin-right: .25em;
vertical-align: -.425em;
}
.download,
.patchSelectLabel {
margin-left: var(--default-horizontal-margin);
}
.header select {
margin-left: .5em;
}
.header .reply {
margin-left: var(--default-horizontal-margin);
}
gr-reply-dialog {
min-width: 30em;
max-width: 50em;
}
.changeStatus {
color: #999;
text-transform: capitalize;
}
section {
margin: 10px 0;
padding: 10px var(--default-horizontal-margin);
}
/* Strong specificity here is needed due to
https://github.com/Polymer/polymer/issues/2531 */
.container section.changeInfo {
border-bottom: 1px solid #ddd;
display: flex;
margin-top: 0;
padding-top: 0;
}
.changeInfo-column:not(:last-of-type) {
margin-right: 1em;
padding-right: 1em;
}
.changeMetadata {
border-right: 1px solid #ddd;
font-size: .9em;
}
gr-change-actions {
margin-top: 1em;
}
.commitMessage {
font-family: var(--monospace-font-family);
flex: 0 0 72ch;
margin-right: 2em;
margin-bottom: 1em;
}
.commitMessage h4 {
font-family: var(--font-family);
font-weight: bold;
margin-bottom: .25em;
}
.commitAndRelated {
align-content: flex-start;
display: flex;
flex: 1;
flex-wrap: wrap;
}
gr-file-list {
margin-bottom: 1em;
padding: 0 var(--default-horizontal-margin);
}
@media screen and (max-width: 50em) {
.container {
margin: .5em 0 !important;
}
.container.loading {
margin: 1em var(--default-horizontal-margin) !important;
}
.headerContainer {
height: 5.15em;
}
.header {
align-items: flex-start;
flex-direction: column;
padding: .5em var(--default-horizontal-margin) !important;
}
gr-change-star {
vertical-align: middle;
}
.header-title,
.header-actions,
.header.pinned {
width: 100% !important;
}
.header-title {
font-size: 1.1em;
}
.header-actions {
align-items: center;
display: flex;
justify-content: space-between;
margin-top: .5em;
}
gr-reply-dialog {
min-width: initial;
width: 90vw;
}
.download {
display: none;
}
.patchSelectLabel {
margin-left: 0 !important;
margin-right: .5em;
}
.header select {
margin-left: 0 !important;
margin-right: .5em;
}
.header .reply {
margin-left: 0 !important;
margin-right: .5em;
}
.changeInfo-column:not(:last-of-type) {
margin-right: 0;
padding-right: 0;
}
.changeInfo,
.commitAndRelated {
flex-direction: column;
flex-wrap: nowrap;
}
.changeMetadata {
font-size: 1em;
border-right: none;
margin-bottom: 1em;
margin-top: .25em;
max-width: none;
}
.commitMessage {
flex: initial;
margin-right: 0;
}
}
</style>
<gr-ajax id="detailXHR"
url="[[_computeDetailPath(_changeNum)]]"
params="[[_computeDetailQueryParams()]]"
last-response="{{_change}}"
loading="{{_loading}}"></gr-ajax>
<gr-ajax id="commentsXHR"
url="[[_computeCommentsPath(_changeNum)]]"
last-response="{{_comments}}"></gr-ajax>
<gr-ajax id="commitInfoXHR"
url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]"
last-response="{{_commitInfo}}"></gr-ajax>
<!-- TODO(andybons): Cache the project config. -->
<gr-ajax id="configXHR"
auto
url="[[_computeProjectConfigPath(_change.project)]]"
last-response="{{_projectConfig}}"></gr-ajax>
<div class="container loading" hidden$="{{!_loading}}">Loading...</div>
<div class="container" hidden$="{{_loading}}">
<div class="headerContainer">
<div class="header">
<span class="header-title">
<gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
<a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
<span>[[_change.subject]]</span>
<span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
</span>
<span class="header-actions">
<gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button>
<gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button>
<span>
<label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
<select id="patchSetSelect" on-change="_handlePatchChange">
<template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
<option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
<span>[[patchNumber]]</span>
/
<span>[[_computeLatestPatchNum(_change)]]</span>
</option>
</template>
</select>
</span>
</span>
</div>
</div>
<section class="changeInfo">
<div class="changeInfo-column changeMetadata">
<gr-change-metadata
change="[[_change]]"
mutable="[[_loggedIn]]"></gr-change-metadata>
<gr-change-actions id="actions"
actions="[[_change.actions]]"
change-num="[[_changeNum]]"
patch-num="[[_patchNum]]"
on-reload-change="_handleReloadChange"></gr-change-actions>
</div>
<div class="changeInfo-column commitAndRelated">
<div class="commitMessage">
<h4>Commit message</h4>
<gr-linked-text pre
content="[[_commitInfo.message]]"
config="[[_projectConfig.commentlinks]]"></gr-linked-text>
</div>
<div class="relatedChanges">
<gr-related-changes-list id="relatedChanges"
change="[[_change]]"
server-config="[[serverConfig]]"
patch-num="[[_patchNum]]"></gr-related-changes-list>
</div>
</div>
</section>
<gr-file-list id="fileList"
change-num="[[_changeNum]]"
patch-num="[[_patchNum]]"
comments="[[_comments]]"
selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
<gr-messages-list id="messageList"
change-num="[[_changeNum]]"
messages="[[_change.messages]]"
comments="[[_comments]]"
project-config="[[_projectConfig]]"
show-reply-buttons="[[_loggedIn]]"
on-reply="_handleMessageReply"></gr-messages-list>
</div>
<gr-overlay id="downloadOverlay" with-backdrop>
<gr-download-dialog
change="[[_change]]"
patch-num="[[_patchNum]]"
config="[[serverConfig.download]]"
on-close="_handleDownloadDialogClose"></gr-download-dialog>
</gr-overlay>
<gr-overlay id="replyOverlay"
on-iron-overlay-opened="_handleReplyOverlayOpen"
with-backdrop>
<gr-reply-dialog id="replyDialog"
change-num="[[_changeNum]]"
patch-num="[[_patchNum]]"
labels="[[_change.labels]]"
permitted-labels="[[_change.permitted_labels]]"
on-send="_handleReplySent"
on-cancel="_handleReplyCancel"
hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
</gr-overlay>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-change-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
viewState: {
type: Object,
notify: true,
value: function() { return {}; },
},
serverConfig: Object,
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_comments: Object,
_change: {
type: Object,
observer: '_changeChanged',
},
_commitInfo: Object,
_changeNum: String,
_patchNum: String,
_allPatchSets: {
type: Array,
computed: '_computeAllPatchSets(_change)',
},
_loggedIn: {
type: Boolean,
value: false,
},
_loading: Boolean,
_headerContainerEl: Object,
_headerEl: Object,
_projectConfig: Object,
_boundScrollHandler: {
type: Function,
value: function() { return this._handleBodyScroll.bind(this); },
},
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
}.bind(this));
this._headerEl = this.$$('.header');
},
attached: function() {
window.addEventListener('scroll', this._boundScrollHandler);
},
detached: function() {
window.removeEventListener('scroll', this._boundScrollHandler);
},
_handleBodyScroll: function(e) {
var containerEl = this._headerContainerEl ||
this.$$('.headerContainer');
// Calculate where the header is relative to the window.
var top = containerEl.offsetTop;
for (var offsetParent = containerEl.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
// The element may not be displayed yet, in which case do nothing.
if (top == 0) { return; }
this._headerEl.classList.toggle('pinned', window.scrollY >= top);
},
_resetHeaderEl: function() {
var el = this._headerEl || this.$$('.header');
this._headerEl = el;
el.classList.remove('pinned');
},
_handlePatchChange: function(e) {
var patchNum = e.target.value;
var currentPatchNum =
this._change.revisions[this._change.current_revision]._number;
if (patchNum == currentPatchNum) {
page.show(this._computeChangePath(this._changeNum));
return;
}
page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
},
_handleReplyTap: function(e) {
e.preventDefault();
this.$.replyOverlay.open();
},
_handleDownloadTap: function(e) {
e.preventDefault();
this.$.downloadOverlay.open();
},
_handleDownloadDialogClose: function(e) {
this.$.downloadOverlay.close();
},
_handleMessageReply: function(e) {
var msg = e.detail.message.message;
var quoteStr = msg.split('\n').map(
function(line) { return '> ' + line; }).join('\n') + '\n\n';
this.$.replyDialog.draft += quoteStr;
this.$.replyOverlay.open();
},
_handleReplyOverlayOpen: function(e) {
this.$.replyDialog.reload().then(function() {
this.async(function() { this.$.replyOverlay.center() }, 1);
}.bind(this));
this.$.replyDialog.focus();
},
_handleReplySent: function(e) {
this.$.replyOverlay.close();
this._reload();
},
_handleReplyCancel: function(e) {
this.$.replyOverlay.close();
},
_paramsChanged: function(value) {
if (value.view != this.tagName.toLowerCase()) { return; }
this._changeNum = value.changeNum;
this._patchNum = value.patchNum;
if (this.viewState.changeNum != this._changeNum ||
this.viewState.patchNum != this._patchNum) {
this.set('viewState.selectedFileIndex', 0);
this.set('viewState.changeNum', this._changeNum);
this.set('viewState.patchNum', this._patchNum);
}
if (!this._changeNum) {
return;
}
this._reload().then(function() {
this.$.messageList.topMargin = this._headerEl.offsetHeight;
// Allow the message list to render before scrolling.
this.async(function() {
var msgPrefix = '#message-';
var hash = window.location.hash;
if (hash.indexOf(msgPrefix) == 0) {
this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
}
}.bind(this), 1);
app.accountReady.then(function() {
if (!this._loggedIn) { return; }
if (this.viewState.showReplyDialog) {
this.$.replyOverlay.open();
this.set('viewState.showReplyDialog', false);
}
}.bind(this));
}.bind(this));
},
_changeChanged: function(change) {
if (!change) { return; }
this._patchNum = this._patchNum ||
change.revisions[change.current_revision]._number;
var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
this.fire('title-change', {title: title});
},
_computeChangePath: function(changeNum) {
return '/c/' + changeNum;
},
_computeChangePermalink: function(changeNum) {
return '/' + changeNum;
},
_computeChangeStatus: function(change, patchNum) {
var status = change.status;
if (status == this.ChangeStatus.NEW) {
var rev = this._getRevisionNumber(change, patchNum);
// TODO(davido): Figure out, why sometimes revision is not there
if (rev == undefined || !rev.draft) { return ''; }
status = this.ChangeStatus.DRAFT;
}
return '(' + status.toLowerCase() + ')';
},
_computeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeCommitInfoPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/commit?links';
},
_computeCommentsPath: function(changeNum) {
return '/changes/' + changeNum + '/comments';
},
_computeProjectConfigPath: function(project) {
return '/projects/' + encodeURIComponent(project) + '/config';
},
_computeDetailQueryParams: function() {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.ALL_REVISIONS,
this.ListChangesOption.CHANGE_ACTIONS,
this.ListChangesOption.DOWNLOAD_COMMANDS
);
return {O: options};
},
_computeLatestPatchNum: function(change) {
return change.revisions[change.current_revision]._number;
},
_computeAllPatchSets: function(change) {
var patchNums = [];
for (var rev in change.revisions) {
patchNums.push(change.revisions[rev]._number);
}
return patchNums.sort(function(a, b) {
return a - b;
});
},
_getRevisionNumber: function(change, patchNum) {
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
return change.revisions[rev];
}
}
},
_computePatchIndexIsSelected: function(index, patchNum) {
return this._allPatchSets[index] == patchNum;
},
_computeLabelNames: function(labels) {
return Object.keys(labels).sort();
},
_computeLabelValues: function(labelName, labels) {
var result = [];
var t = labels[labelName];
if (!t) { return result; }
var approvals = t.all || [];
approvals.forEach(function(label) {
if (label.value && label.value != labels[labelName].default_value) {
var labelClassName;
var labelValPrefix = '';
if (label.value > 0) {
labelValPrefix = '+';
labelClassName = 'approved';
} else if (label.value < 0) {
labelClassName = 'notApproved';
}
result.push({
value: labelValPrefix + label.value,
className: labelClassName,
account: label,
});
}
});
return result;
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 65: // 'a'
e.preventDefault();
this.$.replyOverlay.open();
break;
case 85: // 'u'
e.preventDefault();
page.show('/');
break;
}
},
_handleReloadChange: function() {
page.show(this._computeChangePath(this._changeNum));
},
_reload: function() {
var detailCompletes = this.$.detailXHR.generateRequest().completes;
this.$.commentsXHR.generateRequest();
var reloadPatchNumDependentResources = function() {
return Promise.all([
this.$.commitInfoXHR.generateRequest().completes,
this.$.actions.reload(),
this.$.fileList.reload(),
]);
}.bind(this);
var reloadDetailDependentResources = function() {
return this.$.relatedChanges.reload();
}.bind(this);
this._resetHeaderEl();
if (this._patchNum) {
return reloadPatchNumDependentResources().then(function() {
return detailCompletes;
}).then(reloadDetailDependentResources);
} else {
// The patch number is reliant on the change detail request.
return detailCompletes.then(reloadPatchNumDependentResources).then(
reloadDetailDependentResources);
}
},
});
})();
</script>
</dom-module>

View File

@@ -1,94 +0,0 @@
<!--
Copyright (C) 2015 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">
<dom-module id="gr-date-formatter">
<template>
<style>
:host {
display: inline;
}
</style>
<span>[[_computeDateStr(dateStr)]]</span>
</template>
<script>
(function() {
'use strict';
var Duration = {
HOUR: 1000 * 60 * 60,
DAY: 1000 * 60 * 60 * 24,
};
var ShortMonthNames = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
'Nov', 'Dec'
];
Polymer({
is: 'gr-date-formatter',
properties: {
dateStr: {
type: String,
value: null,
notify: true
}
},
_computeDateStr: function(dateStr) {
return this._dateStr(this._parseDateStr(dateStr), new Date());
},
_parseDateStr: function(dateStr) {
if (!dateStr) { return null; }
return util.parseDate(dateStr);
},
_dateStr: function(t, now) {
if (!t) { return ''; }
var diff = now.getTime() - t.getTime();
if (diff < Duration.DAY && t.getDay() == now.getDay()) {
// Within 24 hours and on the same day:
// '2:14 AM'
var pm = t.getHours() >= 12;
var hours = t.getHours();
if (hours == 0) {
hours = 12;
} else if (hours > 12) {
hours = t.getHours() - 12;
}
var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
t.getMinutes();
return hours + ':' + minutes + (pm ? ' PM' : ' AM');
} else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
diff < 180 * Duration.DAY) {
// From one to six months:
// 'Aug 29'
return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
} else if (diff >= 180 * Duration.DAY) {
// More than six months:
// 'Aug 29, 1997'
return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
t.getFullYear();
}
},
});
})();
</script>
</dom-module>

View File

@@ -1,257 +0,0 @@
<!--
Copyright (C) 2015 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="gr-diff-comment.html">
<dom-module id="gr-diff-comment-thread">
<template>
<style>
:host {
display: block;
white-space: normal;
}
gr-diff-comment {
border-left: 1px solid #ddd;
}
gr-diff-comment:first-of-type {
border-top: 1px solid #ddd;
}
gr-diff-comment:last-of-type {
border-bottom: 1px solid #ddd;
}
</style>
<div id="container">
<template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment">
<gr-diff-comment
comment="{{comment}}"
change-num="[[changeNum]]"
patch-num="[[patchNum]]"
draft="[[comment.__draft]]"
show-actions="[[showActions]]"
project-config="[[projectConfig]]"
on-height-change="_handleCommentHeightChange"
on-reply="_handleCommentReply"
on-discard="_handleCommentDiscard"
on-done="_handleCommentDone"></gr-diff-comment>
</template>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff-comment-thread',
/**
* Fired when the height of the thread changes.
*
* @event height-change
*/
/**
* Fired when the thread should be discarded.
*
* @event discard
*/
properties: {
changeNum: String,
comments: {
type: Array,
value: function() { return []; },
},
patchNum: String,
path: String,
showActions: Boolean,
projectConfig: Object,
_boundWindowResizeHandler: {
type: Function,
value: function() { return this._handleWindowResize.bind(this); }
},
_lastHeight: Number,
_orderedComments: Array,
},
get naturalHeight() {
return this.$.container.offsetHeight;
},
observers: [
'_commentsChanged(comments.splices)',
],
attached: function() {
window.addEventListener('resize', this._boundWindowResizeHandler);
},
detached: function() {
window.removeEventListener('resize', this._boundWindowResizeHandler);
},
_handleWindowResize: function(e) {
this._heightChanged();
},
_commentsChanged: function(changeRecord) {
this._orderedComments = this._sortedComments(this.comments);
},
_sortedComments: function(comments) {
comments.sort(function(c1, c2) {
var c1Date = c1.__date || util.parseDate(c1.updated);
var c2Date = c2.__date || util.parseDate(c2.updated);
return c1Date - c2Date;
});
var commentIDToReplies = {};
var topLevelComments = [];
for (var i = 0; i < comments.length; i++) {
var c = comments[i];
if (c.in_reply_to) {
if (commentIDToReplies[c.in_reply_to] == null) {
commentIDToReplies[c.in_reply_to] = [];
}
commentIDToReplies[c.in_reply_to].push(c);
} else {
topLevelComments.push(c);
}
}
var results = [];
for (var i = 0; i < topLevelComments.length; i++) {
this._visitComment(topLevelComments[i], commentIDToReplies, results);
}
return results;
},
_visitComment: function(parent, commentIDToReplies, results) {
results.push(parent);
var replies = commentIDToReplies[parent.id];
if (!replies) { return; }
for (var i = 0; i < replies.length; i++) {
this._visitComment(replies[i], commentIDToReplies, results);
}
},
_handleCommentHeightChange: function(e) {
e.stopPropagation();
this._heightChanged();
},
_handleCommentReply: function(e) {
var comment = e.detail.comment;
var quoteStr;
if (e.detail.quote) {
var msg = comment.message;
var quoteStr = msg.split('\n').map(
function(line) { return ' > ' + line; }).join('\n') + '\n\n';
}
var reply =
this._newReply(comment.id, comment.line, this.path, quoteStr);
this.push('comments', reply);
// Allow the reply to render in the dom-repeat.
this.async(function() {
var commentEl = this._commentElWithDraftID(reply.__draftID);
commentEl.editing = true;
this.async(this._heightChanged.bind(this), 1);
}.bind(this), 1);
},
_handleCommentDone: function(e) {
var comment = e.detail.comment;
var reply = this._newReply(comment.id, comment.line, this.path, 'Done');
this.push('comments', reply);
// Allow the reply to render in the dom-repeat.
this.async(function() {
var commentEl = this._commentElWithDraftID(reply.__draftID);
commentEl.save();
this.async(this._heightChanged.bind(this), 1);
}.bind(this), 1);
},
_commentElWithDraftID: function(draftID) {
var commentEls =
Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
for (var i = 0; i < commentEls.length; i++) {
if (commentEls[i].comment.__draftID == draftID) {
return commentEls[i];
}
}
return null;
},
_newReply: function(inReplyTo, line, path, opt_message) {
var c = {
__draft: true,
__draftID: Math.random().toString(36),
__date: new Date(),
line: line,
path: path,
in_reply_to: inReplyTo,
};
if (opt_message != null) {
c.message = opt_message;
}
return c;
},
_handleCommentDiscard: function(e) {
// TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady
// DOM, it respects the bubbles property.
// https://github.com/Polymer/polymer/issues/3226
e.stopPropagation();
var diffCommentEl = Polymer.dom(e).rootTarget;
var idx = this._indexOf(diffCommentEl.comment, this.comments);
if (idx == -1) {
throw Error('Cannot find comment ' +
JSON.stringify(diffCommentEl.comment));
}
this.splice('comments', idx, 1);
if (this.comments.length == 0) {
this.fire('discard', null, {bubbles: false});
return;
}
this.async(this._heightChanged.bind(this), 1);
},
_heightChanged: function() {
var height = this.$.container.offsetHeight;
if (height == this._lastHeight) { return; }
this.fire('height-change', {height: height}, {bubbles: false});
this._lastHeight = height;
},
_indexOf: function(comment, arr) {
for (var i = 0; i < arr.length; i++) {
var c = arr[i];
if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
(c.id != null && c.id == comment.id)) {
return i;
}
}
return -1;
},
});
})();
</script>
</dom-module>

View File

@@ -1,389 +0,0 @@
<!--
Copyright (C) 2015 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-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-date-formatter.html">
<link rel="import" href="gr-linked-text.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-diff-comment">
<template>
<style>
:host {
background-color: #ffd;
display: block;
--iron-autogrow-textarea: {
padding: 2px;
};
}
:host([disabled]) {
pointer-events: none;
}
:host([disabled]) .container {
opacity: .5;
}
.header,
.message,
.actions {
padding: .5em .7em;
}
.header {
display: flex;
padding-bottom: 0;
font-family: 'Open Sans', sans-serif;
}
.headerLeft {
flex: 1;
}
.authorName,
.draftLabel {
font-weight: bold;
}
.draftLabel {
color: #999;
display: none;
}
.date {
justify-content: flex-end;
margin-left: 5px;
}
a.date:link,
a.date:visited {
color: #666;
}
.actions {
display: flex;
padding-top: 0;
}
.action {
margin-right: .5em;
}
.danger {
display: flex;
flex: 1;
justify-content: flex-end;
}
.editMessage {
display: none;
margin: .5em .7em;
width: calc(100% - 1.4em - 2px);
}
.danger .action {
margin-right: 0;
}
.container:not(.draft) .actions :not(.reply):not(.quote):not(.done) {
display: none;
}
.draft .reply,
.draft .quote,
.draft .done {
display: none;
}
.draft .draftLabel {
display: inline;
}
.draft:not(.editing) .save,
.draft:not(.editing) .cancel {
display: none;
}
.editing .message,
.editing .reply,
.editing .quote,
.editing .done,
.editing .edit {
display: none;
}
.editing .editMessage {
background-color: #fff;
display: block;
}
</style>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
<span class="authorName">[[comment.author.name]]</span>
<span class="draftLabel">DRAFT</span>
</div>
<a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
<gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
</a>
</div>
<iron-autogrow-textarea
id="editTextarea"
class="editMessage"
disabled="{{disabled}}"
rows="4"
bind-value="{{_editDraft}}"
on-keyup="_handleTextareaKeyup"
on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
<gr-linked-text class="message"
pre
content="[[comment.message]]"
config="[[projectConfig.commentlinks]]"></gr-linked-text>
<div class="actions" hidden$="[[!showActions]]">
<gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
<gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button>
<gr-button class="action done" on-tap="_handleDone">Done</gr-button>
<gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button>
<gr-button class="action save" on-tap="_handleSave"
disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button>
<gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button>
<div class="danger">
<gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button>
</div>
</div>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff-comment',
/**
* Fired when the height of the comment changes.
*
* @event height-change
*/
/**
* Fired when the Reply action is triggered.
*
* @event reply
*/
/**
* Fired when the Done action is triggered.
*
* @event done
*/
/**
* Fired when this comment is discarded.
*
* @event discard
*/
properties: {
changeNum: String,
comment: {
type: Object,
notify: true,
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
draft: {
type: Boolean,
value: false,
observer: '_draftChanged',
},
editing: {
type: Boolean,
value: false,
observer: '_editingChanged',
},
patchNum: String,
showActions: Boolean,
projectConfig: Object,
_xhrPromise: Object, // Used for testing.
_editDraft: String,
},
ready: function() {
this._editDraft = (this.comment && this.comment.message) || '';
this.editing = this._editDraft.length == 0;
},
attached: function() {
this._heightChanged();
},
save: function() {
this.comment.message = this._editDraft;
this.disabled = true;
var endpoint = this._restEndpoint(this.comment.id);
this._send('PUT', endpoint).then(function(req) {
this.disabled = false;
var comment = req.response;
comment.__draft = true;
// Maintain the ephemeral draft ID for identification by other
// elements.
if (this.comment.__draftID) {
comment.__draftID = this.comment.__draftID;
}
this.comment = comment;
this.editing = false;
}.bind(this)).catch(function(err) {
alert('Your draft couldnt be saved. Check the console and contact ' +
'the PolyGerrit team for assistance.');
this.disabled = false;
}.bind(this));
},
_heightChanged: function() {
this.async(function() {
this.fire('height-change', {height: this.offsetHeight},
{bubbles: false});
}.bind(this));
},
_draftChanged: function(draft) {
this.$.container.classList.toggle('draft', draft);
},
_editingChanged: function(editing) {
this.$.container.classList.toggle('editing', editing);
if (editing) {
var textarea = this.$.editTextarea.textarea;
// Put the cursor at the end always.
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.selectionStart;
this.async(function() {
textarea.focus();
}.bind(this));
}
if (this.comment && this.comment.id) {
this.$$('.cancel').hidden = !editing;
}
this._heightChanged();
},
_computeLinkToComment: function(comment) {
return '#' + comment.line;
},
_computeSaveDisabled: function(draft) {
return draft == null || draft.trim() == '';
},
_handleTextareaKeyup: function(e) {
// TODO(andybons): This isn't always true, but I can't currently think
// of a better metric.
this._heightChanged();
},
_handleTextareaKeydown: function(e) {
if (e.keyCode == 27) { // 'esc'
this._handleCancel(e);
}
},
_handleLinkTap: function(e) {
e.preventDefault();
var hash = this._computeLinkToComment(this.comment);
// Don't add the hash to the window history if it's already there.
// Otherwise you mess up expected back button behavior.
if (window.location.hash == hash) { return; }
// Change the URL but dont trigger a nav event. Otherwise it will
// reload the page.
page.show(window.location.pathname + hash, null, false);
},
_handleReply: function(e) {
this._preventDefaultAndBlur(e);
this.fire('reply', {comment: this.comment}, {bubbles: false});
},
_handleQuote: function(e) {
this._preventDefaultAndBlur(e);
this.fire('reply', {comment: this.comment, quote: true},
{bubbles: false});
},
_handleDone: function(e) {
this._preventDefaultAndBlur(e);
this.fire('done', {comment: this.comment}, {bubbles: false});
},
_handleEdit: function(e) {
this._preventDefaultAndBlur(e);
this._editDraft = this.comment.message;
this.editing = true;
},
_handleSave: function(e) {
this._preventDefaultAndBlur(e);
this.save();
},
_handleCancel: function(e) {
this._preventDefaultAndBlur(e);
if (this.comment.message == null || this.comment.message.length == 0) {
this.fire('discard', null, {bubbles: false});
return;
}
this._editDraft = this.comment.message;
this.editing = false;
},
_handleDiscard: function(e) {
this._preventDefaultAndBlur(e);
if (!this.comment.__draft) {
throw Error('Cannot discard a non-draft comment.');
}
this.disabled = true;
var commentID = this.comment.id;
if (!commentID) {
this.fire('discard', null, {bubbles: false});
return;
}
this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
this.fire('discard', null, {bubbles: false});
}.bind(this)).catch(function(err) {
alert('Your draft couldnt be deleted. Check the console and ' +
'contact the PolyGerrit team for assistance.');
this.disabled = false;
}.bind(this));
},
_preventDefaultAndBlur: function(e) {
e.preventDefault();
Polymer.dom(e).rootTarget.blur();
},
_send: function(method, url) {
var xhr = document.createElement('gr-request');
var opts = {
method: method,
url: url,
};
if (method == 'PUT' || method == 'POST') {
opts.body = this.comment;
}
this._xhrPromise = xhr.send(opts);
return this._xhrPromise;
},
_restEndpoint: function(id) {
var path = '/changes/' + this.changeNum + '/revisions/' +
this.patchNum + '/drafts';
if (id) {
path += '/' + id;
}
return path;
},
});
})();
</script>
</dom-module>

View File

@@ -1,698 +0,0 @@
<!--
Copyright (C) 2015 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="gr-diff-comment-thread.html">
<dom-module id="gr-diff-side">
<template>
<style>
:host,
.container {
display: flex;
flex: 0 0 auto;
}
.lineNum:before,
.code:before {
/* To ensure the height is non-zero in these elements, a
zero-width space is set as its content. The character
itself doesn't matter. Just that there is something
there. */
content: '\200B';
}
.lineNum {
background-color: #eee;
color: #666;
padding: 0 .75em;
text-align: right;
}
.canComment .lineNum {
cursor: pointer;
text-decoration: underline;
}
.canComment .lineNum:hover {
background-color: #ccc;
}
.lightHighlight {
background-color: var(--light-highlight-color);
}
hl,
.darkHighlight {
background-color: var(--dark-highlight-color);
}
.br:after {
/* Line feed */
content: '\A';
}
.tab {
display: inline-block;
}
.tab.withIndicator:before {
color: #C62828;
/* >> character */
content: '\00BB';
}
.numbers,
.content {
white-space: pre;
}
.numbers .filler {
background-color: #eee;
}
.contextControl {
background-color: #fef;
}
.contextControl a:link,
.contextControl a:visited {
display: block;
text-decoration: none;
}
.numbers .contextControl {
padding: 0 .75em;
text-align: right;
}
.content .contextControl {
text-align: center;
}
</style>
<div class$="[[_computeContainerClass(canComment)]]">
<div class="numbers" id="numbers"></div>
<div class="content" id="content"></div>
</div>
</template>
<script>
(function() {
'use strict';
var CharCode = {
LESS_THAN: '<'.charCodeAt(0),
GREATER_THAN: '>'.charCodeAt(0),
AMPERSAND: '&'.charCodeAt(0),
SEMICOLON: ';'.charCodeAt(0),
};
var TAB_REGEX = /\t/g;
Polymer({
is: 'gr-diff-side',
/**
* Fired when an expand context control is clicked.
*
* @event expand-context
*/
/**
* Fired when a thread's height is changed.
*
* @event thread-height-change
*/
/**
* Fired when a draft should be added.
*
* @event add-draft
*/
/**
* Fired when a thread is removed.
*
* @event remove-thread
*/
properties: {
canComment: {
type: Boolean,
value: false,
},
content: {
type: Array,
notify: true,
observer: '_contentChanged',
},
prefs: {
type: Object,
value: function() { return {}; },
},
changeNum: String,
patchNum: String,
path: String,
projectConfig: {
type: Object,
observer: '_projectConfigChanged',
},
_lineFeedHTML: {
type: String,
value: '<span class="style-scope gr-diff-side br"></span>',
readOnly: true,
},
_highlightStartTag: {
type: String,
value: '<hl class="style-scope gr-diff-side">',
readOnly: true,
},
_highlightEndTag: {
type: String,
value: '</hl>',
readOnly: true,
},
_diffChunkLineNums: {
type: Array,
value: function() { return []; },
},
_commentThreadLineNums: {
type: Array,
value: function() { return []; },
},
_focusedLineNum: {
type: Number,
value: 1,
},
},
listeners: {
'tap': '_tapHandler',
},
observers: [
'_prefsChanged(prefs.*)',
],
rowInserted: function(index) {
this.renderLineIndexRange(index, index);
this._updateDOMIndices();
this._updateJumpIndices();
},
rowRemoved: function(index) {
var removedEls = Polymer.dom(this.root).querySelectorAll(
'[data-index="' + index + '"]');
for (var i = 0; i < removedEls.length; i++) {
removedEls[i].parentNode.removeChild(removedEls[i]);
}
this._updateDOMIndices();
this._updateJumpIndices();
},
rowUpdated: function(index) {
var removedEls = Polymer.dom(this.root).querySelectorAll(
'[data-index="' + index + '"]');
for (var i = 0; i < removedEls.length; i++) {
removedEls[i].parentNode.removeChild(removedEls[i]);
}
this.renderLineIndexRange(index, index);
},
scrollToLine: function(lineNum) {
if (isNaN(lineNum) || lineNum < 1) { return; }
var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
if (!el) { return; }
// Calculate where the line is relative to the window.
var top = el.offsetTop;
for (var offsetParent = el.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
// Scroll the element to the middle of the window. Dividing by a third
// instead of half the inner height feels a bit better otherwise the
// element appears to be below the center of the window even when it
// isn't.
window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
},
scrollToNextDiffChunk: function() {
this._scrollToNextChunkOrThread(this._diffChunkLineNums);
},
scrollToPreviousDiffChunk: function() {
this._scrollToPreviousChunkOrThread(this._diffChunkLineNums);
},
scrollToNextCommentThread: function() {
this._scrollToNextChunkOrThread(this._commentThreadLineNums);
},
scrollToPreviousCommentThread: function() {
this._scrollToPreviousChunkOrThread(this._commentThreadLineNums);
},
renderLineIndexRange: function(startIndex, endIndex) {
this._render(this.content, startIndex, endIndex);
},
hideElementsWithIndex: function(index) {
var els = Polymer.dom(this.root).querySelectorAll(
'[data-index="' + index + '"]');
for (var i = 0; i < els.length; i++) {
els[i].setAttribute('hidden', true);
}
},
getRowHeight: function(index) {
var row = this.content[index];
// Filler elements should not be taken into account when determining
// height calculations.
if (row.type == 'FILLER') {
return 0;
}
if (row.height != null) {
return row.height;
}
var selector = '[data-index="' + index + '"]';
var els = Polymer.dom(this.root).querySelectorAll(selector);
if (els.length != 2) {
throw Error('Rows should only consist of two elements');
}
return Math.max(els[0].offsetHeight, els[1].offsetHeight);
},
getRowNaturalHeight: function(index) {
var contentEl = this.$$('.content [data-index="' + index + '"]');
return contentEl.naturalHeight || contentEl.offsetHeight;
},
setRowNaturalHeight: function(index) {
var lineEl = this.$$('.numbers [data-index="' + index + '"]');
var contentEl = this.$$('.content [data-index="' + index + '"]');
contentEl.style.height = null;
var height = contentEl.offsetHeight;
lineEl.style.height = height + 'px';
this.content[index].height = height;
return height;
},
setRowHeight: function(index, height) {
var selector = '[data-index="' + index + '"]';
var els = Polymer.dom(this.root).querySelectorAll(selector);
for (var i = 0; i < els.length; i++) {
els[i].style.height = height + 'px';
}
this.content[index].height = height;
},
_scrollToNextChunkOrThread: function(lineNums) {
for (var i = 0; i < lineNums.length; i++) {
if (lineNums[i] > this._focusedLineNum) {
this._focusedLineNum = lineNums[i];
this.scrollToLine(this._focusedLineNum);
return;
}
}
},
_scrollToPreviousChunkOrThread: function(lineNums) {
for (var i = lineNums.length - 1; i >= 0; i--) {
if (this._focusedLineNum > lineNums[i]) {
this._focusedLineNum = lineNums[i];
this.scrollToLine(this._focusedLineNum);
return;
}
}
},
_updateJumpIndices: function() {
this._commentThreadLineNums = [];
this._diffChunkLineNums = [];
var inHighlight = false;
for (var i = 0; i < this.content.length; i++) {
switch (this.content[i].type) {
case 'COMMENT_THREAD':
this._commentThreadLineNums.push(
this.content[i].comments[0].line);
break;
case 'CODE':
// Only grab the first line of the highlighted chunk.
if (!inHighlight && this.content[i].highlight) {
this._diffChunkLineNums.push(this.content[i].lineNum);
inHighlight = true;
} else if (!this.content[i].highlight) {
inHighlight = false;
}
break;
}
}
},
_updateDOMIndices: function() {
// There is no way to select elements with a data-index greater than a
// given value. For now, just update all DOM elements.
var lineEls = Polymer.dom(this.root).querySelectorAll(
'.numbers [data-index]');
var contentEls = Polymer.dom(this.root).querySelectorAll(
'.content [data-index]');
if (lineEls.length != contentEls.length) {
throw Error(
'There must be the same number of line and content elements');
}
var index = 0;
for (var i = 0; i < this.content.length; i++) {
if (this.content[i].hidden) { continue; }
lineEls[index].setAttribute('data-index', i);
contentEls[index].setAttribute('data-index', i);
index++;
}
},
_prefsChanged: function(changeRecord) {
var prefs = changeRecord.base;
this.$.content.style.width = prefs.line_length + 'ch';
},
_projectConfigChanged: function(projectConfig) {
var threadEls =
Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
for (var i = 0; i < threadEls.length; i++) {
threadEls[i].projectConfig = projectConfig;
}
},
_contentChanged: function(diff) {
this._clearChildren(this.$.numbers);
this._clearChildren(this.$.content);
this._render(diff, 0, diff.length - 1);
this._updateJumpIndices();
},
_computeContainerClass: function(canComment) {
return 'container' + (canComment ? ' canComment' : '');
},
_tapHandler: function(e) {
var lineEl = Polymer.dom(e).rootTarget;
if (!this.canComment || !lineEl.classList.contains('lineNum')) {
return;
}
e.preventDefault();
var index = parseInt(lineEl.getAttribute('data-index'), 10);
var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
this.fire('add-draft', {
index: index,
line: line
}, {bubbles: false});
},
_clearChildren: function(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
},
_handleContextControlClick: function(context, e) {
e.preventDefault();
this.fire('expand-context', {context: context}, {bubbles: false});
},
_render: function(diff, startIndex, endIndex) {
var beforeLineEl;
var beforeContentEl;
if (endIndex != diff.length - 1) {
beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
if (!beforeLineEl && !beforeContentEl) {
// `endIndex` may be present within the model, but not in the DOM.
// Insert it before its successive element.
beforeLineEl = this.$$(
'.numbers [data-index="' + (endIndex + 1) + '"]');
beforeContentEl = this.$$(
'.content [data-index="' + (endIndex + 1) + '"]');
}
}
for (var i = startIndex; i <= endIndex; i++) {
if (diff[i].hidden) { continue; }
switch (diff[i].type) {
case 'CODE':
this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
break;
case 'FILLER':
this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
break;
case 'CONTEXT_CONTROL':
this._renderContextControl(diff[i], i, beforeLineEl,
beforeContentEl);
break;
case 'COMMENT_THREAD':
this._renderCommentThread(diff[i], i, beforeLineEl,
beforeContentEl);
break;
}
}
},
_handleCommentThreadHeightChange: function(e) {
var threadEl = Polymer.dom(e).rootTarget;
var index = parseInt(threadEl.getAttribute('data-index'), 10);
this.content[index].height = e.detail.height;
var lineEl = this.$$('.numbers [data-index="' + index + '"]');
lineEl.style.height = e.detail.height + 'px';
this.fire('thread-height-change', {
index: index,
height: e.detail.height,
}, {bubbles: false});
},
_handleCommentThreadDiscard: function(e) {
var threadEl = Polymer.dom(e).rootTarget;
var index = parseInt(threadEl.getAttribute('data-index'), 10);
this.fire('remove-thread', {index: index}, {bubbles: false});
},
_renderCommentThread: function(thread, index, beforeLineEl,
beforeContentEl) {
var lineEl = this._createElement('div', 'commentThread');
lineEl.classList.add('filler');
lineEl.setAttribute('data-index', index);
var threadEl = document.createElement('gr-diff-comment-thread');
threadEl.addEventListener('height-change',
this._handleCommentThreadHeightChange.bind(this));
threadEl.addEventListener('discard',
this._handleCommentThreadDiscard.bind(this));
threadEl.setAttribute('data-index', index);
threadEl.changeNum = this.changeNum;
threadEl.patchNum = thread.patchNum || this.patchNum;
threadEl.path = this.path;
threadEl.comments = thread.comments;
threadEl.showActions = this.canComment;
threadEl.projectConfig = this.projectConfig;
this.$.numbers.insertBefore(lineEl, beforeLineEl);
this.$.content.insertBefore(threadEl, beforeContentEl);
},
_renderContextControl: function(control, index, beforeLineEl,
beforeContentEl) {
var lineEl = this._createElement('div', 'contextControl');
lineEl.setAttribute('data-index', index);
lineEl.textContent = '@@';
var contentEl = this._createElement('div', 'contextControl');
contentEl.setAttribute('data-index', index);
var a = this._createElement('a');
a.href = '#';
a.textContent = 'Show ' + control.numLines + ' common ' +
(control.numLines == 1 ? 'line' : 'lines') + '...';
a.addEventListener('click',
this._handleContextControlClick.bind(this, control));
contentEl.appendChild(a);
this.$.numbers.insertBefore(lineEl, beforeLineEl);
this.$.content.insertBefore(contentEl, beforeContentEl);
},
_renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
var lineFillerEl = this._createElement('div', 'filler');
lineFillerEl.setAttribute('data-index', index);
var fillerEl = this._createElement('div', 'filler');
fillerEl.setAttribute('data-index', index);
var numLines = filler.numLines || 1;
lineFillerEl.textContent = '\n'.repeat(numLines);
for (var i = 0; i < numLines; i++) {
var newlineEl = this._createElement('span', 'br');
fillerEl.appendChild(newlineEl);
}
this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
this.$.content.insertBefore(fillerEl, beforeContentEl);
},
_renderCode: function(code, index, beforeLineEl, beforeContentEl) {
var lineNumEl = this._createElement('div', 'lineNum');
lineNumEl.setAttribute('data-line-num', code.lineNum);
lineNumEl.setAttribute('data-index', index);
var numLines = code.numLines || 1;
lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
var contentEl = this._createElement('div', 'code');
contentEl.setAttribute('data-line-num', code.lineNum);
contentEl.setAttribute('data-index', index);
if (code.highlight) {
contentEl.classList.add(code.intraline.length > 0 ?
'lightHighlight' : 'darkHighlight');
}
var html = util.escapeHTML(code.content);
if (code.highlight && code.intraline.length > 0) {
html = this._addIntralineHighlights(code.content, html,
code.intraline);
}
if (numLines > 1) {
html = this._addNewLines(code.content, html, numLines);
}
html = this._addTabWrappers(code.content, html);
// If the html is equivalent to the text then it didn't get highlighted
// or escaped. Use textContent which is faster than innerHTML.
if (code.content == html) {
contentEl.textContent = code.content;
} else {
contentEl.innerHTML = html;
}
this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
this.$.content.insertBefore(contentEl, beforeContentEl);
},
// Advance `index` by the appropriate number of characters that would
// represent one source code character and return that index. For
// example, for source code '<span>' the escaped html string is
// '&lt;span&gt;'. Advancing from index 0 on the prior html string would
// return 4, since &lt; maps to one source code character ('<').
_advanceChar: function(html, index) {
// Any tags don't count as characters
while (index < html.length &&
html.charCodeAt(index) == CharCode.LESS_THAN) {
while (index < html.length &&
html.charCodeAt(index) != CharCode.GREATER_THAN) {
index++;
}
index++; // skip the ">" itself
}
// An HTML entity (e.g., &lt;) counts as one character.
if (index < html.length &&
html.charCodeAt(index) == CharCode.AMPERSAND) {
while (index < html.length &&
html.charCodeAt(index) != CharCode.SEMICOLON) {
index++;
}
}
return index + 1;
},
_addIntralineHighlights: function(content, html, highlights) {
var startTag = this._highlightStartTag;
var endTag = this._highlightEndTag;
for (var i = 0; i < highlights.length; i++) {
var hl = highlights[i];
var htmlStartIndex = 0;
for (var j = 0; j < hl.startIndex; j++) {
htmlStartIndex = this._advanceChar(html, htmlStartIndex);
}
var htmlEndIndex = 0;
if (hl.endIndex != null) {
for (var j = 0; j < hl.endIndex; j++) {
htmlEndIndex = this._advanceChar(html, htmlEndIndex);
}
} else {
// If endIndex isn't present, continue to the end of the line.
htmlEndIndex = html.length;
}
// The start and end indices could be the same if a highlight is meant
// to start at the end of a line and continue onto the next one.
// Ignore it.
if (htmlStartIndex != htmlEndIndex) {
html = html.slice(0, htmlStartIndex) + startTag +
html.slice(htmlStartIndex, htmlEndIndex) + endTag +
html.slice(htmlEndIndex);
}
}
return html;
},
_addNewLines: function(content, html, numLines) {
var htmlIndex = 0;
var indices = [];
var numChars = 0;
for (var i = 0; i < content.length; i++) {
if (numChars > 0 && numChars % this.prefs.line_length == 0) {
indices.push(htmlIndex);
}
htmlIndex = this._advanceChar(html, htmlIndex);
if (content[i] == '\t') {
numChars += this.prefs.tab_size;
} else {
numChars++;
}
}
var result = html;
var linesLeft = numLines;
// Since the result string is being altered in place, start from the end
// of the string so that the insertion indices are not affected as the
// result string changes.
for (var i = indices.length - 1; i >= 0; i--) {
result = result.slice(0, indices[i]) + this._lineFeedHTML +
result.slice(indices[i]);
linesLeft--;
}
// numLines is the total number of lines this code block should take up.
// Fill in the remaining ones.
for (var i = 0; i < linesLeft; i++) {
result += this._lineFeedHTML;
}
return result;
},
_addTabWrappers: function(content, html) {
// TODO(andybons): CSS tab-size is not supported in IE.
// Force this to be a number to prevent arbitrary injection.
var tabSize = +this.prefs.tab_size;
var htmlStr = '<span class="style-scope gr-diff-side tab ' +
(this.prefs.show_tabs ? 'withIndicator" ' : '" ') +
'style="tab-size:' + tabSize + ';' +
'-moz-tab-size:' + tabSize + ';">\t</span>';
return html.replace(TAB_REGEX, htmlStr);
},
_createElement: function(tagName, className) {
var el = document.createElement(tagName);
// When Shady DOM is being used, these classes are added to account for
// Polymer's polyfill behavior. In order to guarantee sufficient
// specificity within the CSS rules, these are added to every element.
// Since the Polymer DOM utility functions (which would do this
// automatically) are not being used for performance reasons, this is
// done manually.
el.classList.add('style-scope', 'gr-diff-side');
if (!!className) {
el.classList.add(className);
}
return el;
},
});
})();
</script>
</dom-module>

View File

@@ -1,477 +0,0 @@
<!--
Copyright (C) 2015 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-dropdown/iron-dropdown.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-diff.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-diff-view">
<template>
<style>
:host {
background-color: var(--view-background-color);
display: block;
}
h3 {
margin-top: 1em;
padding: .75em var(--default-horizontal-margin);
}
.reviewed {
display: inline-block;
margin: 0 .25em;
vertical-align: .15em;
}
.jumpToFileContainer {
display: inline-block;
}
.mobileJumpToFileContainer {
display: none;
}
.downArrow {
display: inline-block;
font-size: .6em;
vertical-align: middle;
}
.dropdown-trigger {
color: #00e;
cursor: pointer;
padding: 0;
}
.dropdown-content {
background-color: #fff;
box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
}
.dropdown-content a {
cursor: pointer;
display: block;
font-weight: normal;
padding: .3em .5em;
}
.dropdown-content a:before {
color: #ccc;
content: attr(data-key-nav);
display: inline-block;
margin-right: .5em;
width: .3em;
}
.dropdown-content a:hover {
background-color: #00e;
color: #fff;
}
.dropdown-content a[selected] {
color: #000;
font-weight: bold;
pointer-events: none;
text-decoration: none;
}
.dropdown-content a[selected]:hover {
background-color: #fff;
color: #000;
}
gr-button {
font: inherit;
padding: .3em 0;
text-decoration: none;
}
@media screen and (max-width: 50em) {
.dash {
display: none;
}
.reviewed {
vertical-align: -.1em;
}
.jumpToFileContainer {
display: none;
}
.mobileJumpToFileContainer {
display: block;
width: 100%;
}
}
</style>
<gr-ajax id="changeDetailXHR"
auto
url="[[_computeChangeDetailPath(_changeNum)]]"
params="[[_computeChangeDetailQueryParams()]]"
last-response="{{_change}}"></gr-ajax>
<gr-ajax id="filesXHR"
auto
url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
on-response="_handleFilesResponse"></gr-ajax>
<gr-ajax id="configXHR"
auto
url="[[_computeProjectConfigPath(_change.project)]]"
last-response="{{_projectConfig}}"></gr-ajax>
<h3>
<a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]">
[[_changeNum]]</a><span>:</span>
<span>[[_change.subject]]</span>
<span class="dash"></span>
<input id="reviewed"
class="reviewed"
type="checkbox"
on-change="_handleReviewedChange"
hidden$="[[!_loggedIn]]" hidden>
<div class="jumpToFileContainer">
<gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
<span>[[_computeFileDisplayName(_path)]]</span>
<span class="downArrow">&#9660;</span>
</gr-button>
<iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
<div class="dropdown-content">
<template is="dom-repeat" items="[[_fileList]]" as="path">
<a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]"
selected$="[[_computeFileSelected(path, _path)]]"
data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
on-tap="_handleFileTap">
[[_computeFileDisplayName(path)]]
</a>
</template>
</div>
</iron-dropdown>
</div>
<div class="mobileJumpToFileContainer">
<select on-change="_handleMobileSelectChange">
<template is="dom-repeat" items="[[_fileList]]" as="path">
<option
value$="[[path]]"
selected$="[[_computeFileSelected(path, _path)]]">
[[_computeFileDisplayName(path)]]
</option>
</template>
</select>
</div>
</h3>
<gr-diff id="diff"
change-num="[[_changeNum]]"
prefs="{{prefs}}"
patch-range="[[_patchRange]]"
path="[[_path]]"
project-config="[[_projectConfig]]"
available-patches="[[_computeAvailablePatches(_change.revisions)]]"
on-render="_handleDiffRender">
</gr-diff>
</template>
<script>
(function() {
'use strict';
var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
Polymer({
is: 'gr-diff-view',
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
properties: {
prefs: {
type: Object,
notify: true,
},
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
changeViewState: {
type: Object,
notify: true,
value: function() { return {}; },
},
_patchRange: Object,
_change: Object,
_changeNum: String,
_diff: Object,
_fileList: {
type: Array,
value: function() { return []; },
},
_path: {
type: String,
observer: '_pathChanged',
},
_loggedIn: {
type: Boolean,
value: false,
},
_xhrPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
if (this._loggedIn) {
this._setReviewed(true);
}
}.bind(this));
},
attached: function() {
if (this._path) {
this.fire('title-change',
{title: this._computeFileDisplayName(this._path)});
}
window.addEventListener('resize', this._boundWindowResizeHandler);
},
detached: function() {
window.removeEventListener('resize', this._boundWindowResizeHandler);
},
_handleReviewedChange: function(e) {
this._setReviewed(Polymer.dom(e).rootTarget.checked);
},
_setReviewed: function(reviewed) {
this.$.reviewed.checked = reviewed;
var method = reviewed ? 'PUT' : 'DELETE';
var url = this.changeBaseURL(this._changeNum,
this._patchRange.patchNum) + '/files/' +
encodeURIComponent(this._path) + '/reviewed';
this._send(method, url).catch(function(err) {
alert('Couldnt change file review status. Check the console ' +
'and contact the PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 219: // '['
e.preventDefault();
this._navToFile(this._fileList, -1);
break;
case 221: // ']'
e.preventDefault();
this._navToFile(this._fileList, 1);
break;
case 78: // 'n'
if (e.shiftKey) {
this.$.diff.scrollToNextCommentThread();
} else {
this.$.diff.scrollToNextDiffChunk();
}
break;
case 80: // 'p'
if (e.shiftKey) {
this.$.diff.scrollToPreviousCommentThread();
} else {
this.$.diff.scrollToPreviousDiffChunk();
}
break;
case 65: // 'a'
if (!this._loggedIn) { return; }
this.set('changeViewState.showReplyDialog', true);
/* falls through */ // required by JSHint
case 85: // 'u'
if (this._changeNum && this._patchRange.patchNum) {
e.preventDefault();
page.show(this._computeChangePath(
this._changeNum,
this._patchRange.patchNum,
this._change && this._change.revisions));
}
break;
case 188: // ','
this.$.diff.showDiffPreferences();
break;
}
},
_handleDiffRender: function() {
if (window.location.hash.length > 0) {
this.$.diff.scrollToLine(
parseInt(window.location.hash.substring(1), 10));
}
},
_navToFile: function(fileList, direction) {
if (fileList.length == 0) { return; }
var idx = fileList.indexOf(this._path) + direction;
if (idx < 0 || idx > fileList.length - 1) {
page.show(this._computeChangePath(
this._changeNum,
this._patchRange.patchNum,
this._change && this._change.revisions));
return;
}
page.show(this._computeDiffURL(this._changeNum,
this._patchRange,
fileList[idx]));
},
_paramsChanged: function(value) {
if (value.view != this.tagName.toLowerCase()) { return; }
this._changeNum = value.changeNum;
this._patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum || 'PARENT',
};
this._path = value.path;
this.fire('title-change',
{title: this._computeFileDisplayName(this._path)});
// When navigating away from the page, there is a possibility that the
// patch number is no longer a part of the URL (say when navigating to
// the top-level change info view) and therefore undefined in `params`.
if (!this._patchRange.patchNum) {
return;
}
this.$.diff.reload();
},
_pathChanged: function(path) {
if (this._fileList.length == 0) { return; }
this.set('changeViewState.selectedFileIndex',
this._fileList.indexOf(path));
if (this._loggedIn) {
this._setReviewed(true);
}
},
_computeDiffURL: function(changeNum, patchRange, path) {
var patchStr = patchRange.patchNum;
if (patchRange.basePatchNum != null &&
patchRange.basePatchNum != 'PARENT') {
patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
}
return '/c/' + changeNum + '/' + patchStr + '/' + path;
},
_computeAvailablePatches: function(revisions) {
var patchNums = [];
for (var rev in revisions) {
patchNums.push(revisions[rev]._number);
}
return patchNums.sort(function(a, b) { return a - b; });
},
_computeChangePath: function(changeNum, patchNum, revisions) {
var base = '/c/' + changeNum + '/';
// The change may not have loaded yet, making revisions unavailable.
if (!revisions) {
return base + patchNum;
}
var latestPatchNum = -1;
for (var rev in revisions) {
latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number);
}
if (parseInt(patchNum, 10) != latestPatchNum) {
return base + patchNum;
}
return base;
},
_computeFileDisplayName: function(path) {
return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
},
_computeChangeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeChangeDetailQueryParams: function() {
return {O: this.listChangesOptionsToHex(
this.ListChangesOption.ALL_REVISIONS
)};
},
_computeFilesPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/files';
},
_computeProjectConfigPath: function(project) {
return '/projects/' + encodeURIComponent(project) + '/config';
},
_computeFileSelected: function(path, currentPath) {
return path == currentPath;
},
_computeKeyNav: function(path, selectedPath, fileList) {
var selectedIndex = fileList.indexOf(selectedPath);
if (fileList.indexOf(path) == selectedIndex - 1) {
return '[';
}
if (fileList.indexOf(path) == selectedIndex + 1) {
return ']';
}
return '';
},
_handleFileTap: function(e) {
this.$.dropdown.close();
},
_handleMobileSelectChange: function(e) {
var path = Polymer.dom(e).rootTarget.value;
page.show(
this._computeDiffURL(this._changeNum, this._patchRange, path));
},
_handleFilesResponse: function(e, req) {
this._fileList = Object.keys(e.detail.response).sort();
},
_showDropdownTapHandler: function(e) {
this.$.dropdown.open();
},
_send: function(method, url) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: url,
});
return this._xhrPromise;
},
});
})();
</script>
</dom-module>

View File

@@ -1,857 +0,0 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-diff-preferences.html">
<link rel="import" href="gr-diff-side.html">
<link rel="import" href="gr-overlay.html">
<link rel="import" href="gr-patch-range-select.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-diff">
<template>
<style>
.loading {
padding: 0 var(--default-horizontal-margin) 1em;
color: #666;
}
.header {
display: flex;
justify-content: space-between;
margin: 0 var(--default-horizontal-margin) .75em;
}
.prefsButton {
text-align: right;
}
.diffContainer {
border-bottom: 1px solid #eee;
border-top: 1px solid #eee;
display: flex;
font: 12px var(--monospace-font-family);
overflow-x: auto;
}
gr-diff-side:first-of-type {
--light-highlight-color: #fee;
--dark-highlight-color: #ffd4d4;
}
gr-diff-side:last-of-type {
--light-highlight-color: #efe;
--dark-highlight-color: #d4ffd4;
border-right: 1px solid #ddd;
}
</style>
<gr-ajax id="diffXHR"
url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
last-response="{{_diffResponse}}"
loading="{{_loading}}"></gr-ajax>
<gr-ajax id="baseCommentsXHR"
url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
<gr-ajax id="commentsXHR"
url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
<gr-ajax id="baseDraftsXHR"
url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
<div class="loading" hidden$="[[!_loading]]">Loading...</div>
<div hidden$="[[_loading]]" hidden>
<div class="header">
<gr-patch-range-select
path="[[path]]"
change-num="[[changeNum]]"
patch-range="[[patchRange]]"
available-patches="[[availablePatches]]"></gr-patch-range-select>
<gr-button link
class="prefsButton"
on-tap="_handlePrefsTap"
hidden$="[[!prefs]]"
hidden>Diff View Preferences</gr-button>
</div>
<gr-overlay id="prefsOverlay" with-backdrop>
<gr-diff-preferences
prefs="{{prefs}}"
on-save="_handlePrefsSave"
on-cancel="_handlePrefsCancel"></gr-diff-preferences>
</gr-overlay>
<div class="diffContainer">
<gr-diff-side id="leftDiff"
change-num="[[changeNum]]"
patch-num="[[patchRange.basePatchNum]]"
path="[[path]]"
content="{{_diff.leftSide}}"
prefs="[[prefs]]"
can-comment="[[_loggedIn]]"
project-config="[[projectConfig]]"
on-expand-context="_handleExpandContext"
on-thread-height-change="_handleThreadHeightChange"
on-add-draft="_handleAddDraft"
on-remove-thread="_handleRemoveThread"></gr-diff-side>
<gr-diff-side id="rightDiff"
change-num="[[changeNum]]"
patch-num="[[patchRange.patchNum]]"
path="[[path]]"
content="{{_diff.rightSide}}"
prefs="[[prefs]]"
can-comment="[[_loggedIn]]"
project-config="[[projectConfig]]"
on-expand-context="_handleExpandContext"
on-thread-height-change="_handleThreadHeightChange"
on-add-draft="_handleAddDraft"
on-remove-thread="_handleRemoveThread"></gr-diff-side>
</div>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff',
/**
* Fired when the diff is rendered.
*
* @event render
*/
properties: {
availablePatches: Array,
changeNum: String,
/*
* A single object to encompass basePatchNum and patchNum is used
* so that both can be set at once without incremental observers
* firing after each property changes.
*/
patchRange: Object,
path: String,
prefs: {
type: Object,
notify: true,
},
projectConfig: Object,
_prefsReady: {
type: Object,
readOnly: true,
value: function() {
return new Promise(function(resolve) {
this._resolvePrefsReady = resolve;
}.bind(this));
},
},
_baseComments: Array,
_comments: Array,
_drafts: Array,
_baseDrafts: Array,
/**
* Base (left side) comments and drafts grouped by line number.
* Only used for initial rendering.
*/
_groupedBaseComments: {
type: Object,
value: function() { return {}; },
},
/**
* Comments and drafts (right side) grouped by line number.
* Only used for initial rendering.
*/
_groupedComments: {
type: Object,
value: function() { return {}; },
},
_diffResponse: Object,
_diff: {
type: Object,
value: function() { return {}; },
},
_loggedIn: {
type: Boolean,
value: false,
},
_initialRenderComplete: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: true,
},
_savedPrefs: Object,
_diffRequestsPromise: Object, // Used for testing.
_diffPreferencesPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_prefsChanged(prefs.*)',
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
}.bind(this));
},
scrollToLine: function(lineNum) {
// TODO(andybons): Should this always be the right side?
this.$.rightDiff.scrollToLine(lineNum);
},
scrollToNextDiffChunk: function() {
this.$.rightDiff.scrollToNextDiffChunk();
},
scrollToPreviousDiffChunk: function() {
this.$.rightDiff.scrollToPreviousDiffChunk();
},
scrollToNextCommentThread: function() {
this.$.rightDiff.scrollToNextCommentThread();
},
scrollToPreviousCommentThread: function() {
this.$.rightDiff.scrollToPreviousCommentThread();
},
reload: function(changeNum, patchRange, path) {
// If a diff takes a considerable amount of time to render, the previous
// diff can end up showing up while the DOM is constructed. Clear the
// content on a reload to prevent this.
this._diff = {
leftSide: [],
rightSide: [],
};
var promises = [
this._prefsReady,
this.$.diffXHR.generateRequest().completes
];
var basePatchNum = this.patchRange.basePatchNum;
return app.accountReady.then(function() {
promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
this._diffRequestsPromise = Promise.all(promises).then(function() {
this._render();
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
}.bind(this));
},
showDiffPreferences: function() {
this.$.prefsOverlay.open();
},
_prefsChanged: function(changeRecord) {
if (this._initialRenderComplete) {
this._render();
}
this._resolvePrefsReady(changeRecord.base);
},
_render: function() {
this._groupCommentsAndDrafts();
this._processContent();
// Allow for the initial rendering to complete before firing the event.
this.async(function() {
this.fire('render', null, {bubbles: false});
}.bind(this), 1);
this._initialRenderComplete = true;
},
_getCommentsAndDrafts: function(basePatchNum, loggedIn) {
function onlyParent(c) { return c.side == 'PARENT'; }
function withoutParent(c) { return c.side != 'PARENT'; }
var promises = [];
var commentsPromise = this.$.commentsXHR.generateRequest().completes;
promises.push(commentsPromise.then(function(req) {
var comments = req.response[this.path] || [];
if (basePatchNum == 'PARENT') {
this._baseComments = comments.filter(onlyParent);
}
this._comments = comments.filter(withoutParent);
}.bind(this)));
if (basePatchNum != 'PARENT') {
commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
promises.push(commentsPromise.then(function(req) {
this._baseComments =
(req.response[this.path] || []).filter(withoutParent);
}.bind(this)));
}
if (!loggedIn) {
this._baseDrafts = [];
this._drafts = [];
return Promise.all(promises);
}
var draftsPromise = this.$.draftsXHR.generateRequest().completes;
promises.push(draftsPromise.then(function(req) {
var drafts = req.response[this.path] || [];
if (basePatchNum == 'PARENT') {
this._baseDrafts = drafts.filter(onlyParent);
}
this._drafts = drafts.filter(withoutParent);
}.bind(this)));
if (basePatchNum != 'PARENT') {
draftsPromise = this.$.baseDraftsXHR.generateRequest().completes;
promises.push(draftsPromise.then(function(req) {
this._baseDrafts =
(req.response[this.path] || []).filter(withoutParent);
}.bind(this)));
}
return Promise.all(promises);
},
_computeDiffPath: function(changeNum, patchNum, path) {
return this.changeBaseURL(changeNum, patchNum) + '/files/' +
encodeURIComponent(path) + '/diff';
},
_computeCommentsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/comments';
},
_computeDraftsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/drafts';
},
_computeDiffQueryParams: function(basePatchNum) {
var params = {
context: 'ALL',
intraline: null
};
if (basePatchNum != 'PARENT') {
params.base = basePatchNum;
}
return params;
},
_handlePrefsTap: function(e) {
e.preventDefault();
// TODO(andybons): This is not supported in IE. Implement a polyfill.
// NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
// an object as a value, it must be marked enumerable.
this._savedPrefs = Object.assign({}, this.prefs);
this.$.prefsOverlay.open();
},
_handlePrefsSave: function(e) {
e.stopPropagation();
var el = Polymer.dom(e).rootTarget;
el.disabled = true;
app.accountReady.then(function() {
if (!this._loggedIn) {
el.disabled = false;
this.$.prefsOverlay.close();
return;
}
this._saveDiffPreferences().then(function() {
this.$.prefsOverlay.close();
el.disabled = false;
}.bind(this)).catch(function(err) {
el.disabled = false;
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
}.bind(this));
},
_saveDiffPreferences: function() {
var xhr = document.createElement('gr-request');
this._diffPreferencesPromise = xhr.send({
method: 'PUT',
url: '/accounts/self/preferences.diff',
body: this.prefs,
});
return this._diffPreferencesPromise;
},
_handlePrefsCancel: function(e) {
e.stopPropagation();
this.prefs = this._savedPrefs;
this.$.prefsOverlay.close();
},
_handleExpandContext: function(e) {
var ctx = e.detail.context;
var contextControlIndex = -1;
for (var i = ctx.start; i <= ctx.end; i++) {
this._diff.leftSide[i].hidden = false;
this._diff.rightSide[i].hidden = false;
if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
contextControlIndex = i;
}
}
this._diff.leftSide[contextControlIndex].hidden = true;
this._diff.rightSide[contextControlIndex].hidden = true;
this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
},
_handleThreadHeightChange: function(e) {
var index = e.detail.index;
var diffEl = Polymer.dom(e).rootTarget;
var otherSide = diffEl == this.$.leftDiff ?
this.$.rightDiff : this.$.leftDiff;
var threadHeight = e.detail.height;
var otherSideHeight;
if (otherSide.content[index].type == 'COMMENT_THREAD') {
otherSideHeight = otherSide.getRowNaturalHeight(index);
} else {
otherSideHeight = otherSide.getRowHeight(index);
}
var maxHeight = Math.max(threadHeight, otherSideHeight);
this.$.leftDiff.setRowHeight(index, maxHeight);
this.$.rightDiff.setRowHeight(index, maxHeight);
},
_handleAddDraft: function(e) {
var insertIndex = e.detail.index + 1;
var diffEl = Polymer.dom(e).rootTarget;
var content = diffEl.content;
if (content[insertIndex] &&
content[insertIndex].type == 'COMMENT_THREAD') {
// A thread is already here. Do nothing.
return;
}
var comment = {
type: 'COMMENT_THREAD',
comments: [{
__draft: true,
__draftID: Math.random().toString(36),
line: e.detail.line,
path: this.path,
}]
};
if (diffEl == this.$.leftDiff &&
this.patchRange.basePatchNum == 'PARENT') {
comment.comments[0].side = 'PARENT';
comment.patchNum = this.patchRange.patchNum;
}
if (content[insertIndex] &&
content[insertIndex].type == 'FILLER') {
content[insertIndex] = comment;
diffEl.rowUpdated(insertIndex);
} else {
content.splice(insertIndex, 0, comment);
diffEl.rowInserted(insertIndex);
}
var otherSide = diffEl == this.$.leftDiff ?
this.$.rightDiff : this.$.leftDiff;
if (otherSide.content[insertIndex] == null ||
otherSide.content[insertIndex].type != 'COMMENT_THREAD') {
otherSide.content.splice(insertIndex, 0, {
type: 'FILLER',
});
otherSide.rowInserted(insertIndex);
}
},
_handleRemoveThread: function(e) {
var diffEl = Polymer.dom(e).rootTarget;
var otherSide = diffEl == this.$.leftDiff ?
this.$.rightDiff : this.$.leftDiff;
var index = e.detail.index;
if (otherSide.content[index].type == 'FILLER') {
otherSide.content.splice(index, 1);
otherSide.rowRemoved(index);
diffEl.content.splice(index, 1);
diffEl.rowRemoved(index);
} else if (otherSide.content[index].type == 'COMMENT_THREAD') {
diffEl.content[index] = {type: 'FILLER'};
diffEl.rowUpdated(index);
var height = otherSide.setRowNaturalHeight(index);
diffEl.setRowHeight(index, height);
} else {
throw Error('A thread cannot be opposite anything but filler or ' +
'another thread');
}
},
_processContent: function() {
var leftSide = [];
var rightSide = [];
var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
var ctx = {
hidingLines: false,
lastNumLinesHidden: 0,
left: {
lineNum: initialLineNum,
},
right: {
lineNum: initialLineNum,
}
};
var content = this._breakUpCommonChunksWithComments(ctx,
this._diffResponse.content);
var context = this.prefs.context;
if (context == -1) {
// Show the entire file.
context = Infinity;
}
for (var i = 0; i < content.length; i++) {
if (i == 0) {
ctx.skipRange = [0, context];
} else if (i == content.length - 1) {
ctx.skipRange = [context, 0];
} else {
ctx.skipRange = [context, context];
}
ctx.diffChunkIndex = i;
this._addDiffChunk(ctx, content[i], leftSide, rightSide);
}
this._diff = {
leftSide: leftSide,
rightSide: rightSide,
};
},
// In order to show comments out of the bounds of the selected context,
// treat them as diffs within the model so that the content (and context
// surrounding it) renders correctly.
_breakUpCommonChunksWithComments: function(ctx, content) {
var result = [];
var leftLineNum = ctx.left.lineNum;
var rightLineNum = ctx.right.lineNum;
for (var i = 0; i < content.length; i++) {
if (!content[i].ab) {
result.push(content[i]);
if (content[i].a) {
leftLineNum += content[i].a.length;
}
if (content[i].b) {
rightLineNum += content[i].b.length;
}
continue;
}
var chunk = content[i].ab;
var currentChunk = {ab: []};
for (var j = 0; j < chunk.length; j++) {
leftLineNum++;
rightLineNum++;
if (this._groupedBaseComments[leftLineNum] == null &&
this._groupedComments[rightLineNum] == null) {
currentChunk.ab.push(chunk[j]);
} else {
if (currentChunk.ab && currentChunk.ab.length > 0) {
result.push(currentChunk);
currentChunk = {ab: []};
}
// Append an annotation to indicate that this line should not be
// highlighted even though it's implied with both `a` and `b`
// defined. This is needed since there may be two lines that
// should be highlighted but are equal (blank lines, for example).
result.push({
__noHighlight: true,
a: [chunk[j]],
b: [chunk[j]],
});
}
}
if (currentChunk.ab != null && currentChunk.ab.length > 0) {
result.push(currentChunk);
}
}
return result;
},
_groupCommentsAndDrafts: function() {
this._baseDrafts.forEach(function(d) { d.__draft = true; });
this._drafts.forEach(function(d) { d.__draft = true; });
var allLeft = this._baseComments.concat(this._baseDrafts);
var allRight = this._comments.concat(this._drafts);
var leftByLine = {};
var rightByLine = {};
var mapFunc = function(byLine) {
return function(c) {
// File comments/drafts are grouped with line 1 for now.
var line = c.line || 1;
if (byLine[line] == null) {
byLine[line] = [];
}
byLine[line].push(c);
};
};
allLeft.forEach(mapFunc(leftByLine));
allRight.forEach(mapFunc(rightByLine));
this._groupedBaseComments = leftByLine;
this._groupedComments = rightByLine;
},
_addContextControl: function(ctx, leftSide, rightSide) {
var numLinesHidden = ctx.lastNumLinesHidden;
var leftStart = leftSide.length - numLinesHidden;
var leftEnd = leftSide.length;
var rightStart = rightSide.length - numLinesHidden;
var rightEnd = rightSide.length;
if (leftStart != rightStart || leftEnd != rightEnd) {
throw Error(
'Left and right ranges for context control should be equal:' +
'Left: [' + leftStart + ', ' + leftEnd + '] ' +
'Right: [' + rightStart + ', ' + rightEnd + ']');
}
var obj = {
type: 'CONTEXT_CONTROL',
numLines: numLinesHidden,
start: leftStart,
end: leftEnd,
};
// NOTE: Be careful, here. This object is meant to be immutable. If the
// object is altered within one side's array it will reflect the
// alterations in another.
leftSide.push(obj);
rightSide.push(obj);
},
_addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
for (var i = 0; i < chunk.ab.length; i++) {
var numLines = Math.ceil(
this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
var hidden = i >= ctx.skipRange[0] &&
i < chunk.ab.length - ctx.skipRange[1];
if (ctx.hidingLines && hidden == false) {
// No longer hiding lines. Add a context control.
this._addContextControl(ctx, leftSide, rightSide);
ctx.lastNumLinesHidden = 0;
}
ctx.hidingLines = hidden;
if (hidden) {
ctx.lastNumLinesHidden++;
}
// Blank lines within a diff content array indicate a newline.
leftSide.push({
type: 'CODE',
hidden: hidden,
content: chunk.ab[i] || '\n',
numLines: numLines,
lineNum: ++ctx.left.lineNum,
});
rightSide.push({
type: 'CODE',
hidden: hidden,
content: chunk.ab[i] || '\n',
numLines: numLines,
lineNum: ++ctx.right.lineNum,
});
this._addCommentsIfPresent(ctx, leftSide, rightSide);
}
if (ctx.lastNumLinesHidden > 0) {
this._addContextControl(ctx, leftSide, rightSide);
}
},
_addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
if (chunk.ab) {
this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
return;
}
var leftHighlights = [];
if (chunk.edit_a) {
leftHighlights =
this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
}
var rightHighlights = [];
if (chunk.edit_b) {
rightHighlights =
this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
}
var aLen = (chunk.a && chunk.a.length) || 0;
var bLen = (chunk.b && chunk.b.length) || 0;
var maxLen = Math.max(aLen, bLen);
for (var i = 0; i < maxLen; i++) {
var hasLeftContent = chunk.a && i < chunk.a.length;
var hasRightContent = chunk.b && i < chunk.b.length;
var leftContent = hasLeftContent ? chunk.a[i] : '';
var rightContent = hasRightContent ? chunk.b[i] : '';
var highlight = !chunk.__noHighlight;
var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
if (hasLeftContent) {
leftSide.push({
type: 'CODE',
content: leftContent || '\n',
numLines: maxNumLines,
lineNum: ++ctx.left.lineNum,
highlight: highlight,
intraline: highlight && leftHighlights.filter(function(hl) {
return hl.contentIndex == i;
}),
});
} else {
leftSide.push({
type: 'FILLER',
numLines: maxNumLines,
});
}
if (hasRightContent) {
rightSide.push({
type: 'CODE',
content: rightContent || '\n',
numLines: maxNumLines,
lineNum: ++ctx.right.lineNum,
highlight: highlight,
intraline: highlight && rightHighlights.filter(function(hl) {
return hl.contentIndex == i;
}),
});
} else {
rightSide.push({
type: 'FILLER',
numLines: maxNumLines,
});
}
this._addCommentsIfPresent(ctx, leftSide, rightSide);
}
},
_addCommentsIfPresent: function(ctx, leftSide, rightSide) {
var leftComments = this._groupedBaseComments[ctx.left.lineNum];
var rightComments = this._groupedComments[ctx.right.lineNum];
if (leftComments) {
var thread = {
type: 'COMMENT_THREAD',
comments: leftComments,
};
if (this.patchRange.basePatchNum == 'PARENT') {
thread.patchNum = this.patchRange.patchNum;
}
leftSide.push(thread);
}
if (rightComments) {
rightSide.push({
type: 'COMMENT_THREAD',
comments: rightComments,
});
}
if (leftComments && !rightComments) {
rightSide.push({type: 'FILLER'});
} else if (!leftComments && rightComments) {
leftSide.push({type: 'FILLER'});
}
this._groupedBaseComments[ctx.left.lineNum] = null;
this._groupedComments[ctx.right.lineNum] = null;
},
// The `highlights` array consists of a list of <skip length, mark length>
// pairs, where the skip length is the number of characters between the
// end of the previous edit and the start of this edit, and the mark
// length is the number of edited characters following the skip. The start
// of the edits is from the beginning of the related diff content lines.
//
// Note that the implied newline character at the end of each line is
// included in the length calculation, and thus it is possible for the
// edits to span newlines.
//
// A line highlight object consists of three fields:
// - contentIndex: The index of the diffChunk `content` field (the line
// being referred to).
// - startIndex: Where the highlight should begin.
// - endIndex: (optional) Where the highlight should end. If omitted, the
// highlight is meant to be a continuation onto the next line.
_normalizeIntralineHighlights: function(content, highlights) {
var contentIndex = 0;
var idx = 0;
var normalized = [];
for (var i = 0; i < highlights.length; i++) {
var line = content[contentIndex] + '\n';
var hl = highlights[i];
var j = 0;
while (j < hl[0]) {
if (idx == line.length) {
idx = 0;
line = content[++contentIndex] + '\n';
continue;
}
idx++;
j++;
}
var lineHighlight = {
contentIndex: contentIndex,
startIndex: idx,
};
j = 0;
while (line && j < hl[1]) {
if (idx == line.length) {
idx = 0;
line = content[++contentIndex] + '\n';
normalized.push(lineHighlight);
lineHighlight = {
contentIndex: contentIndex,
startIndex: idx,
};
continue;
}
idx++;
j++;
}
lineHighlight.endIndex = idx;
normalized.push(lineHighlight);
}
return normalized;
},
_visibleLineLength: function(contents) {
// http://jsperf.com/performance-of-match-vs-split
var numTabs = contents.split('\t').length - 1;
return contents.length - numTabs + (this.prefs.tab_size * numTabs);
},
_maxLinesSpanned: function(left, right) {
return Math.max(
Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
},
});
})();
</script>
</dom-module>

View File

@@ -1,352 +0,0 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-file-list">
<template>
<style>
:host {
display: block;
}
.row {
display: flex;
padding: .1em .25em;
}
.header {
font-weight: bold;
}
.positionIndicator,
.reviewed,
.status {
align-items: center;
display: inline-flex;
}
.reviewed,
.status {
justify-content: center;
width: 1.5em;
}
.positionIndicator {
justify-content: flex-start;
visibility: hidden;
width: 1.25em;
}
.row[selected] {
background-color: #ebf5fb;
}
.row[selected] .positionIndicator {
visibility: visible;
}
.path {
flex: 1;
overflow: hidden;
padding-left: .35em;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.row:not(.header) .path:hover {
text-decoration: underline;
}
.comments,
.stats {
text-align: right;
}
.comments {
min-width: 10em;
}
.stats {
min-width: 7em;
}
.invisible {
visibility: hidden;
}
.row:not(.header) .stats {
font-family: var(--monospace-font-family);
}
.added {
color: #388E3C;
}
.removed {
color: #D32F2F;
}
.reviewed input[type="checkbox"] {
display: inline-block;
}
.drafts {
color: #C62828;
font-weight: bold;
}
@media screen and (max-width: 50em) {
.row[selected] {
background-color: transparent;
}
.positionIndicator,
.stats {
display: none;
}
.reviewed,
.status {
justify-content: flex-start;
}
.comments {
min-width: initial;
}
}
</style>
<gr-ajax id="filesXHR"
url="[[_computeFilesURL(changeNum, patchNum)]]"
on-response="_handleResponse"></gr-ajax>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsURL(changeNum, patchNum)]]"
last-response="{{_drafts}}"></gr-ajax>
<gr-ajax id="reviewedXHR"
url="[[_computeReviewedURL(changeNum, patchNum)]]"
last-response="{{_reviewed}}"></gr-ajax>
</gr-ajax>
<div class="row header">
<div class="positionIndicator"></div>
<div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
<div class="status"></div>
<div class="path">Path</div>
<div class="comments">Comments</div>
<div class="stats">Stats</div>
</div>
<template is="dom-repeat" items="{{files}}" as="file">
<div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
<div class="positionIndicator">&#x25b6;</div>
<div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
<input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
data-path$="[[file.__path]]" on-change="_handleReviewedChange">
</div>
<div class$="[[_computeClass('status', file.__path)]]">
[[_computeFileStatus(file.status)]]
</div>
<a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">
[[_computeFileDisplayName(file.__path)]]
</a>
<div class="comments">
<span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
[[_computeCommentsString(comments, patchNum, file.__path)]]
</div>
<div class$="[[_computeClass('stats', file.__path)]]">
<span class="added">+[[file.lines_inserted]]</span>
<span class="removed">-[[file.lines_deleted]]</span>
</div>
</div>
</template>
</template>
<script>
(function() {
'use strict';
var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
Polymer({
is: 'gr-file-list',
properties: {
patchNum: String,
changeNum: String,
comments: Object,
files: Array,
selectedIndex: {
type: Number,
notify: true,
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_loggedIn: {
type: Boolean,
value: false,
},
_drafts: Object,
_reviewed: {
type: Array,
value: function() { return []; },
},
_xhrPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
reload: function() {
if (!this.changeNum || !this.patchNum) {
return Promise.resolve();
}
return Promise.all([
this.$.filesXHR.generateRequest().completes,
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
if (!app.loggedIn) { return; }
this.$.draftsXHR.generateRequest();
this.$.reviewedXHR.generateRequest();
}.bind(this)),
]);
},
_computeFilesURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/files';
},
_computeCommentsString: function(comments, patchNum, path) {
var patchComments = (comments[path] || []).filter(function(c) {
return c.patch_set == patchNum;
});
var num = patchComments.length;
if (num == 0) { return ''; }
if (num == 1) { return '1 comment'; }
if (num > 1) { return num + ' comments'; }
},
_computeReviewedURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed';
},
_computeReviewed: function(file, _reviewed) {
return _reviewed.indexOf(file.__path) != -1;
},
_handleReviewedChange: function(e) {
var path = Polymer.dom(e).rootTarget.getAttribute('data-path');
var index = this._reviewed.indexOf(path);
var reviewed = index != -1;
if (reviewed) {
this.splice('_reviewed', index, 1);
} else {
this.push('_reviewed', path);
}
var method = reviewed ? 'DELETE' : 'PUT';
var url = this.changeBaseURL(this.changeNum, this.patchNum) +
'/files/' + encodeURIComponent(path) + '/reviewed';
this._send(method, url).catch(function(err) {
alert('Couldnt change file review status. Check the console ' +
'and contact the PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_computeDraftsURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/drafts';
},
_computeDraftsString: function(drafts, path) {
var num = (drafts[path] || []).length;
if (num == 0) { return ''; }
if (num == 1) { return '1 draft'; }
if (num > 1) { return num + ' drafts'; }
},
_handleResponse: function(e, req) {
var result = e.detail.response;
var paths = Object.keys(result).sort();
var files = [];
for (var i = 0; i < paths.length; i++) {
var info = result[paths[i]];
info.__path = paths[i];
info.lines_inserted = info.lines_inserted || 0;
info.lines_deleted = info.lines_deleted || 0;
files.push(info);
}
this.files = files;
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 74: // 'j'
e.preventDefault();
this.selectedIndex =
Math.min(this.files.length - 1, this.selectedIndex + 1);
break;
case 75: // 'k'
e.preventDefault();
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
break;
case 219: // '['
e.preventDefault();
this._openSelectedFile(this.files.length - 1);
break;
case 221: // ']'
e.preventDefault();
this._openSelectedFile(0);
break;
case 13: // <enter>
case 79: // 'o'
e.preventDefault();
this._openSelectedFile();
break;
}
},
_openSelectedFile: function(opt_index) {
if (opt_index != null) {
this.selectedIndex = opt_index;
}
page.show(this._computeDiffURL(this.changeNum, this.patchNum,
this.files[this.selectedIndex].__path));
},
_computeFileSelected: function(index, selectedIndex) {
return index == selectedIndex;
},
_computeFileStatus: function(status) {
return status || 'M';
},
_computeDiffURL: function(changeNum, patchNum, path) {
return '/c/' + changeNum + '/' + patchNum + '/' + path;
},
_computeFileDisplayName: function(path) {
return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
},
_computeClass: function(baseClass, path) {
var classes = [baseClass];
if (path == COMMIT_MESSAGE_PATH) {
classes.push('invisible');
}
return classes.join(' ');
},
_send: function(method, url) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: url,
});
return this._xhrPromise;
},
});
})();
</script>
</dom-module>

View File

@@ -1,101 +0,0 @@
<!--
Copyright (C) 2015 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">
<script src="../scripts/ba-linkify.js"></script>
<script src="../scripts/link-text-parser.js"></script>
<dom-module id="gr-linked-text">
<template>
<style>
:host {
display: block;
}
:host([pre]) span {
white-space: pre-wrap;
word-wrap: break-word;
}
:host([disabled]) a {
color: inherit;
text-decoration: none;
pointer-events: none;
}
</style>
<span id="output"></span>
</template>
<script>
'use strict';
Polymer({
is: 'gr-linked-text',
properties: {
content: {
type: String,
observer: '_contentChanged',
},
pre: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
config: Object,
},
observers: [
'_contentOrConfigChanged(content, config)',
],
_contentChanged: function(content) {
// In the case where the config may not be set (perhaps due to the
// request for it still being in flight), set the content anyway to
// prevent waiting on the config to display the text.
if (this.config != null) { return; }
this.$.output.textContent = content;
},
_contentOrConfigChanged: function(content, config) {
var output = Polymer.dom(this.$.output);
output.textContent = '';
var parser = new GrLinkTextParser(config, function(text, href, html) {
if (href) {
var a = document.createElement('a');
a.href = href;
a.textContent = text;
a.target = '_blank';
output.appendChild(a);
} else if (html) {
var fragment = document.createDocumentFragment();
// Create temporary div to hold the nodes in.
var div = document.createElement('div');
div.innerHTML = html;
while (div.firstChild) {
fragment.appendChild(div.firstChild);
}
output.appendChild(fragment);
} else {
output.appendChild(document.createTextNode(text));
}
});
parser.parse(content);
}
});
</script>
</dom-module>

View File

@@ -1,162 +0,0 @@
<!--
Copyright (C) 2015 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="gr-button.html">
<link rel="import" href="gr-message.html">
<dom-module id="gr-messages-list">
<template>
<style>
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: .35em;
}
.header,
gr-message {
padding: 0 var(--default-horizontal-margin);
}
.highlighted {
animation: 3s fadeOut;
}
@keyframes fadeOut {
0% { background-color: #fff9c4; }
100% { background-color: #fff; }
}
</style>
<div class="header">
<h3>Messages</h3>
<gr-button link on-tap="_handleExpandCollapseTap">
[[_computeExpandCollapseMessage(_expanded)]]
</gr-button>
</div>
<template is="dom-repeat" items="[[messages]]" as="message">
<gr-message
change-num="[[changeNum]]"
message="[[message]]"
comments="[[_computeCommentsForMessage(comments, message, index)]]"
project-config="[[projectConfig]]"
show-reply-button="[[showReplyButtons]]"
on-scroll-to="_handleScrollTo"
data-message-id$="[[message.id]]"></gr-message>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-messages-list',
properties: {
changeNum: Number,
messages: {
type: Array,
value: function() { return []; },
},
comments: Object,
projectConfig: Object,
topMargin: Number,
showReplyButtons: {
type: Boolean,
value: false,
},
_expanded: {
type: Boolean,
value: false,
},
},
scrollToMessage: function(messageID) {
var el = this.$$('[data-message-id="' + messageID + '"]');
if (!el) { return; }
el.expanded = true;
var top = el.offsetTop;
for (var offsetParent = el.offsetParent;
offsetParent;
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
window.scrollTo(0, top - this.topMargin);
this._highlightEl(el);
},
_highlightEl: function(el) {
var highlightedEls =
Polymer.dom(this.root).querySelectorAll('.highlighted');
for (var i = 0; i < highlightedEls.length; i++) {
highlightedEls[i].classList.remove('highlighted');
}
function handleAnimationEnd() {
el.removeEventListener('animationend', handleAnimationEnd);
el.classList.remove('highlighted');
}
el.addEventListener('animationend', handleAnimationEnd);
el.classList.add('highlighted');
},
_handleExpandCollapseTap: function(e) {
e.preventDefault();
this._expanded = !this._expanded;
var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message');
for (var i = 0; i < messageEls.length; i++) {
messageEls[i].expanded = this._expanded;
}
},
_handleScrollTo: function(e) {
this.scrollToMessage(e.detail.message.id);
},
_computeExpandCollapseMessage: function(expanded) {
return expanded ? 'Collapse all' : 'Expand all';
},
_computeCommentsForMessage: function(comments, message, index) {
comments = comments || {};
var messages = this.messages || [];
var msgComments = {};
var mDate = util.parseDate(message.date);
var nextMDate;
if (index < messages.length - 1) {
nextMDate = util.parseDate(messages[index + 1].date);
}
for (var file in comments) {
var fileComments = comments[file];
for (var i = 0; i < fileComments.length; i++) {
var cDate = util.parseDate(fileComments[i].updated);
if (cDate >= mDate) {
if (nextMDate && cDate >= nextMDate) {
continue;
}
msgComments[file] = msgComments[file] || [];
msgComments[file].push(fileComments[i]);
}
}
}
return msgComments;
},
});
})();
</script>
</dom-module>

View File

@@ -1,65 +0,0 @@
<!--
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-overlay-behavior/iron-overlay-behavior.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<dom-module id="gr-overlay">
<template>
<style>
:host {
background: #fff;
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
}
</style>
<content></content>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-overlay',
behaviors: [
Polymer.IronOverlayBehavior,
],
detached: function() {
// For good measure.
Gerrit.KeyboardShortcutBehavior.enabled = true;
},
open: function() {
Gerrit.KeyboardShortcutBehavior.enabled = false;
Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments);
},
close: function() {
Gerrit.KeyboardShortcutBehavior.enabled = true;
Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments);
},
cancel: function() {
Gerrit.KeyboardShortcutBehavior.enabled = true;
Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments);
},
});
})();
</script>
</dom-module>

View File

@@ -1,363 +0,0 @@
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<dom-module id="gr-related-changes-list">
<template>
<style>
:host {
display: block;
}
h3 {
margin: .5em 0 0;
}
section {
margin-bottom: 1em;
}
a {
display: block;
}
.relatedChanges a {
display: inline-block;
}
.strikethrough {
color: #666;
text-decoration: line-through;
}
.status {
color: #666;
font-weight: bold;
}
.notCurrent {
color: #e65100;
}
.indirectAncestor {
color: #33691e;
}
.submittable {
color: #1b5e20;
}
.hidden {
display: none;
}
</style>
<gr-ajax id="relatedXHR"
url="[[_computeRelatedURL(change._number, patchNum)]]"
last-response="{{_relatedResponse}}"></gr-ajax>
<gr-ajax id="submittedTogetherXHR"
url="[[_computeSubmittedTogetherURL(change._number)]]"
last-response="{{_submittedTogether}}"></gr-ajax>
<gr-ajax id="conflictsXHR"
url="/changes/"
params="[[_computeConflictsQueryParams(change._number)]]"
last-response="{{_conflicts}}"></gr-ajax>
<gr-ajax id="cherryPicksXHR"
url="/changes/"
params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]"
last-response="{{_cherryPicks}}"></gr-ajax>
<gr-ajax id="sameTopicXHR"
url="/changes/"
params="[[_computeSameTopicQueryParams(change.topic)]]"
last-response="{{_sameTopic}}"></gr-ajax>
<div hidden$="[[!_loading]]">Loading...</div>
<section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
<h4>Relation Chain</h4>
<template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change">
<div>
<a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.commit.subject]]
</a>
<span class$="[[_computeChangeStatusClass(change)]]">
([[_computeChangeStatus(change)]])
</span>
</div>
</template>
</section>
<section hidden$="[[!_submittedTogether.length]]" hidden>
<h4>Submitted together</h4>
<template is="dom-repeat" items="[[_submittedTogether]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.project]]: [[change.branch]]: [[change.subject]]
</a>
</template>
</section>
<section hidden$="[[!_sameTopic.length]]" hidden>
<h4>Same topic</h4>
<template is="dom-repeat" items="[[_sameTopic]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.project]]: [[change.branch]]: [[change.subject]]
</a>
</template>
</section>
<section hidden$="[[!_conflicts.length]]" hidden>
<h4>Merge conflicts</h4>
<template is="dom-repeat" items="[[_conflicts]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.subject]]
</a>
</template>
</section>
<section hidden$="[[!_cherryPicks.length]]" hidden>
<h4>Cherry picks</h4>
<template is="dom-repeat" items="[[_cherryPicks]]" as="change">
<a href$="[[_computeChangeURL(change._number)]]"
class$="[[_computeLinkClass(change)]]">
[[change.subject]]
</a>
</template>
</section>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-related-changes-list',
properties: {
change: Object,
patchNum: String,
serverConfig: {
type: Object,
observer: '_serverConfigChanged',
},
hidden: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
_loading: Boolean,
_resolveServerConfigReady: Function,
_serverConfigReady: {
type: Object,
value: function() {
return new Promise(function(resolve) {
this._resolveServerConfigReady = resolve;
}.bind(this));
}
},
_connectedRevisions: {
type: Array,
computed: '_computeConnectedRevisions(change, patchNum, ' +
'_relatedResponse.changes)',
},
_relatedResponse: Object,
_submittedTogether: Array,
_conflicts: Array,
_cherryPicks: Array,
_sameTopic: Array,
},
behaviors: [
Gerrit.RESTClientBehavior,
],
observers: [
'_resultsChanged(_relatedResponse.changes, _submittedTogether, ' +
'_conflicts, _cherryPicks, _sameTopic)',
],
reload: function() {
if (!this.change || !this.patchNum) {
return Promise.resolve();
}
this._loading = true;
var promises = [
this.$.relatedXHR.generateRequest().completes,
this.$.submittedTogetherXHR.generateRequest().completes,
this.$.conflictsXHR.generateRequest().completes,
this.$.cherryPicksXHR.generateRequest().completes,
];
return this._serverConfigReady.then(function() {
if (this.change.topic &&
!this.serverConfig.change.submit_whole_topic) {
return this.$.sameTopicXHR.generateRequest().completes;
} else {
this._sameTopic = [];
}
return Promise.resolve();
}.bind(this)).then(Promise.all(promises)).then(function() {
this._loading = false;
}.bind(this));
},
_computeRelatedURL: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/related';
},
_computeSubmittedTogetherURL: function(changeNum) {
return this.changeBaseURL(changeNum) + '/submitted_together';
},
_computeConflictsQueryParams: function(changeNum) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
return {
O: options,
q: 'status:open is:mergeable conflicts:' + changeNum,
};
},
_computeCherryPicksQueryParams: function(project, changeID, changeNum) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT
);
var query = [
'project:' + project,
'change:' + changeID,
'-change:' + changeNum,
'-is:abandoned',
].join(' ');
return {
O: options,
q: query
}
},
_computeSameTopicQueryParams: function(topic) {
var options = this.listChangesOptionsToHex(
this.ListChangesOption.LABELS,
this.ListChangesOption.CURRENT_REVISION,
this.ListChangesOption.CURRENT_COMMIT,
this.ListChangesOption.DETAILED_LABELS
);
return {
O: options,
q: 'status:open topic:' + topic,
};
},
_computeChangeURL: function(changeNum, patchNum) {
var urlStr = '/c/' + changeNum;
if (patchNum != null) {
urlStr += '/' + patchNum;
}
return urlStr;
},
_computeLinkClass: function(change) {
if (change.status == this.ChangeStatus.ABANDONED) {
return 'strikethrough';
}
},
_computeChangeStatusClass: function(change) {
var classes = ['status'];
if (change._revision_number != change._current_revision_number) {
classes.push('notCurrent');
} else if (this._isIndirectAncestor(change)) {
classes.push('indirectAncestor');
} else if (change.submittable) {
classes.push('submittable');
} else if (change.status == this.ChangeStatus.NEW) {
classes.push('hidden');
}
return classes.join(' ');
},
_computeChangeStatus: function(change) {
switch (change.status) {
case this.ChangeStatus.MERGED:
return 'Merged';
case this.ChangeStatus.ABANDONED:
return 'Abandoned';
case this.ChangeStatus.DRAFT:
return 'Draft';
}
if (change._revision_number != change._current_revision_number) {
return 'Not current';
} else if (this._isIndirectAncestor(change)) {
return 'Indirect ancestor';
} else if (change.submittable) {
return 'Submittable';
}
return ''
},
_serverConfigChanged: function(config) {
this._resolveServerConfigReady(config);
},
_resultsChanged: function(related, submittedTogether, conflicts,
cherryPicks, sameTopic) {
var results = [
related,
submittedTogether,
conflicts,
cherryPicks,
sameTopic
];
for (var i = 0; i < results.length; i++) {
if (results[i].length > 0) {
this.hidden = false;
return;
}
}
this.hidden = true;
},
_isIndirectAncestor: function(change) {
return this._connectedRevisions.indexOf(change.commit.commit) == -1;
},
_computeConnectedRevisions: function(change, patchNum, relatedChanges) {
var connected = [];
var changeRevision;
for (var rev in change.revisions) {
if (change.revisions[rev]._number == patchNum) {
changeRevision = rev;
}
}
var commits = relatedChanges.map(function(c) { return c.commit; });
var pos = commits.length - 1;
while (pos >= 0) {
var commit = commits[pos].commit;
connected.push(commit);
if (commit == changeRevision) {
break;
}
pos--;
}
while (pos >= 0) {
for (var i = 0; i < commits[pos].parents.length; i++) {
if (connected.indexOf(commits[pos].parents[i].commit) != -1) {
connected.push(commits[pos].commit);
break;
}
}
--pos;
}
return connected;
},
});
})();
</script>
</dom-module>

View File

@@ -1,307 +0,0 @@
<!--
Copyright (C) 2015 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-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../bower_components/iron-selector/iron-selector.html">
<link rel="import" href="../behaviors/rest-client-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-reply-dialog">
<style>
:host {
display: block;
max-height: 90vh;
}
:host([disabled]) {
pointer-events: none;
}
:host([disabled]) .container {
opacity: .5;
}
.container {
display: flex;
flex-direction: column;
max-height: 90vh;
}
section {
border-top: 1px solid #ddd;
padding: .5em .75em;
}
.textareaContainer,
.labelsContainer,
.actionsContainer {
flex-shrink: 0;
}
.textareaContainer {
position: relative;
}
iron-autogrow-textarea {
padding: 0;
font-family: var(--monospace-font-family);
}
.message {
border: none;
width: 100%;
}
.labelContainer:not(:first-of-type) {
margin-top: .5em;
}
.labelName {
display: inline-block;
width: 7em;
margin-right: .5em;
white-space: nowrap;
}
iron-selector {
display: inline-flex;
}
iron-selector > gr-button {
margin-right: .25em;
}
iron-selector > gr-button:first-of-type {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
iron-selector > gr-button:last-of-type {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
iron-selector > gr-button.iron-selected {
background-color: #ddd;
}
.draftsContainer {
overflow-y: auto;
}
.draftsContainer h3 {
margin-top: .25em;
}
.actionsContainer {
display: flex;
justify-content: space-between;
}
.action:link,
.action:visited {
color: #00e;
}
</style>
<template>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsURL(changeNum)]]"
last-response="{{_drafts}}"></gr-ajax>
<div class="container">
<section class="textareaContainer">
<iron-autogrow-textarea
id="textarea"
class="message"
placeholder="Say something..."
disabled="{{disabled}}"
rows="4"
max-rows="15"
bind-value="{{draft}}"></iron-autogrow-textarea>
</section>
<section class="labelsContainer">
<template is="dom-repeat"
items="[[_computeLabelArray(permittedLabels)]]" as="label">
<div class="labelContainer">
<span class="labelName">[[label]]</span>
<iron-selector data-label$="[[label]]"
selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
<template is="dom-repeat"
items="[[_computePermittedLabelValues(permittedLabels, label)]]"
as="value">
<gr-button data-value$="[[value]]">[[value]]</gr-button>
</template>
</iron-selector>
</div>
</template>
</section>
<section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
<h3>[[_computeDraftsTitle(_drafts)]]</h3>
<gr-comment-list
comments="[[_drafts]]"
change-num="[[changeNum]]"
patch-num="[[patchNum]]"></gr-comment-list>
</section>
<section class="actionsContainer">
<gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
<gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button>
</section>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-reply-dialog',
/**
* Fired when a reply is successfully sent.
*
* @event send
*/
/**
* Fired when the user presses the cancel button.
*
* @event cancel
*/
properties: {
changeNum: String,
patchNum: String,
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
draft: {
type: String,
value: '',
},
labels: Object,
permittedLabels: Object,
_account: Object,
_drafts: Object,
_xhrPromise: Object, // Used for testing.
},
behaviors: [
Gerrit.RESTClientBehavior,
],
ready: function() {
app.accountReady.then(function() {
this._account = app.account;
}.bind(this));
},
reload: function() {
return this.$.draftsXHR.generateRequest().completes;
},
focus: function() {
this.async(function() {
this.$.textarea.textarea.focus();
}.bind(this));
},
_computeDraftsURL: function(changeNum) {
return '/changes/' + changeNum + '/drafts';
},
_computeHideDraftList: function(drafts) {
return Object.keys(drafts || {}).length == 0;
},
_computeDraftsTitle: function(drafts) {
var total = 0;
for (var file in drafts) {
total += drafts[file].length;
}
if (total == 0) { return ''; }
if (total == 1) { return '1 Draft'; }
if (total > 1) { return total + ' Drafts'; }
},
_computeLabelArray: function(labelsObj) {
return Object.keys(labelsObj).sort();
},
_computeIndexOfLabelValue: function(
labels, permittedLabels, labelName, account) {
var t = labels[labelName];
if (!t) { return null; }
var labelValue = t.default_value;
// Is there an existing vote for the current user? If so, use that.
var votes = labels[labelName];
if (votes.all && votes.all.length > 0) {
for (var i = 0; i < votes.all.length; i++) {
if (votes.all[i]._account_id == account._account_id) {
labelValue = votes.all[i].value;
break;
}
}
}
var len = permittedLabels[labelName] != null ?
permittedLabels[labelName].length : 0;
for (var i = 0; i < len; i++) {
var val = parseInt(permittedLabels[labelName][i], 10);
if (val == labelValue) {
return i;
}
}
return null;
},
_computePermittedLabelValues: function(permittedLabels, label) {
return permittedLabels[label];
},
_cancelTapHandler: function(e) {
e.preventDefault();
this._drafts = null;
this.fire('cancel', null, {bubbles: false});
},
_sendTapHandler: function(e) {
e.preventDefault();
var obj = {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {},
};
for (var label in this.permittedLabels) {
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
selectedVal = parseInt(selectedVal, 10);
obj.labels[label] = selectedVal;
}
if (this.draft != null) {
obj.message = this.draft;
}
this.disabled = true;
this._send(obj).then(function(req) {
this.fire('send', null, {bubbles: false});
this.draft = '';
this.disabled = false;
this._drafts = null;
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_send: function(payload) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: 'POST',
url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review',
body: payload,
});
return this._xhrPromise;
},
});
})();
</script>
</dom-module>

View File

@@ -1,49 +0,0 @@
<!--
Copyright (C) 2015 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-ajax/iron-request.html">
<dom-module id="gr-request">
<template>
<iron-request id="xhr"></iron-request>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-request',
hostAttributes: {
hidden: true
},
send: function(options) {
options.headers = options.headers || {};
if (options.body != null) {
options.headers['content-type'] =
options.headers['content-type'] || 'application/json';
}
options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] ||
util.getCookie('XSRF_TOKEN');
options.jsonPrefix = options.jsonPrefix || ')]}\'';
return this.$.xhr.send(options);
},
});
})();
</script>
</dom-module>

View File

@@ -1,450 +0,0 @@
<!--
Copyright (C) 2015 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="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-reviewer-list">
<style>
:host {
display: block;
}
:host([disabled]) {
opacity: .8;
pointer-events: none;
}
.autocompleteContainer {
position: relative;
}
.inputContainer {
display: flex;
margin-top: .25em;
}
.inputContainer input {
flex: 1;
font: inherit;
}
.dropdown {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
position: absolute;
left: 0;
top: 100%;
}
.dropdown .reviewer {
cursor: pointer;
padding: .5em .75em;
}
.dropdown .reviewer[selected] {
background-color: #ccc;
}
.remove,
.cancel {
color: #999;
}
.remove {
font-size: .9em;
}
.cancel {
font-size: 2em;
line-height: 1;
padding: 0 .15em;
text-decoration: none;
}
</style>
<template>
<gr-ajax id="autocompleteXHR"
url="[[_computeAutocompleteURL(change)]]"
params="[[_computeAutocompleteParams(_inputVal)]]"
on-response="_handleResponse"></gr-ajax>
<template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
<div class="reviewer">
<gr-account-link account="[[reviewer]]" show-email></gr-account-link>
<gr-button link
class="remove"
data-account-id$="[[reviewer._account_id]]"
on-tap="_handleRemoveTap"
hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom>
</div>
</template>
<div class="controlsContainer" hidden$="[[!mutable]]">
<div class="autocompleteContainer" hidden$="[[!_showInput]]">
<div class="inputContainer">
<input is="iron-input" id="input"
bind-value="{{_inputVal}}" disabled$="[[disabled]]">
<gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button>
</div>
<div class="dropdown" hidden$="[[_hideAutocomplete]]">
<template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer">
<div class="reviewer"
data-index$="[[index]]"
on-mouseenter="_handleMouseEnterItem"
on-tap="_handleItemTap"
selected$="[[_computeSelected(index, _selectedIndex)]]">
<template is="dom-if" if="[[reviewer.account]]">
<gr-account-label
account="[[reviewer.account]]" show-email></gr-account-label>
</template>
<template is="dom-if" if="[[reviewer.group]]">
<span>[[reviewer.group.name]] (group)</span>
</template>
</div>
</template>
</div>
</div>
<gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
hidden$="[[_showInput]]">Add reviewer</gr-button>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-reviewer-list',
properties: {
change: Object,
mutable: {
type: Boolean,
value: false,
},
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
suggestFrom: {
type: Number,
value: 3,
},
_reviewers: {
type: Array,
value: function() { return []; },
},
_autocompleteData: {
type: Array,
value: function() { return []; },
observer: '_autocompleteDataChanged',
},
_inputVal: {
type: String,
value: '',
observer: '_inputValChanged',
},
_inputRequestHandle: Number,
_inputRequestTimeout: {
type: Number,
value: 250,
},
_showInput: {
type: Boolean,
value: false,
},
_hideAutocomplete: {
type: Boolean,
value: true,
observer: '_hideAutocompleteChanged',
},
_selectedIndex: {
type: Number,
value: 0,
},
_boundBodyClickHandler: {
type: Function,
value: function() {
return this._handleBodyClick.bind(this);
},
},
// Used for testing.
_lastAutocompleteRequest: Object,
_xhrPromise: Object,
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
],
observers: [
'_reviewersChanged(change.reviewers.*, change.owner)',
],
detached: function() {
this._clearInputRequestHandle();
},
_clearInputRequestHandle: function() {
if (this._inputRequestHandle != null) {
this.cancelAsync(this._inputRequestHandle);
this._inputRequestHandle = null;
}
},
_reviewersChanged: function(changeRecord, owner) {
var result = [];
var reviewers = changeRecord.base;
for (var key in reviewers) {
if (key == 'REVIEWER' || key == 'CC') {
result = result.concat(reviewers[key]);
}
}
this._reviewers = result.filter(function(reviewer) {
return reviewer._account_id != owner._account_id;
});
},
_computeCanRemoveReviewer: function(reviewer, mutable) {
if (!mutable) { return false; }
for (var i = 0; i < this.change.removable_reviewers.length; i++) {
if (this.change.removable_reviewers[i]._account_id ==
reviewer._account_id) {
return true;
}
}
return false;
},
_computeAutocompleteURL: function(change) {
return '/changes/' + change._number + '/suggest_reviewers';
},
_computeAutocompleteParams: function(inputVal) {
return {
n: 10, // Return max 10 results
q: inputVal,
};
},
_computeSelected: function(index, selectedIndex) {
return index == selectedIndex;
},
_handleResponse: function(e) {
this._autocompleteData = e.detail.response.filter(function(reviewer) {
var account = reviewer.account;
if (!account) { return true; }
for (var i = 0; i < this._reviewers.length; i++) {
if (account._account_id == this.change.owner._account_id ||
account._account_id == this._reviewers[i]._account_id) {
return false;
}
}
return true;
}, this);
},
_handleBodyClick: function(e) {
var eventPath = Polymer.dom(e).path;
for (var i = 0; i < eventPath.length; i++) {
if (eventPath[i] == this) {
return;
}
}
this._selectedIndex = -1;
this._autocompleteData = [];
},
_handleRemoveTap: function(e) {
e.preventDefault();
var target = Polymer.dom(e).rootTarget;
var accountID = parseInt(target.getAttribute('data-account-id'), 10);
this._send('DELETE', this._restEndpoint(accountID)).then(function(req) {
var reviewers = this.change.reviewers;
['REVIEWER', 'CC'].forEach(function(type) {
reviewers[type] = reviewers[type] || [];
for (var i = 0; i < reviewers[type].length; i++) {
if (reviewers[type][i]._account_id == accountID) {
this.splice('change.reviewers.' + type, i, 1);
break;
}
}
}, this);
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
}.bind(this));
},
_handleAddTap: function(e) {
e.preventDefault();
this._showInput = true;
this.$.input.focus();
},
_handleCancelTap: function(e) {
e.preventDefault();
this._cancel();
},
_handleMouseEnterItem: function(e) {
this._selectedIndex =
parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10);
},
_handleItemTap: function(e) {
var reviewerEl;
var eventPath = Polymer.dom(e).path;
for (var i = 0; i < eventPath.length; i++) {
var el = eventPath[i];
if (el.classList && el.classList.contains('reviewer')) {
reviewerEl = el;
break;
}
}
this._selectedIndex =
parseInt(reviewerEl.getAttribute('data-index'), 10);
this._sendAddRequest();
},
_autocompleteDataChanged: function(data) {
this._hideAutocomplete = data.length == 0;
},
_hideAutocompleteChanged: function(hidden) {
if (hidden) {
document.body.removeEventListener('click',
this._boundBodyClickHandler);
this._selectedIndex = -1;
} else {
document.body.addEventListener('click', this._boundBodyClickHandler);
this._selectedIndex = 0;
}
},
_inputValChanged: function(val) {
var sendRequest = function() {
if (this.disabled || val == null || val.trim().length == 0) {
return;
}
if (val.length < this.suggestFrom) {
this._clearInputRequestHandle();
this._hideAutocomplete = true;
this._selectedIndex = -1;
return;
}
this._lastAutocompleteRequest =
this.$.autocompleteXHR.generateRequest();
}.bind(this);
this._clearInputRequestHandle();
if (this._inputRequestTimeout == 0) {
sendRequest();
} else {
this._inputRequestHandle =
this.async(sendRequest, this._inputRequestTimeout);
}
},
_handleKey: function(e) {
if (this._hideAutocomplete) {
if (e.keyCode == 27) { // 'esc'
e.preventDefault();
this._cancel();
}
return;
}
switch (e.keyCode) {
case 38: // 'up':
e.preventDefault();
this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
break;
case 40: // 'down'
e.preventDefault();
this._selectedIndex = Math.min(this._selectedIndex + 1,
this._autocompleteData.length - 1);
break;
case 27: // 'esc'
e.preventDefault();
this._hideAutocomplete = true;
break;
case 13: // 'enter'
e.preventDefault();
this._sendAddRequest();
break;
}
},
_cancel: function() {
this._showInput = false;
this._selectedIndex = 0;
this._inputVal = '';
this._autocompleteData = [];
this.$.addReviewer.focus();
},
_sendAddRequest: function() {
this._clearInputRequestHandle();
var reviewerID;
var reviewer = this._autocompleteData[this._selectedIndex];
if (reviewer.account) {
reviewerID = reviewer.account._account_id;
} else if (reviewer.group) {
reviewerID = reviewer.group.id;
}
this._autocompleteData = [];
this._send('POST', this._restEndpoint(), reviewerID).then(function(req) {
this.change.reviewers.CC = this.change.reviewers.CC || [];
req.response.reviewers.forEach(function(r) {
this.push('change.removable_reviewers', r);
this.push('change.reviewers.CC', r);
}, this);
this._inputVal = '';
this.$.input.focus();
}.bind(this)).catch(function(err) {
// TODO(andybons): Use the message returned by the server.
alert('Unable to add ' + reviewerID + ' as a reviewer.');
throw err;
}.bind(this));
},
_send: function(method, url, reviewerID) {
this.disabled = true;
var request = document.createElement('gr-request');
var opts = {
method: method,
url: url,
};
if (reviewerID) {
opts.body = {reviewer: reviewerID};
}
this._xhrPromise = request.send(opts);
var enableEl = function() { this.disabled = false; }.bind(this);
this._xhrPromise.then(enableEl).catch(enableEl);
return this._xhrPromise;
},
_restEndpoint: function(id) {
var path = '/changes/' + this.change._number + '/reviewers';
if (id) {
path += '/' + id;
}
return path;
},
});
})();
</script>
</dom-module>

View File

@@ -1,115 +0,0 @@
<!--
Copyright (C) 2015 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="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="gr-button.html">
<dom-module id="gr-search-bar">
<template>
<style>
:host {
display: inline-block;
}
form {
display: flex;
}
input {
border: 1px solid #d1d2d3;
outline: none;
}
input {
flex: 1;
font: inherit;
border-radius: 2px 0 0 2px;
}
gr-button {
background-color: #f1f2f3;
border-radius: 0 2px 2px 0;
border-left-width: 0;
}
</style>
<form>
<input is="iron-input" id="searchInput" bind-value="{{_inputVal}}">
<gr-button id="searchButton">Search</gr-button>
</form>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-search-bar',
behaviors: [
Gerrit.KeyboardShortcutBehavior,
],
listeners: {
'searchInput.keydown': '_inputKeyDownHandler',
'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
},
properties: {
value: {
type: String,
value: '',
notify: true,
observer: '_valueChanged',
},
keyEventTarget: {
type: Object,
value: function() { return document.body; },
},
_inputVal: String,
},
_valueChanged: function(value) {
this._inputVal = value;
},
_inputKeyDownHandler: function(e) {
if (e.keyCode == 13) { // Enter key
this._preventDefaultAndNavigateToInputVal(e);
}
},
_preventDefaultAndNavigateToInputVal: function(e) {
e.preventDefault();
Polymer.dom(e).rootTarget.blur();
page.show('/q/' + this._inputVal);
},
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
switch (e.keyCode) {
case 191: // '/' or '?' with shift key.
// TODO(andybons): Localization using e.key/keypress event.
if (e.shiftKey) { break; }
e.preventDefault();
var s = this.$.searchInput;
s.focus();
s.setSelectionRange(0, s.value.length);
break;
}
},
});
})();
</script>
</dom-module>

View File

@@ -14,9 +14,9 @@ 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-dropdown/iron-dropdown.html">
<link rel="import" href="gr-button.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
<link rel="import" href="../gr-button/gr-button.html">
<dom-module id="gr-account-dropdown">
<style>
@@ -78,21 +78,5 @@ limitations under the License.
</div>
</iron-dropdown>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-account-dropdown',
properties: {
account: Object,
},
_showDropdownTapHandler: function(e) {
this.$.dropdown.open();
},
});
})();
</script>
<script src="gr-account-dropdown.js"></script>
</dom-module>

View File

@@ -0,0 +1,28 @@
// 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-dropdown',
properties: {
account: Object,
},
_showDropdownTapHandler: function(e) {
this.$.dropdown.open();
},
});
})();

View File

@@ -18,11 +18,11 @@ 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-dropdown</title>
<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<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="../elements/gr-account-dropdown.html">
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-account-dropdown.html">
<test-fixture id="basic">
<template>

View File

@@ -14,8 +14,8 @@ 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="gr-avatar.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-avatar/gr-avatar.html">
<dom-module id="gr-account-label">
<template>
@@ -44,39 +44,5 @@ limitations under the License.
</span>
</span>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-account-label',
properties: {
account: Object,
avatarImageSize: {
type: Number,
value: 32,
},
showEmail: {
type: Boolean,
value: false,
},
},
_computeAccountTitle: function(account) {
if (!account || !account.name) { return; }
var result = util.escapeHTML(account.name);
if (account.email) {
result += ' <' + util.escapeHTML(account.email) + '>';
}
return result;
},
_computeShowEmail: function(showEmail, account) {
return !!(showEmail && account && account.email);
},
});
})();
</script>
<script src="gr-account-label.js"></script>
</dom-module>

View File

@@ -0,0 +1,45 @@
// 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-label',
properties: {
account: Object,
avatarImageSize: {
type: Number,
value: 32,
},
showEmail: {
type: Boolean,
value: false,
},
},
_computeAccountTitle: function(account) {
if (!account || !account.name) { return; }
var result = util.escapeHTML(account.name);
if (account.email) {
result += ' <' + util.escapeHTML(account.email) + '>';
}
return result;
},
_computeShowEmail: function(showEmail, account) {
return !!(showEmail && account && account.email);
},
});
})();

View File

@@ -18,12 +18,12 @@ 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-label</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../elements/gr-account-label.html">
<link rel="import" href="gr-account-label.html">
<test-fixture id="basic">
<template>

View File

@@ -14,8 +14,8 @@ 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="gr-account-label.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-account-label/gr-account-label.html">
<dom-module id="gr-account-link">
<template>
@@ -40,28 +40,5 @@ limitations under the License.
</a>
</span>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-account-link',
properties: {
account: Object,
avatarImageSize: {
type: Number,
value: 32,
},
},
_computeOwnerLink: function(account) {
if (!account) { return; }
var accountID = account.email || account._account_id;
return '/q/owner:' + encodeURIComponent(accountID) + '+status:open';
},
});
})();
</script>
<script src="gr-account-link.js"></script>
</dom-module>

View File

@@ -0,0 +1,34 @@
// 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-link',
properties: {
account: Object,
avatarImageSize: {
type: Number,
value: 32,
},
},
_computeOwnerLink: function(account) {
if (!account) { return; }
var accountID = account.email || account._account_id;
return '/q/owner:' + encodeURIComponent(accountID) + '+status:open';
},
});
})();

View File

@@ -18,12 +18,12 @@ 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-link</title>
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/fake-app.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../elements/gr-account-link.html">
<link rel="import" href="gr-account-link.html">
<test-fixture id="basic">
<template>

View File

@@ -0,0 +1,35 @@
<!--
Copyright (C) 2015 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-ajax/iron-ajax.html">
<dom-module id="gr-ajax">
<template>
<iron-ajax id="xhr"
auto="[[auto]]"
url="[[url]]"
params="[[params]]"
json-prefix=")]}'"
last-error="{{lastError}}"
last-response="{{lastResponse}}"
loading="{{loading}}"
on-response="_handleResponse"
on-error="_handleError"
debounce-duration="300"></iron-ajax>
</template>
<script src="gr-ajax.js"></script>
</dom-module>

Some files were not shown because too many files have changed in this diff Show More