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:
@@ -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">▶</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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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)]]">← Prev</a>
|
||||
<a href$="[[_computeNavLink(_query, _offset, 1)]]"
|
||||
hidden$="[[_hideNextArrow(_changes.length)]]">Next →</a>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
<script src="gr-change-list-view.js"></script>
|
||||
</dom-module>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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 can’t 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 '';
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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];
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
159
polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
Normal file
159
polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
Normal 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">▶</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>
|
||||
205
polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
Normal file
205
polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
Normal 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('Couldn’t 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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 don’t 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>
|
||||
111
polygerrit-ui/app/elements/change/gr-message/gr-message.js
Normal file
111
polygerrit-ui/app/elements/change/gr-message/gr-message.js
Normal 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 don’t 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});
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 couldn’t 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 don’t 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 couldn’t 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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});
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
613
polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
Normal file
613
polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
Normal 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
|
||||
// '<span>'. Advancing from index 0 on the prior html string would
|
||||
// return 4, since < 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., <) 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
174
polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
Normal file
174
polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
Normal 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">▼</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>
|
||||
315
polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
Normal file
315
polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
Normal 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('Couldn’t 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;
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
123
polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
Normal file
123
polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
Normal 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>
|
||||
746
polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
Normal file
746
polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
Normal 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));
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
171
polygerrit-ui/app/elements/gr-app.js
Normal file
171
polygerrit-ui/app/elements/gr-app.js
Normal 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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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 can’t 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>
|
||||
@@ -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">▶</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>
|
||||
@@ -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)]]">← Prev</a>
|
||||
<a href$="[[_computeNavLink(_query, _offset, 1)]]"
|
||||
hidden$="[[_hideNextArrow(_changes.length)]]">Next →</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 couldn’t 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 don’t 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 couldn’t 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>
|
||||
@@ -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
|
||||
// '<span>'. Advancing from index 0 on the prior html string would
|
||||
// return 4, since < 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., <) 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>
|
||||
@@ -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">▼</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('Couldn’t 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>
|
||||
@@ -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>
|
||||
@@ -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">▶</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('Couldn’t 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -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>
|
||||
35
polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
Normal file
35
polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
Normal 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
Reference in New Issue
Block a user