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
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
new file mode 100644
index 0000000..caa9674
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
new file mode 100644
index 0000000..5d03cba
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
new file mode 100644
index 0000000..0a4aec4
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<link rel="import" href="gr-change-list-item.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list-item></gr-change-list-item>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list-item tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeChangeStatusString({mergeable: true}), '');
+ assert.equal(element._computeChangeStatusString({mergeable: false}),
+ 'Merge Conflict');
+ assert.equal(element._computeChangeStatusString({status: 'NEW'}), '');
+ assert.equal(element._computeChangeStatusString({status: 'MERGED'}),
+ 'Merged');
+ assert.equal(element._computeChangeStatusString({status: 'ABANDONED'}),
+ 'Abandoned');
+ assert.equal(element._computeChangeStatusString({status: 'DRAFT'}),
+ 'Draft');
+
+ assert.equal(element._computeLabelClass({labels: {}}), 'cell label');
+ assert.equal(element._computeLabelClass(
+ {labels: {}}, 'Verified'), 'cell label');
+ assert.equal(element._computeLabelClass(
+ {labels: {Verified: {approved: true, value: 1}}}, 'Verified'),
+ 'cell label u-green u-monospace');
+ assert.equal(element._computeLabelClass(
+ {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'),
+ 'cell label u-monospace u-red');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: 1}}}, 'Code-Review'),
+ 'cell label u-green u-monospace');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: -1}}}, 'Code-Review'),
+ 'cell label u-monospace u-red');
+
+ assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'),
+ 'Verified');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'),
+ 'Verified\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'),
+ 'Verified\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}},
+ 'Code-Review'), 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}},
+ 'Code-Review'), 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'},
+ rejected: {name: 'Admin'}}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {approved: {name: 'Diffy'},
+ rejected: {name: 'Admin'}}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'},
+ disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {approved: {name: 'Diffy'},
+ disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
+ 'Code-Review\nby Diffy');
+
+ assert.equal(element._computeLabelValue({labels: {}}), '');
+ assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), '');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {value: 1}}}, 'Verified'), '+1');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {value: -1}}}, 'Verified'), '-1');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {approved: true}}}, 'Verified'), '✓');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
+
+ assert.equal(element._computeProjectURL('combustible-stuff'),
+ '/projects/combustible-stuff,dashboards/default');
+
+ assert.equal(element._computeProjectBranchURL(
+ 'combustible-stuff', 'lemons'),
+ '/q/status:open+project:combustible-stuff+branch:lemons');
+
+ element.change = {_number: 42};
+ assert.equal(element.changeURL, '/c/42/');
+ element.change = {_number: 43};
+ assert.equal(element.changeURL, '/c/43/');
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
new file mode 100644
index 0000000..5b03274
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
new file mode 100644
index 0000000..d0a97c1d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
new file mode 100644
index 0000000..8ff66cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
new file mode 100644
index 0000000..ef71be3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -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');
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
new file mode 100644
index 0000000..0575f85
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -0,0 +1,237 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-change-list></gr-change-list>
+ </template>
+</test-fixture>
+
+<test-fixture id="grouped">
+ <template>
+ <gr-change-list></gr-change-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list basic tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeLabelNames(
+ [[{_number: 0, labels: {}}]]).length, 0);
+ assert.equal(element._computeLabelNames([[
+ {_number: 0, labels: {Verified: {approved: {}}}},
+ {_number: 1, labels: {
+ Verified: {approved: {}}, 'Code-Review': {approved: {}}}},
+ {_number: 2, labels: {
+ Verified: {approved: {}}, 'Library-Compliance': {approved: {}}}},
+ ]]).length, 3);
+
+ assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
+ assert.equal(element._computeLabelShortcut('Verified'), 'V');
+ assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
+ assert.equal(element._computeLabelShortcut(
+ 'Some-Special-Label-7'), 'SSL7');
+ });
+
+ test('keyboard shortcuts', function(done) {
+ element.selectedIndex = 0;
+ element.changes = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ flushAsynchronousOperations();
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 3);
+
+ element.async(function() {
+ assert.isTrue(elementItems[0].selected);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+
+ var showStub = sinon.stub(page, 'show');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/2/'),
+ 'Should navigate to /c/2/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/1/'),
+ 'Should navigate to /c/1/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 0);
+
+ showStub.restore();
+ done();
+ }, 1);
+ });
+
+ test('changes needing review', function() {
+ element.changes = [
+ {
+ _number: 0,
+ reviewed: true,
+ owner: { _account_id: 0 },
+ },
+ {
+ _number: 1,
+ owner: { _account_id: 0 },
+ },
+ {
+ _number: 2,
+ status: 'MERGED',
+ owner: { _account_id: 0 },
+ },
+ {
+ _number: 3,
+ owner: { _account_id: 42 },
+ }
+ ];
+ flushAsynchronousOperations();
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 4);
+ for (var i = 0; i < elementItems.length; i++) {
+ assert.isFalse(elementItems[i].hasAttribute('needs-review'));
+ }
+
+ element.showReviewedState = true;
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 4);
+ assert.isFalse(elementItems[0].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[1].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[2].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[3].hasAttribute('needs-review'));
+
+ element.account = { _account_id: 42 };
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 4);
+ assert.isFalse(elementItems[0].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[1].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[2].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ });
+
+ test('no changes', function() {
+ element.changes = [];
+ flushAsynchronousOperations();
+ var listItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(listItems.length, 0);
+ var noChangesMsg = Polymer.dom(element.root).querySelector('.noChanges');
+ assert.ok(noChangesMsg);
+ });
+
+ test('empty groups', function() {
+ element.groups = [[], []];
+ flushAsynchronousOperations();
+ var listItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(listItems.length, 0);
+ var noChangesMsg = Polymer.dom(element.root).querySelectorAll(
+ '.noChanges');
+ assert.equal(noChangesMsg.length, 2);
+ });
+ });
+
+ suite('gr-change-list groups', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('keyboard shortcuts', function() {
+ element.selectedIndex = 0;
+ element.groups = [
+ [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ],
+ [
+ {_number: 3},
+ {_number: 4},
+ {_number: 5},
+ ],
+ [
+ {_number: 6},
+ {_number: 7},
+ {_number: 8},
+ ]
+ ];
+ element.groupTitles = ['Group 1', 'Group 2', 'Group 3'];
+ flushAsynchronousOperations();
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 9);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+
+ var showStub = sinon.stub(page, 'show');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/2/'),
+ 'Should navigate to /c/2/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/1/'),
+ 'Should navigate to /c/1/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 4);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/4/'),
+ 'Should navigate to /c/4/');
+ showStub.restore();
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
new file mode 100644
index 0000000..e351f44
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -0,0 +1,64 @@
+<!--
+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">
+
+<dom-module id="gr-dashboard-view">
+ <template>
+ <style>
+ :host {
+ background-color: var(--view-background-color);
+ display: block;
+ margin: 0 var(--default-horizontal-margin);
+ }
+ .loading {
+ margin-top: 1em;
+ color: #666;
+ background-color: #f1f2f3;
+ }
+ gr-change-list {
+ margin-top: 1em;
+ width: 100%;
+ }
+ @media only screen and (max-width: 50em) {
+ :host {
+ margin: 0;
+ }
+ .loading {
+ padding: 0 var(--default-horizontal-margin);
+ }
+ }
+ </style>
+ <gr-ajax
+ auto
+ url="/changes/"
+ params="[[_computeQueryParams()]]"
+ last-response="{{_results}}"
+ loading="{{_loading}}"></gr-ajax>
+ <div class="loading" hidden$="[[!_loading]]">Loading...</div>
+ <div hidden$="[[_loading]]" hidden>
+ <gr-change-list
+ show-star
+ show-reviewed-state
+ account="[[account]]"
+ selected-index="{{viewState.selectedChangeIndex}}"
+ groups="{{_results}}"
+ group-titles="[[_groupTitles]]"></gr-change-list>
+ </div>
+ </template>
+ <script src="gr-dashboard-view.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
new file mode 100644
index 0000000..fc6a3ff
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -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',
+ ],
+ };
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
new file mode 100644
index 0000000..1593fab
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
new file mode 100644
index 0000000..a89c0aa
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
new file mode 100644
index 0000000..a89a7a5
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-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>
+
+<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>
+ <gr-change-actions></gr-change-actions>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-actions tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ server = sinon.fakeServer.create();
+
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/actions',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ cherrypick: {
+ method: 'POST',
+ label: 'Cherry Pick',
+ title: 'Cherry pick change to a different branch',
+ enabled: true
+ },
+ rebase: {
+ method: 'POST',
+ label: 'Rebase',
+ title: 'Rebase onto tip of branch or parent change'
+ },
+ submit: {
+ method: 'POST',
+ label: 'Submit',
+ title: 'Submit patch set 1 into master',
+ enabled: true
+ }
+ }),
+ ]
+ );
+
+ server.respondWith(
+ 'POST',
+ '/changes/42/revisions/2/submit',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n{}', // The response is not used by the element.
+ ]
+ );
+
+ server.respondWith(
+ 'POST',
+ '/changes/42/revisions/2/rebase',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n{}', // The response is not used by the element.
+ ]
+ );
+
+ element.changeNum = '42';
+ element.patchNum = '2';
+ element.reload();
+
+ server.respond();
+ });
+
+ test('submit and rebase buttons show', function(done) {
+ element.async(function() {
+ var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
+ assert.equal(buttonEls.length, 2);
+ assert.isFalse(element.hidden);
+ done();
+ }, 1);
+ });
+
+ test('submit change', function(done) {
+ element.async(function() {
+ var submitButton = element.$$('gr-button[data-action-key="submit"]');
+ assert.ok(submitButton);
+ MockInteractions.tap(submitButton);
+ server.respond();
+
+ // Upon success it should fire the reload-change event.
+ element.addEventListener('reload-change', function(e) {
+ done();
+ });
+ }, 1);
+ });
+
+ test('rebase change', function(done) {
+ element.async(function() {
+ var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
+ MockInteractions.tap(rebaseButton);
+
+ element.$.confirmRebase.base = '1234';
+ element._handleRebaseConfirm();
+ server.respond();
+ var lastRequest = server.requests[server.requests.length - 1];
+ assert.equal(lastRequest.requestBody, '{"base":"1234"}');
+
+ element.$.confirmRebase.base = '';
+ element._handleRebaseConfirm();
+ server.respond();
+ lastRequest = server.requests[server.requests.length - 1];
+ assert.equal(lastRequest.requestBody, '{}');
+
+ element.$.confirmRebase.base = 'does not matter';
+ element.$.confirmRebase.clearParent = true;
+ element._handleRebaseConfirm();
+ server.respond();
+ lastRequest = server.requests[server.requests.length - 1];
+ assert.equal(lastRequest.requestBody, '{"base":""}');
+
+ // Upon each request success it should fire the reload-change event.
+ var numEvents = 0;
+ element.addEventListener('reload-change', function(e) {
+ if (++numEvents == 3) { done(); }
+ });
+ }, 1);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
similarity index 62%
rename from polygerrit-ui/app/elements/gr-change-metadata.html
rename to polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 81b580f..7b1a2f1 100644
--- a/polygerrit-ui/app/elements/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,13 +14,13 @@
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 @@
</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>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
new file mode 100644
index 0000000..3d2633b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
new file mode 100644
index 0000000..6c97b5a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-change-metadata></gr-change-metadata>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-metadata tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.isFalse(element._computeHideStrategy({status: 'NEW'}));
+ assert.isFalse(element._computeHideStrategy({status: 'DRAFT'}));
+ assert.isTrue(element._computeHideStrategy({status: 'MERGED'}));
+ assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'}));
+ assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}),
+ 'Cherry Pick');
+ });
+
+ test('show strategy for open change', function() {
+ element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
+ flushAsynchronousOperations();
+ var strategy = element.$$('.strategy');
+ assert.ok(strategy);
+ assert.isFalse(strategy.hasAttribute('hidden'));
+ assert.equal(strategy.children[1].innerHTML, 'Cherry Pick');
+ });
+
+ test('hide strategy for closed change', function() {
+ element.change = {status: 'MERGED', labels: {}};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$$('.strategy').hasAttribute('hidden'));
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
new file mode 100644
index 0000000..41cb058
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
new file mode 100644
index 0000000..a42a379
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -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);
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
new file mode 100644
index 0000000..ed9d28d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-change-view></gr-change-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-view tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.$.configXHR.auto = false;
+
+ server = sinon.fakeServer.create();
+ // Eat any requests made by elements in this suite.
+ server.respondWith(
+ 'GET',
+ /\/changes\/(.*)/,
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n{}',
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('keyboard shortcuts', function() {
+ var showStub = sinon.stub(page, 'show');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u'
+ assert(showStub.lastCall.calledWithExactly('/'),
+ 'Should navigate to /');
+ showStub.restore();
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ var overlayEl = element.$.replyOverlay;
+ assert.isTrue(overlayEl.opened);
+ overlayEl.close();
+ assert.isFalse(overlayEl.opened);
+ });
+
+ test('patch num change', function(done) {
+ element._changeNum = '42';
+ element._patchNum = 2;
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ status: 'NEW',
+ labels: {},
+ };
+ flushAsynchronousOperations();
+ var selectEl = element.$$('.header select');
+ assert.ok(selectEl);
+ var optionEls =
+ Polymer.dom(element.root).querySelectorAll('.header option');
+ assert.equal(optionEls.length, 4);
+ assert.isFalse(
+ element.$$('.header option[value="1"]').hasAttribute('selected'));
+ assert.isTrue(
+ element.$$('.header option[value="2"]').hasAttribute('selected'));
+ assert.isFalse(
+ element.$$('.header option[value="3"]').hasAttribute('selected'));
+ assert.equal(optionEls[3].value, 13);
+
+ var showStub = sinon.stub(page, 'show');
+
+ var numEvents = 0;
+ selectEl.addEventListener('change', function(e) {
+ numEvents++;
+ if (numEvents == 1) {
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+ selectEl.value = '3';
+ element.fire('change', {}, {node: selectEl});
+ } else if (numEvents == 2) {
+ assert(showStub.lastCall.calledWithExactly('/c/42'),
+ 'Should navigate to /c/42');
+ showStub.restore();
+ done();
+ }
+ });
+ selectEl.value = '1';
+ element.fire('change', {}, {node: selectEl});
+ });
+
+ test('change status new', function() {
+ element._changeNum = '1';
+ element._patchNum = 1;
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ };
+ var status = element._computeChangeStatus(element._change, '1');
+ assert.equal(status, '');
+ });
+
+ test('change status draft', function() {
+ element._changeNum = '1';
+ element._patchNum = 1;
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'DRAFT',
+ labels: {},
+ };
+ var status = element._computeChangeStatus(element._change, '1');
+ assert.equal(status, '(draft)');
+ });
+
+ test('revision status draft', function() {
+ element._changeNum = '1';
+ element._patchNum = 2;
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {
+ _number: 2,
+ draft: true,
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ };
+ var status = element._computeChangeStatus(element._change, '2');
+ assert.equal(status, '(draft)');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
new file mode 100644
index 0000000..263fb28
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -0,0 +1,68 @@
+<!--
+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-comment-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .file {
+ border-top: 1px solid #ddd;
+ font-weight: bold;
+ margin: 10px 0 3px;
+ padding: 10px 0 5px;
+ }
+ .container {
+ display: flex;
+ margin: 5px 0;
+ }
+ .lineNum {
+ margin-right: .35em;
+ min-width: 7em;
+ }
+ .message {
+ flex: 1;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+ </style>
+ <template is="dom-repeat" items="{{_files}}" as="file">
+ <div class="file">
+ <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
+ </div>
+ <template is="dom-repeat"
+ items="[[_computeCommentsForFile(file)]]" as="comment">
+ <div class="container">
+ <a class="lineNum"
+ href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
+ <span hidden$="[[!comment.line]]">
+ <span>[[_computePatchDisplayName(comment)]]</span>
+ Line <span>[[comment.line]]</span>:
+ </span>
+ <span hidden$="[[comment.line]]">
+ File comment:
+ </span>
+ </a>
+ <div class="message">[[comment.message]]</div>
+ </div>
+ </template>
+ </template>
+ </template>
+ <script src="gr-comment-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
new file mode 100644
index 0000000..b40c18e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -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 '';
+ }
+ });
+})();
diff --git a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
similarity index 66%
rename from polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 96c7188..3896ffa 100644
--- a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -14,8 +14,8 @@
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 @@
</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>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
new file mode 100644
index 0000000..42f2167
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
new file mode 100644
index 0000000..c02e11e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-confirm-rebase-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-confirm-rebase-dialog.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-confirm-rebase-dialog tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('controls', function() {
+ assert.isFalse(element.$.parentInput.hasAttribute('disabled'));
+ assert.isFalse(element.$.clearParent.checked);
+ element.base = 'something great';
+ MockInteractions.tap(element.$.clearParent);
+ assert.isTrue(element.$.parentInput.hasAttribute('disabled'));
+ assert.isTrue(element.$.clearParent.checked);
+ assert.equal(element.base, '');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
new file mode 100644
index 0000000..77a262d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -0,0 +1,144 @@
+<!--
+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-button/gr-button.html">
+
+<dom-module id="gr-download-dialog">
+ <template>
+ <style>
+ :host {
+ display: block;
+ padding: 1em;
+ }
+ ul {
+ list-style: none;
+ margin-bottom: .5em;
+ }
+ li {
+ display: inline-block;
+ margin: 0;
+ padding: 0;
+ }
+ li gr-button {
+ margin-right: 1em;
+ }
+ label,
+ input {
+ display: block;
+ }
+ label {
+ font-weight: bold;
+ }
+ input {
+ font-family: var(--monospace-font-family);
+ font-size: inherit;
+ margin-bottom: .5em;
+ width: 60em;
+ }
+ li[selected] gr-button {
+ color: #000;
+ font-weight: bold;
+ text-decoration: none;
+ }
+ header {
+ display: flex;
+ justify-content: space-between;
+ }
+ main {
+ border-bottom: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+ padding: .5em;
+ }
+ footer {
+ display: flex;
+ justify-content: space-between;
+ padding-top: .75em;
+ }
+ .closeButtonContainer {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ .patchFiles {
+ margin-right: 2em;
+ }
+ .patchFiles a,
+ .archives a {
+ display: inline-block;
+ margin-right: 1em;
+ }
+ .patchFiles a:last-of-type,
+ .archives a:last-of-type {
+ margin-right: 0;
+ }
+ </style>
+ <header>
+ <ul hidden$="[[!_schemes.length]]" hidden>
+ <template is="dom-repeat" items="[[_schemes]]" as="scheme">
+ <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]">
+ <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
+ [[scheme]]
+ </gr-button>
+ </li>
+ </template>
+ </ul>
+ <span class="closeButtonContainer">
+ <gr-button link on-tap="_handleCloseTap">Close</gr-button>
+ </span>
+ </header>
+ <main hidden$="[[!_schemes.length]]" hidden>
+ <template is="dom-repeat"
+ items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
+ as="command">
+ <div class="command">
+ <label>[[command.title]]</label>
+ <input is="iron-input"
+ type="text"
+ bind-value="[[command.command]]"
+ on-tap="_handleInputTap"
+ readonly>
+ </div>
+ </template>
+ </main>
+ <footer>
+ <div class="patchFiles">
+ <label>Patch file</label>
+ <div>
+ <a href$="[[_computeDownloadLink(change, patchNum)]]">
+ [[_computeDownloadFilename(change, patchNum)]]
+ </a>
+ <a href$="[[_computeZipDownloadLink(change, patchNum)]]">
+ [[_computeZipDownloadFilename(change, patchNum)]]
+ </a>
+ </div>
+ </div>
+ <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
+ <label>Archive</label>
+ <div class="archives">
+ <template is="dom-repeat" items="[[config.archives]]" as="format">
+ <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
+ [[format]]
+ </a>
+ </template>
+ </div>
+ </div>
+ </footer>
+ </template>
+ <script src="gr-download-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
new file mode 100644
index 0000000..6677d62
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -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];
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
new file mode 100644
index 0000000..2480c4a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-download-dialog</title>
+
+<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="gr-download-dialog.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-download-dialog></gr-download-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-download-dialog tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ element.change = {
+ current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
+ revisions: {
+ '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
+ _number: 1,
+ fetch: {
+ repo: {
+ commands: {
+ repo: 'repo download test-project 5/1'
+ }
+ },
+ ssh: {
+ commands: {
+ 'Checkout': 'git fetch ssh://andybons@localhost:29418/test-project refs/changes/05/5/1 && git checkout FETCH_HEAD',
+ 'Cherry Pick': 'git fetch ssh://andybons@localhost:29418/test-project refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
+ 'Format Patch': 'git fetch ssh://andybons@localhost:29418/test-project refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD',
+ 'Pull': 'git pull ssh://andybons@localhost:29418/test-project refs/changes/05/5/1'
+ }
+ },
+ http: {
+ commands: {
+ 'Checkout': 'git fetch http://andybons@localhost:8080/a/test-project refs/changes/05/5/1 && git checkout FETCH_HEAD',
+ 'Cherry Pick': 'git fetch http://andybons@localhost:8080/a/test-project refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
+ 'Format Patch': 'git fetch http://andybons@localhost:8080/a/test-project refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD',
+ 'Pull': 'git pull http://andybons@localhost:8080/a/test-project refs/changes/05/5/1'
+ }
+ }
+ }
+ }
+ }
+ };
+ element.patchNum = 1;
+ element.config = {
+ schemes: {
+ 'anonymous http': {},
+ http: {},
+ repo: {},
+ ssh: {},
+ },
+ archives: ['tgz', 'tar', 'tbz2', 'txz'],
+ };
+ });
+
+ test('element visibility', function() {
+ assert.isFalse(element.$$('ul').hasAttribute('hidden'));
+ assert.isFalse(element.$$('main').hasAttribute('hidden'));
+ assert.isFalse(element.$$('.archivesContainer').hasAttribute('hidden'));
+
+ element.set('config.archives', []);
+ assert.isTrue(element.$$('.archivesContainer').hasAttribute('hidden'));
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeArchiveDownloadLink(
+ {_number: 123}, 2, 'tgz'),
+ '/changes/123/revisions/2/archive?format=tgz');
+ });
+
+ test('close event', function(done) {
+ element.addEventListener('close', function() {
+ done();
+ });
+ MockInteractions.tap(element.$$('.closeButtonContainer gr-button'));
+ });
+
+ test('tab selection', function() {
+ flushAsynchronousOperations();
+ var el = element.$$('[data-scheme="http"]').parentElement;
+ assert.isTrue(el.hasAttribute('selected'));
+ ['repo', 'ssh'].forEach(function(scheme) {
+ var el = element.$$('[data-scheme="' + scheme + '"]').parentElement;
+ assert.isFalse(el.hasAttribute('selected'));
+ });
+
+ MockInteractions.tap(element.$$('[data-scheme="ssh"]'));
+ el = element.$$('[data-scheme="ssh"]').parentElement;
+ assert.isTrue(el.hasAttribute('selected'));
+ ['http', 'repo'].forEach(function(scheme) {
+ var el = element.$$('[data-scheme="' + scheme + '"]').parentElement;
+ assert.isFalse(el.hasAttribute('selected'));
+ });
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
new file mode 100644
index 0000000..e010468
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
new file mode 100644
index 0000000..9fe5ca1
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
new file mode 100644
index 0000000..06a01c6
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-file-list></gr-file-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-file-list tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/files',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': {
+ status: 'A',
+ lines_inserted: 9,
+ size_delta: 317,
+ size: 317
+ },
+ 'myfile.txt': {
+ lines_inserted: 35,
+ size_delta: 1146,
+ size: 1167
+ }
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/files',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': {
+ status: 'A',
+ lines_inserted: 9,
+ size_delta: 317,
+ size: 317
+ },
+ 'myfile.txt': {
+ lines_inserted: 35,
+ size_delta: 1146,
+ size: 1167
+ },
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 98,
+ size_delta: 234,
+ size: 136
+ }
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/drafts',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '{}',
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/drafts',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '{}',
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/files?reviewed',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '["/COMMIT_MSG"]',
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/files?reviewed',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '["/COMMIT_MSG","myfile.txt"]',
+ ]
+ );
+ server.respondWith(
+ 'PUT',
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
+ [
+ 201,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '""',
+ ]
+ );
+ server.respondWith(
+ 'DELETE',
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
+ [
+ 204,
+ {'Content-Type': 'application/json'},
+ '',
+ ]
+ );
+
+ app.loggedIn = true;
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('requests', function(done) {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.reload();
+ server.respond();
+
+ element.async(function() {
+ var filenames = element.files.map(function(f) {
+ return f.__path;
+ });
+ assert.deepEqual(filenames, ['/COMMIT_MSG', 'myfile.txt']);
+ assert.deepEqual(element._reviewed, ['/COMMIT_MSG']);
+
+ element.patchNum = '2';
+ element.reload();
+ server.respond();
+ element.async(function() {
+ filenames = element.files.map(function(f) {
+ return f.__path;
+ });
+ assert.deepEqual(filenames,
+ ['/COMMIT_MSG', 'file_added_in_rev2.txt', 'myfile.txt']);
+ assert.deepEqual(element._reviewed, ['/COMMIT_MSG', 'myfile.txt']);
+ done();
+ }, 1);
+ }, 1);
+ });
+
+ test('keyboard shortcuts', function(done) {
+ element.changeNum = '42';
+ element.patchNum = '2';
+ element.selectedIndex = 0;
+ element.reload();
+ server.respond();
+
+ element.async(function() {
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ '.row:not(.header)');
+ assert.equal(elementItems.length, 3);
+ assert.isTrue(elementItems[0].hasAttribute('selected'));
+ assert.isFalse(elementItems[1].hasAttribute('selected'));
+ assert.isFalse(elementItems[2].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+
+ var showStub = sinon.stub(page, 'show');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
+ 'Should navigate to /c/42/2/myfile.txt');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 79); // 'o'
+ assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
+ 'Should navigate to /c/42/2/file_added_in_rev2.txt');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 0);
+
+ showStub.restore();
+ done();
+ }, 1);
+ });
+
+ test('comment filtering', function() {
+ var comments = {
+ '/COMMIT_MSG': [
+ {patch_set: 1, message: 'Done'},
+ {patch_set: 1, message: 'oh hay'},
+ {patch_set: 2, message: 'hello'},
+ ],
+ 'myfile.txt': [
+ {patch_set: 1, message: 'good news!'},
+ {patch_set: 2, message: 'wat!?'},
+ {patch_set: 2, message: 'hi'},
+ ],
+ };
+ assert.equal(
+ element._computeCommentsString(comments, '1', '/COMMIT_MSG'),
+ '2 comments');
+ assert.equal(
+ element._computeCommentsString(comments, '1', 'myfile.txt'),
+ '1 comment');
+ assert.equal(
+ element._computeCommentsString(comments, '1',
+ 'file_added_in_rev2.txt'),
+ '');
+ assert.equal(
+ element._computeCommentsString(comments, '2', '/COMMIT_MSG'),
+ '1 comment');
+ assert.equal(
+ element._computeCommentsString(comments, '2', 'myfile.txt'),
+ '2 comments');
+ assert.equal(
+ element._computeCommentsString(comments, '2',
+ 'file_added_in_rev2.txt'),
+ '');
+ });
+
+ test('computed properties', function() {
+ assert.equal(element._computeFileStatus('A'), 'A');
+ assert.equal(element._computeFileStatus(undefined), 'M');
+ assert.equal(element._computeFileStatus(null), 'M');
+
+ assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
+ '/foo/bar/baz');
+ assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
+ 'Commit message');
+
+ assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
+ assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
+ 'clazz invisible');
+ });
+
+ test('file review status', function(done) {
+ element.changeNum = '42';
+ element.patchNum = '2';
+ element.reload();
+ server.respond();
+
+ element.async(function() {
+ var fileRows =
+ Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+ var commitMsg = fileRows[0].querySelector('input[type="checkbox"]');
+ var fileAdded = fileRows[1].querySelector('input[type="checkbox"]');
+ var myFile = fileRows[2].querySelector('input[type="checkbox"]');
+
+ assert.isTrue(commitMsg.checked);
+ assert.isFalse(fileAdded.checked);
+ assert.isTrue(myFile.checked);
+
+ assert.equal(element._reviewed.length, 2);
+
+ MockInteractions.tap(commitMsg);
+ server.respond();
+ element._xhrPromise.then(function(req) {
+ assert.equal(element._reviewed.length, 1);
+ assert.equal(req.status, 204);
+ assert.equal(req.url,
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
+
+ MockInteractions.tap(commitMsg);
+ server.respond();
+ }).then(function() {
+ element._xhrPromise.then(function(req) {
+ assert.equal(element._reviewed.length, 2);
+ assert.equal(req.status, 201);
+ assert.equal(req.url,
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
+
+ done();
+ });
+ });
+ }, 1);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
new file mode 100644
index 0000000..5733acd
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -0,0 +1,125 @@
+<!--
+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-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>
+ <style>
+ :host {
+ border-top: 1px solid #ddd;
+ display: block;
+ position: relative;
+ }
+ :host(:not([expanded])) {
+ cursor: pointer;
+ }
+ gr-avatar {
+ position: absolute;
+ left: var(--default-horizontal-margin);
+ }
+ .collapsed .contentContainer {
+ color: #777;
+ white-space: nowrap;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ }
+ .showAvatar.expanded .contentContainer {
+ margin-left: calc(var(--default-horizontal-margin) + 2.5em);
+ padding: 10px 0;
+ }
+ .showAvatar.collapsed .contentContainer {
+ margin-left: calc(var(--default-horizontal-margin) + 1.75em);
+ padding: 10px 75px 10px 0;
+ }
+ .hideAvatar.collapsed .contentContainer,
+ .hideAvatar.expanded .contentContainer {
+ margin-left: 0;
+ padding: 10px 75px 10px 0;
+ }
+ .collapsed gr-avatar {
+ top: 8px;
+ height: 1.75em;
+ width: 1.75em;
+ }
+ .expanded gr-avatar {
+ top: 12px;
+ height: 2.5em;
+ width: 2.5em;
+ }
+ .name {
+ font-weight: bold;
+ }
+ .content {
+ font-family: var(--monospace-font-family);
+ }
+ .collapsed .name,
+ .collapsed .content,
+ .collapsed .message {
+ display: inline;
+ }
+ .collapsed gr-comment-list,
+ .collapsed .replyContainer {
+ display: none;
+ }
+ .collapsed .name {
+ color: var(--default-text-color);
+ }
+ .expanded .name {
+ cursor: pointer;
+ }
+ .date {
+ color: #666;
+ position: absolute;
+ right: var(--default-horizontal-margin);
+ top: 10px;
+ }
+ .replyContainer {
+ padding: .5em 0 1em;
+ }
+ </style>
+ <div class$="[[_computeClass(expanded, showAvatar)]]">
+ <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar>
+ <div class="contentContainer">
+ <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div>
+ <div class="content">
+ <gr-linked-text class="message"
+ pre="[[expanded]]"
+ content="[[message.message]]"
+ disabled="[[!expanded]]"
+ config="[[projectConfig.commentlinks]]"></gr-linked-text>
+ <gr-comment-list
+ comments="[[comments]]"
+ change-num="[[changeNum]]"
+ patch-num="[[message._revision_number]]"></gr-comment-list>
+ </div>
+ <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
+ <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
+ </a>
+ </div>
+ <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
+ <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
+ </div>
+ </div>
+ </template>
+ <script src="gr-message.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
new file mode 100644
index 0000000..1ab5e6c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -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});
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
new file mode 100644
index 0000000..0f09b70
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-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>
+
+<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>
+ <gr-message></gr-message>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-message tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('reply event', function(done) {
+ element.message = {
+ 'id': '47c43261_55aa2c41',
+ 'author': {
+ '_account_id': 1115495,
+ 'name': 'Andrew Bonventre',
+ 'email': 'andybons@chromium.org',
+ },
+ 'date': '2016-01-12 20:24:49.448000000',
+ 'message': 'Uploaded patch set 1.',
+ '_revision_number': 1
+ };
+
+ element.addEventListener('reply', function(e) {
+ assert.deepEqual(e.detail.message, element.message);
+ done();
+ });
+ MockInteractions.tap(element.$$('.replyContainer gr-button'));
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
new file mode 100644
index 0000000..8a66d03
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
new file mode 100644
index 0000000..1b9ce14
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
new file mode 100644
index 0000000..5a562ba
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-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>
+
+<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>
+ <gr-messages-list></gr-messages-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-messages-list tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ element.messages = [
+ {
+ 'id': '47c43261_55aa2c41',
+ 'author': {
+ '_account_id': 1115495,
+ 'name': 'Andrew Bonventre',
+ 'email': 'andybons@chromium.org',
+ },
+ 'date': '2016-01-12 20:24:49.448000000',
+ 'message': 'Uploaded patch set 1.',
+ '_revision_number': 1
+ },
+ {
+ 'id': '47c43261_9593e420',
+ 'author': {
+ '_account_id': 1115495,
+ 'name': 'Andrew Bonventre',
+ 'email': 'andybons@chromium.org',
+ },
+ 'date': '2016-01-12 20:28:33.038000000',
+ 'message': 'Patch Set 1:\n\n(1 comment)',
+ '_revision_number': 1
+ },
+ {
+ 'id': '87b2aaf4_f73260c5',
+ 'author': {
+ '_account_id': 1143760,
+ 'name': 'Mark Mentovai',
+ 'email': 'mark@chromium.org',
+ },
+ 'date': '2016-01-12 21:17:07.554000000',
+ 'message': 'Patch Set 1:\n\n(3 comments)',
+ '_revision_number': 1
+ }
+ ];
+ flushAsynchronousOperations();
+ });
+
+ test('expand/collapse all', function() {
+ var allMessageEls =
+ Polymer.dom(element.root).querySelectorAll('gr-message');
+ for (var i = 0; i < allMessageEls.length; i++) {
+ allMessageEls[i].expanded = false;
+ }
+ MockInteractions.tap(allMessageEls[1]);
+ assert.isTrue(allMessageEls[1].expanded);
+
+ MockInteractions.tap(element.$$('.header gr-button'));
+ allMessageEls =
+ Polymer.dom(element.root).querySelectorAll('gr-message');
+ for (var i = 0; i < allMessageEls.length; i++) {
+ assert.isTrue(allMessageEls[i].expanded);
+ }
+
+ MockInteractions.tap(element.$$('.header gr-button'));
+ allMessageEls =
+ Polymer.dom(element.root).querySelectorAll('gr-message');
+ for (var i = 0; i < allMessageEls.length; i++) {
+ assert.isFalse(allMessageEls[i].expanded);
+ }
+ });
+
+ test('scroll to message', function() {
+ var allMessageEls =
+ Polymer.dom(element.root).querySelectorAll('gr-message');
+ for (var i = 0; i < allMessageEls.length; i++) {
+ allMessageEls[i].expanded = false;
+ }
+
+ var scrollToStub = sinon.stub(window, 'scrollTo');
+ var highlightStub = sinon.stub(element, '_highlightEl');
+
+ element.scrollToMessage('invalid');
+
+ for (var i = 0; i < allMessageEls.length; i++) {
+ assert.isFalse(allMessageEls[i].expanded,
+ 'expected gr-message ' + i + ' to not be expanded');
+ }
+
+ var messageID = '47c43261_9593e420';
+ element.scrollToMessage(messageID);
+ assert.isTrue(
+ element.$$('[data-message-id="' + messageID + '"]').expanded);
+
+ assert.isTrue(scrollToStub.calledOnce);
+ assert.isTrue(highlightStub.calledOnce);
+
+ scrollToStub.restore();
+ highlightStub.restore();
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
new file mode 100644
index 0000000..e93d008
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
new file mode 100644
index 0000000..f3a298e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
new file mode 100644
index 0000000..7e0c236
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-related-changes-list</title>
+
+<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="gr-related-changes-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-related-changes-list></gr-related-changes-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-related-changes-list tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('connected revisions', function() {
+ var change = {
+ revisions: {
+ 'e3c6d60783bfdec9ebae7dcfec4662360433449e': {
+ _number: 1,
+ },
+ '26e5e4c9c7ae31cbd876271cca281ce22b413997': {
+ _number: 2,
+ },
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': {
+ _number: 7,
+ },
+ 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': {
+ _number: 5,
+ },
+ 'd6bcee67570859ccb684873a85cf50b1f0e96fda': {
+ _number: 6,
+ },
+ 'cc960918a7f90388f4a9e05753d0f7b90ad44546': {
+ _number: 3,
+ },
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': {
+ _number: 4,
+ }
+ }
+ };
+ var patchNum = 7;
+ var relatedChanges = [
+ {
+ commit: {
+ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ parents: [
+ {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ parents: [
+ {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ parents: [
+ {
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ parents: [
+ {
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ parents: [
+ {
+ commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75'
+ }
+ ],
+ },
+ }
+ ];
+
+ var connectedChanges =
+ element._computeConnectedRevisions(change, patchNum, relatedChanges);
+ assert.deepEqual(connectedChanges, [
+ '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ ]);
+
+ patchNum = 4;
+ relatedChanges = [
+ {
+ commit: {
+ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ parents: [
+ {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ parents: [
+ {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ parents: [
+ {
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ parents: [
+ {
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e'
+ }
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
+ parents: [
+ {
+ commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c'
+ }
+ ],
+ },
+ }
+ ];
+
+ connectedChanges =
+ element._computeConnectedRevisions(change, patchNum, relatedChanges);
+ assert.deepEqual(connectedChanges, [
+ 'af815dac54318826b7f1fa468acc76349ffc588e',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ ]);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
new file mode 100644
index 0000000..ab21e6c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
new file mode 100644
index 0000000..3cd6e12
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
new file mode 100644
index 0000000..a6f4671
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-reply-dialog></gr-reply-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-reply-dialog tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.changeNum = 42;
+ element.patchNum = 1;
+ element.labels = {
+ Verified: {
+ values: {
+ '-1': 'Fails',
+ ' 0': 'No score',
+ '+1': 'Verified'
+ },
+ default_value: 0
+ },
+ 'Code-Review': {
+ values: {
+ '-2': 'Do not submit',
+ '-1': 'I would prefer that you didn\'t submit this',
+ ' 0': 'No score',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved'
+ },
+ default_value: 0
+ }
+ };
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-1',
+ ' 0',
+ '+1'
+ ],
+ Verified: [
+ '-1',
+ ' 0',
+ '+1'
+ ]
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'POST',
+ '/changes/42/revisions/1/review',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '{' +
+ '"labels": {' +
+ '"Code-Review": -1,' +
+ '"Verified": -1' +
+ '}' +
+ '}'
+ ]
+ );
+
+ // Allow the elements created by dom-repeat to be stamped.
+ flushAsynchronousOperations();
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('cancel event', function(done) {
+ element.addEventListener('cancel', function() { done(); });
+ MockInteractions.tap(element.$$('.cancel'));
+ });
+
+ test('label picker', function(done) {
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ element.async(function() {
+ for (var label in element.permittedLabels) {
+ assert.ok(element.$$('iron-selector[data-label="' + label + '"]'),
+ label);
+ }
+ element.draft = 'I wholeheartedly disapprove';
+ MockInteractions.tap(element.$$(
+ 'iron-selector[data-label="Code-Review"] > ' +
+ 'gr-button[data-value="-1"]'));
+ MockInteractions.tap(element.$$(
+ 'iron-selector[data-label="Verified"] > ' +
+ 'gr-button[data-value="-1"]'));
+
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ element.async(function() {
+ MockInteractions.tap(element.$$('.send'));
+ assert.isTrue(element.disabled);
+
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done sending reply.');
+ assert.equal(req.status, 200);
+ assert.equal(req.url, '/changes/42/revisions/1/review');
+ var reqObj = JSON.parse(req.xhr.requestBody);
+ assert.deepEqual(reqObj, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': -1,
+ 'Verified': -1
+ },
+ message: 'I wholeheartedly disapprove'
+ });
+ assert.equal(req.response.labels['Code-Review'], -1);
+ assert.equal(req.response.labels.Verified, -1);
+ done();
+ });
+ }, 1);
+ }, 1);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
new file mode 100644
index 0000000..d20fd01
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
new file mode 100644
index 0000000..00fc12e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
new file mode 100644
index 0000000..898d328
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -0,0 +1,278 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-reviewer-list></gr-reviewer-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-reviewer-list tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ /\/changes\/42\/suggest_reviewers\?n=10&q=andy(.*)/,
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify([
+ {
+ account: {
+ _account_id: 1021482,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ }
+ },
+ {
+ account: {
+ _account_id: 1021863,
+ name: 'Andrew Bonventre',
+ email: 'andybons@google.com',
+ }
+ },
+ {
+ group: {
+ id: 'c7af6dd375c092ff3b23c0937aa910693dc0c41b',
+ name: 'andy',
+ }
+ }
+ ]),
+ ]
+ );
+ server.respondWith(
+ 'POST',
+ '/changes/42/reviewers',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ reviewers: [{
+ _account_id: 1021482,
+ approvals: {
+ 'Code-Review': ' 0'
+ },
+ email: 'andybons@chromium.org',
+ name: 'Andrew Bonventre',
+ }]
+ }),
+ ]
+ );
+ server.respondWith(
+ 'DELETE',
+ '/changes/42/reviewers/1021482',
+ [
+ 204,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n{}',
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('controls hidden on immutable element', function() {
+ element.mutable = false;
+ assert.isTrue(element.$$('.controlsContainer').hasAttribute('hidden'));
+ element.mutable = true;
+ assert.isFalse(element.$$('.controlsContainer').hasAttribute('hidden'));
+ });
+
+ function getActiveElement() {
+ return document.activeElement.shadowRoot ?
+ document.activeElement.shadowRoot.activeElement :
+ document.activeElement;
+ }
+
+ test('show/hide input', function() {
+ element.mutable = true;
+ assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
+ assert.isTrue(
+ element.$$('.autocompleteContainer').hasAttribute('hidden'));
+ assert.notEqual(getActiveElement().id, 'input');
+ MockInteractions.tap(element.$$('.addReviewer'));
+ assert.isTrue(element.$$('.addReviewer').hasAttribute('hidden'));
+ assert.isFalse(
+ element.$$('.autocompleteContainer').hasAttribute('hidden'));
+ assert.equal(getActiveElement().id, 'input');
+ MockInteractions.pressAndReleaseKeyOn(element, 27); // 'esc'
+ assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden'));
+ assert.isTrue(
+ element.$$('.autocompleteContainer').hasAttribute('hidden'));
+ assert.equal(getActiveElement().id, 'addReviewer');
+ });
+
+ test('only show remove for removable reviewers', function() {
+ element.mutable = true;
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ 'REVIEWER': [
+ {
+ _account_id: 2,
+ name: 'Bojack Horseman',
+ email: 'SecretariatRulez96@hotmail.com',
+ },
+ {
+ _account_id: 3,
+ name: 'Pinky Penguin',
+ },
+ ],
+ 'CC': [
+ {
+ _account_id: 4,
+ name: 'Diane Nguyen',
+ email: 'macarthurfellow2B@juno.com',
+ },
+ ]
+ },
+ removable_reviewers: [
+ {
+ _account_id: 3,
+ name: 'Pinky Penguin',
+ },
+ {
+ _account_id: 4,
+ name: 'Diane Nguyen',
+ email: 'macarthurfellow2B@juno.com',
+ },
+ ]
+ };
+ flushAsynchronousOperations();
+ var removeEls =
+ Polymer.dom(element.root).querySelectorAll('.reviewer > .remove');
+ assert.equal(removeEls.length, 3);
+ Array.from(removeEls).forEach(function(el) {
+ var accountID = parseInt(el.getAttribute('data-account-id'), 10);
+ assert.ok(accountID);
+ if (accountID == 2) {
+ assert.isTrue(el.hasAttribute('hidden'));
+ } else {
+ assert.isFalse(el.hasAttribute('hidden'));
+ }
+ });
+ });
+
+ test('autocomplete starts at >= 3 chars', function() {
+ element._inputRequestTimeout = 0;
+ element._mutable = true;
+ var genRequestStub = sinon.stub(
+ element.$.autocompleteXHR,
+ 'generateRequest',
+ function() {
+ assert(false, 'generateRequest should not be called for input ' +
+ 'lengths of less than 3 chars');
+ }
+ );
+ element._inputVal = 'fo';
+ flushAsynchronousOperations();
+ genRequestStub.restore();
+ });
+
+ test('add/remove reviewer flow', function(done) {
+ element.change = {
+ _number: 42,
+ reviewers: {},
+ removable_reviewers: [],
+ owner: {_account_id: 0},
+ };
+ element._inputRequestTimeout = 0;
+ element._mutable = true;
+ MockInteractions.tap(element.$$('.addReviewer'));
+ flushAsynchronousOperations();
+ element._inputVal = 'andy';
+ server.respond();
+
+ element._lastAutocompleteRequest.completes.then(function() {
+ flushAsynchronousOperations();
+ assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
+ var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
+ assert.equal(itemEls.length, 3);
+ assert.isTrue(itemEls[0].hasAttribute('selected'));
+ assert.isFalse(itemEls[1].hasAttribute('selected'));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 40); // 'down'
+ assert.isFalse(itemEls[0].hasAttribute('selected'));
+ assert.isTrue(itemEls[1].hasAttribute('selected'));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 38); // 'up'
+ assert.isTrue(itemEls[0].hasAttribute('selected'));
+ assert.isFalse(itemEls[1].hasAttribute('selected'));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 27); // 'esc'
+ assert.isTrue(element.$$('.dropdown').hasAttribute('hidden'));
+
+ element._inputVal = 'andyb';
+ server.respond();
+
+ element._lastAutocompleteRequest.completes.then(function() {
+ assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
+ var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
+ assert.equal(itemEls.length, 3);
+ assert.isTrue(itemEls[0].hasAttribute('selected'));
+ assert.isFalse(itemEls[1].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert.isTrue(element.disabled);
+ server.respond();
+
+ element._xhrPromise.then(function() {
+ assert.isFalse(element.disabled);
+ flushAsynchronousOperations();
+ var reviewerEls =
+ Polymer.dom(element.root).querySelectorAll('.reviewer');
+ assert.equal(reviewerEls.length, 1);
+ MockInteractions.tap(element.$$('.reviewer > .remove'));
+ flushAsynchronousOperations();
+ assert.isTrue(element.disabled);
+ server.respond();
+
+ element._xhrPromise.then(function() {
+ flushAsynchronousOperations();
+ assert.isFalse(element.disabled);
+ var reviewerEls =
+ Polymer.dom(element.root).querySelectorAll('.reviewer');
+ assert.equal(reviewerEls.length, 0);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
new file mode 100644
index 0000000..ce7faae
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
new file mode 100644
index 0000000..32c8313
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
new file mode 100644
index 0000000..52ad066
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-diff-comment-thread></gr-diff-comment-thread>
+ </template>
+</test-fixture>
+
+<test-fixture id="withComment">
+ <template>
+ <gr-diff-comment-thread></gr-diff-comment-thread>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-comment-thread tests', function() {
+ var element;
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('comments are sorted correctly', function() {
+ var comments = [
+ {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
+ },
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ },
+ {
+ id: 'sally_to_dr_finklestein',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ },
+ {
+ id: 'sallys_defiance',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ },
+ {
+ id: 'dr_finklesteins_response',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000'
+ },
+ {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 21:00:20.396000000'
+ }
+ ];
+ var results = element._sortedComments(comments);
+ assert.deepEqual(results, [
+ {
+ id: 'sally_to_dr_finklestein',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ },
+ {
+ id: 'dr_finklesteins_response',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000'
+ },
+ {
+ id: 'sallys_defiance',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ },
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ },
+ {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
+ },
+ {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 21:00:20.396000000'
+ }
+ ]);
+ });
+ });
+
+ suite('comment action tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('withComment');
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ }];
+ flushAsynchronousOperations();
+
+ server = sinon.fakeServer.create();
+ // Eat any requests made by elements in this suite.
+ server.respondWith(
+ 'PUT',
+ '/changes/41/1/drafts',
+ [
+ 201,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' + JSON.stringify({
+ id: '7afa4931_de3d65bd',
+ path: '/path/to/file.txt',
+ line: 5,
+ in_reply_to: 'baf0414d_60047215',
+ updated: '2015-12-21 02:01:10.850000000',
+ message: 'Done'
+ }),
+ ]
+ );
+
+ server.respondWith(
+ 'DELETE',
+ '/changes/41/1/drafts/baf0414d_60047215',
+ [
+ 204,
+ {},
+ '',
+ ]
+ );
+ });
+
+ test('reply', function(done) {
+ var commentEl = element.$$('gr-diff-comment');
+ assert.ok(commentEl);
+ commentEl.addEventListener('reply', function() {
+ var drafts = element._orderedComments.filter(function(c) {
+ return c.__draft == true;
+ });
+ assert.equal(drafts.length, 1);
+ assert.notOk(drafts[0].message, 'message should be empty');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ done();
+ });
+ commentEl.fire('reply', {comment: commentEl.comment}, {bubbles: false});
+ });
+
+ test('quote reply', function(done) {
+ var commentEl = element.$$('gr-diff-comment');
+ assert.ok(commentEl);
+ commentEl.addEventListener('reply', function() {
+ var drafts = element._orderedComments.filter(function(c) {
+ return c.__draft == true;
+ });
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, ' > is this a crossover episode!?\n\n');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ done();
+ });
+ commentEl.fire('reply', {comment: commentEl.comment, quote: true},
+ {bubbles: false});
+ });
+
+ test('done', function(done) {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ var commentEl = element.$$('gr-diff-comment');
+ assert.ok(commentEl);
+ commentEl.addEventListener('done', function() {
+ server.respond();
+ var drafts = element._orderedComments.filter(function(c) {
+ return c.__draft == true;
+ });
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, 'Done');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ done();
+ });
+ commentEl.fire('done', {comment: commentEl.comment}, {bubbles: false});
+ });
+
+ test('discard', function(done) {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.push('comments', element._newReply(
+ element.comments[0].id,
+ element.comments[0].line,
+ element.comments[0].path,
+ 'it’s pronouced jiff, not giff'));
+ flushAsynchronousOperations();
+
+ var draftEl =
+ Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
+ assert.ok(draftEl);
+ draftEl.addEventListener('discard', function() {
+ server.respond();
+ var drafts = element.comments.filter(function(c) {
+ return c.__draft == true;
+ });
+ assert.equal(drafts.length, 0);
+ done();
+ });
+ draftEl.fire('discard', null, {bubbles: false});
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
new file mode 100644
index 0000000..ca6815b
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
new file mode 100644
index 0000000..ca0bedb
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
new file mode 100644
index 0000000..799dbf2
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -0,0 +1,269 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-diff-comment></gr-diff-comment>
+ </template>
+</test-fixture>
+
+<test-fixture id="draft">
+ <template>
+ <gr-diff-comment draft="true"></gr-diff-comment>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-comment tests', function() {
+ var element;
+ setup(function() {
+ element = fixture('basic');
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ };
+ });
+
+ test('proper event fires on reply', function(done) {
+ element.addEventListener('reply', function(e) {
+ assert.ok(e.detail.comment);
+ done();
+ });
+ MockInteractions.tap(element.$$('.reply'));
+ });
+
+ test('proper event fires on quote', function(done) {
+ element.addEventListener('reply', function(e) {
+ assert.ok(e.detail.comment);
+ assert.isTrue(e.detail.quote);
+ done();
+ });
+ MockInteractions.tap(element.$$('.quote'));
+ });
+
+ test('proper event fires on done', function(done) {
+ element.addEventListener('done', function(e) {
+ done();
+ });
+ MockInteractions.tap(element.$$('.done'));
+ });
+
+ test('clicking on date link does not trigger nav', function() {
+ var showStub = sinon.stub(page, 'show');
+ var dateEl = element.$$('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+ var dest = window.location.pathname + '#5';
+ assert(showStub.lastCall.calledWithExactly(dest, null, false),
+ 'Should navigate to ' + dest + ' without triggering nav');
+ showStub.restore();
+ });
+ });
+
+ suite('gr-diff-comment draft tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('draft');
+ element.changeNum = 42;
+ element.patchNum = 1;
+ element.editing = false;
+ element.comment = {
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ path: '/path/to/file',
+ line: 5,
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'PUT',
+ '/changes/42/revisions/1/drafts',
+ [
+ 201,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n{' +
+ '"id": "baf0414d_40572e03",' +
+ '"path": "/path/to/file",' +
+ '"line": 5,' +
+ '"updated": "2015-12-08 21:52:36.177000000",' +
+ '"message": "created!"' +
+ '}'
+ ]
+ );
+
+ server.respondWith(
+ 'PUT',
+ /\/changes\/42\/revisions\/1\/drafts\/.+/,
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n{' +
+ '"id": "baf0414d_40572e03",' +
+ '"path": "/path/to/file",' +
+ '"line": 5,' +
+ '"updated": "2015-12-08 21:52:36.177000000",' +
+ '"message": "saved!"' +
+ '}'
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') != 'none';
+ }
+
+ test('button visibility states', function() {
+ element.showActions = false;
+ assert.isTrue(element.$$('.actions').hasAttribute('hidden'));
+ element.showActions = true;
+ assert.isFalse(element.$$('.actions').hasAttribute('hidden'));
+
+ element.draft = true;
+ assert.isTrue(isVisible(element.$$('.edit')), 'edit is visible');
+ assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
+ assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
+ assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible');
+ assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible');
+ assert.isFalse(isVisible(element.$$('.done')), 'done is not visible');
+
+ element.editing = true;
+ assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
+ assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
+ assert.isTrue(isVisible(element.$$('.save')), 'save is visible');
+ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is visible');
+ assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible');
+ assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible');
+ assert.isFalse(isVisible(element.$$('.done')), 'done is not visible');
+
+ element.draft = false;
+ element.editing = false;
+ assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
+ assert.isFalse(isVisible(element.$$('.discard')),
+ 'discard is not visible');
+ assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
+ assert.isTrue(isVisible(element.$$('.reply')), 'reply is visible');
+ assert.isTrue(isVisible(element.$$('.quote')), 'quote is visible');
+ assert.isTrue(isVisible(element.$$('.done')), 'done is visible');
+
+ element.comment.id = 'foo';
+ element.draft = true;
+ element.editing = true;
+ assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
+ });
+
+ test('draft creation/cancelation', function(done) {
+ assert.isFalse(element.editing);
+ MockInteractions.tap(element.$$('.edit'));
+ assert.isTrue(element.editing);
+
+ element._editDraft = '';
+ // Save should be disabled on an empty message.
+ var disabled = element.$$('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+ element._editDraft = ' ';
+ disabled = element.$$('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+
+ var numDiscardEvents = 0;
+ element.addEventListener('discard', function(e) {
+ numDiscardEvents++;
+ if (numDiscardEvents == 3) {
+ done();
+ }
+ });
+ MockInteractions.tap(element.$$('.cancel'));
+ MockInteractions.tap(element.$$('.discard'));
+ MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc
+ });
+
+ test('draft saving/editing', function(done) {
+ element.draft = true;
+ MockInteractions.tap(element.$$('.edit'));
+ element._editDraft = 'good news, everyone!';
+ MockInteractions.tap(element.$$('.save'));
+ assert.isTrue(element.disabled,
+ 'Element should be disabled when creating draft.');
+
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done creating draft.');
+ assert.equal(req.status, 201);
+ assert.equal(req.url, '/changes/42/revisions/1/drafts');
+ assert.equal(req.response.message, 'created!');
+ assert.isFalse(element.editing);
+ }).then(function() {
+ MockInteractions.tap(element.$$('.edit'));
+ element._editDraft = 'You’ll be delivering a package to Chapek 9, a ' +
+ 'world where humans are killed on sight.';
+ MockInteractions.tap(element.$$('.save'));
+ assert.isTrue(element.disabled,
+ 'Element should be disabled when updating draft.');
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done updating draft.');
+ assert.equal(req.status, 200);
+ assert.equal(req.url,
+ '/changes/42/revisions/1/drafts/baf0414d_40572e03');
+ assert.equal(req.response.message, 'saved!');
+ assert.isFalse(element.editing);
+ done();
+ });
+ });
+ });
+
+ test('clicking on date link does not trigger nav', function() {
+ var showStub = sinon.stub(page, 'show');
+ var dateEl = element.$$('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+ var dest = window.location.pathname + '#5';
+ assert(showStub.lastCall.calledWithExactly(dest, null, false),
+ 'Should navigate to ' + dest + ' without triggering nav');
+ showStub.restore();
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
similarity index 66%
rename from polygerrit-ui/app/elements/gr-diff-preferences.html
rename to polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index e320620..b945a45 100644
--- a/polygerrit-ui/app/elements/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -14,9 +14,9 @@
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 @@
<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>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
new file mode 100644
index 0000000..70d176e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -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});
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
new file mode 100644
index 0000000..2d86a05
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-preferences</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-preferences.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-preferences></gr-diff-preferences>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-preferences tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('model changes', function() {
+ element.prefs = {
+ context: 10,
+ line_length: 100,
+ show_tabs: true,
+ tab_size: 8,
+ };
+
+ element.$.contextSelect.value = '50';
+ element.fire('change', {}, {node: element.$.contextSelect});
+ element.$.columnsInput.bindValue = 80;
+ element.$.tabSizeInput.bindValue = 4;
+ MockInteractions.tap(element.$.showTabsInput);
+
+ assert.equal(element.prefs.context, 50);
+ assert.equal(element.prefs.line_length, 80);
+ assert.equal(element.prefs.tab_size, 4);
+ assert.isFalse(element.prefs.show_tabs);
+ });
+
+ test('events', function(done) {
+ var savePromise = new Promise(function(resolve) {
+ element.addEventListener('save', function() { resolve(); });
+ });
+ var cancelPromise = new Promise(function(resolve) {
+ element.addEventListener('cancel', function() { resolve(); });
+ });
+ Promise.all([savePromise, cancelPromise]).then(function() {
+ done();
+ });
+ MockInteractions.tap(element.$$('gr-button[primary]'));
+ MockInteractions.tap(element.$$('gr-button:not([primary])'));
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
new file mode 100644
index 0000000..972dc2d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
new file mode 100644
index 0000000..518da3e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
new file mode 100644
index 0000000..85a1011
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
@@ -0,0 +1,300 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-diff-side></gr-diff-side>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-side tests', function() {
+ var element;
+
+ function isVisibleInWindow(el) {
+ var rect = el.getBoundingClientRect();
+ return rect.top >= 0 && rect.left >= 0 &&
+ rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
+ }
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('comments', function() {
+ assert.isFalse(element.$$('.container').classList.contains('canComment'));
+ element.canComment = true;
+ assert.isTrue(element.$$('.container').classList.contains('canComment'));
+ // TODO(andybons): Check for comment creation events firing/not firing
+ // when implemented.
+ });
+
+ test('scroll to line', function() {
+ var content = [];
+ for (var i = 0; i < 300; i++) {
+ content.push({
+ type: 'CODE',
+ content: 'All work and no play makes Jack a dull boy',
+ numLines: 1,
+ lineNum: i + 1,
+ highlight: false,
+ intraline: [],
+ });
+ }
+ element.content = content;
+
+ window.scrollTo(0, 0);
+ element.scrollToLine(-12849);
+ assert.equal(window.scrollY, 0);
+ element.scrollToLine('sup');
+ assert.equal(window.scrollY, 0);
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="150"]');
+ assert.ok(lineEl);
+ element.scrollToLine(150);
+ assert.isAbove(window.scrollY, 0);
+ assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
+ });
+
+ test('intraline highlights', function() {
+ var content = ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>';
+ var html = util.escapeHTML(content);
+ var highlights = [
+ {startIndex: 0, endIndex: 33},
+ {startIndex: 75},
+ ];
+ assert.equal(
+ content.slice(highlights[0].startIndex, highlights[0].endIndex),
+ ' <gr-linked-text content="');
+ assert.equal(content.slice(highlights[1].startIndex),
+ '"></gr-linked-text>');
+ var result = element._addIntralineHighlights(content, html, highlights);
+ var expected = element._highlightStartTag +
+ ' <gr-linked-text content="' +
+ element._highlightEndTag +
+ '[[_computeCurrentRevisionMessage(change)]]' +
+ element._highlightStartTag +
+ '"></gr-linked-text>' +
+ element._highlightEndTag;
+ assert.equal(result, expected);
+ });
+
+ test('newlines', function() {
+ element.prefs = {
+ line_length: 80,
+ tab_size: 4,
+ };
+
+ element.content = [{
+ type: 'CODE',
+ content: 'A'.repeat(50),
+ numLines: 1,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ var contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ assert.equal(contentEl.innerHTML, 'A'.repeat(50));
+
+ element.content = [{
+ type: 'CODE',
+ content: 'A'.repeat(100),
+ numLines: 2,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+
+ lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ assert.equal(contentEl.innerHTML,
+ 'A'.repeat(80) + element._lineFeedHTML +
+ 'A'.repeat(20) + element._lineFeedHTML);
+ });
+
+ test('tabs', function(done) {
+ element.prefs = {
+ line_length: 80,
+ tab_size: 4,
+ show_tabs: true,
+ };
+
+ element.content = [{
+ type: 'CODE',
+ content: 'A'.repeat(50) + '\t' + 'A'.repeat(50),
+ numLines: 2,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ var contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ var spanEl = contentEl.childNodes[1];
+ assert.equal(spanEl.tagName, 'SPAN');
+ assert.isTrue(spanEl.classList.contains(
+ 'style-scope', 'gr-diff-side', 'tab', 'withIndicator'));
+
+ element.prefs.show_tabs = false;
+ element.content = [{
+ type: 'CODE',
+ content: 'A'.repeat(50) + '\t' + 'A'.repeat(50),
+ numLines: 2,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+ contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ spanEl = contentEl.childNodes[1];
+ assert.equal(spanEl.tagName, 'SPAN');
+ assert.isTrue(spanEl.classList.contains(
+ 'style-scope', 'gr-diff-side', 'tab'));
+
+ var alertStub = sinon.stub(window, 'alert');
+ element.prefs.tab_size =
+ '"><img src="/" onerror="alert(1);"><span class="';
+ element.content = [{
+ type: 'CODE',
+ content: '\t',
+ numLines: 1,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+ element.async(function() {
+ assert.isFalse(alertStub.called);
+ alertStub.restore();
+ done();
+ }, 100); // Allow some time for the img error event to fire.
+ });
+
+ test('diff context', function() {
+ var content = [
+ {type: 'CODE', hidden: true, content: '<!DOCTYPE html>'},
+ {type: 'CODE', hidden: true, content: '<meta charset="utf-8">'},
+ {type: 'CODE', hidden: true, content: '<title>My great page</title>'},
+ {type: 'CODE', hidden: true, content: '<style>'},
+ {type: 'CODE', hidden: true, content: ' *,'},
+ {type: 'CODE', hidden: true, content: ' *:before,'},
+ {type: 'CODE', hidden: true, content: ' *:after {'},
+ {type: 'CODE', hidden: true, content: ' box-sizing: border-box;'},
+ {type: 'CONTEXT_CONTROL', numLines: 8, start: 0, end: 8},
+ {type: 'CODE', hidden: false, content: ' }'},
+ ];
+ element.content = content;
+
+ // Only the context elements and the following code line elements should
+ // be present in the DOM.
+ var contextEls =
+ Polymer.dom(element.root).querySelectorAll('.contextControl');
+ assert.equal(contextEls.length, 2);
+ var codeLineEls =
+ Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
+ assert.equal(codeLineEls.length, 2);
+
+ for (var i = 0; i <= 8; i++) {
+ element.content[i].hidden = false;
+ }
+ element.renderLineIndexRange(0, 8);
+ element.hideElementsWithIndex(8);
+
+ contextEls =
+ Polymer.dom(element.root).querySelectorAll('.contextControl');
+ for (var i = 0; i < contextEls.length; i++) {
+ assert.isTrue(contextEls[i].hasAttribute('hidden'));
+ }
+
+ codeLineEls =
+ Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
+
+ // Nine lines should now be present in the DOM.
+ assert.equal(codeLineEls.length, 9 * 2);
+ });
+
+ test('tap line to add a draft', function() {
+ var numAddDraftEvents = 0;
+ sinon.stub(element, 'fire', function(eventName) {
+ if (eventName == 'add-draft') {
+ numAddDraftEvents++;
+ }
+ });
+ element.content = [{type: 'CODE', content: '<!DOCTYPE html>'}];
+ element.canComment = false;
+ flushAsynchronousOperations();
+
+ var lineEl = element.$$('.lineNum');
+ assert.ok(lineEl);
+ MockInteractions.tap(lineEl);
+ assert.equal(numAddDraftEvents, 0);
+
+ element.canComment = true;
+ MockInteractions.tap(lineEl);
+ assert.equal(numAddDraftEvents, 1);
+ });
+
+ test('jump to diff chunk/thread', function() {
+ element.content = [
+ {type: 'CODE', content: '', intraline: [], lineNum: 1, highlight: true},
+ {type: 'CODE', content: '', intraline: [], lineNum: 2, highlight: true},
+ {type: 'CODE', content: '', intraline: [], lineNum: 3 },
+ {type: 'CODE', content: '', intraline: [], lineNum: 4 },
+ {type: 'COMMENT_THREAD', comments: [ { line: 4 }]},
+ {type: 'CODE', content: '', intraline: [], lineNum: 5 },
+ {type: 'CODE', content: '', intraline: [], lineNum: 6, highlight: true},
+ {type: 'CODE', content: '', intraline: [], lineNum: 7, highlight: true},
+ {type: 'CODE', content: '', intraline: [], lineNum: 8 },
+ {type: 'COMMENT_THREAD', comments: [ { line: 8 }]},
+ {type: 'CODE', content: '', intraline: [], lineNum: 9 },
+ {type: 'CODE', content: '', intraline: [], lineNum: 10,
+ highlight: true},
+ ];
+
+ var scrollToLineStub = sinon.stub(element, 'scrollToLine');
+ element.scrollToNextDiffChunk();
+ assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6));
+ element.scrollToPreviousDiffChunk();
+ assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(1));
+ element.scrollToNextCommentThread();
+ assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(4));
+ element.scrollToNextCommentThread();
+ assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(8));
+ element.scrollToPreviousDiffChunk();
+ assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6));
+
+ scrollToLineStub.restore();
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
new file mode 100644
index 0000000..0dc18ae
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
new file mode 100644
index 0000000..847a641
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
new file mode 100644
index 0000000..bfe4906
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -0,0 +1,395 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-diff-view></gr-diff-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-view tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.$.changeDetailXHR.auto = false;
+ element.$.filesXHR.auto = false;
+ element.$.configXHR.auto = false;
+ element.$.diff.auto = false;
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'PUT',
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
+ [
+ 201,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ '""',
+ ]
+ );
+ server.respondWith(
+ 'DELETE',
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
+ [
+ 204,
+ {'Content-Type': 'application/json'},
+ '',
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('keyboard shortcuts', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ patchNum: '10',
+ };
+ element._change = {
+ revisions: {
+ a: { _number: 10, },
+ },
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+ element.changeViewState.selectedFileIndex = 1;
+
+ var showStub = sinon.stub(page, 'show');
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u'
+ assert(showStub.lastCall.calledWithExactly('/c/42/'),
+ 'Should navigate to /c/42/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221); // ']'
+ assert(showStub.lastCall.calledWithExactly('/c/42/10/wheatley.md'),
+ 'Should navigate to /c/42/10/wheatley.md');
+ element._path = 'wheatley.md';
+ assert.equal(element.changeViewState.selectedFileIndex, 2);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/10/glados.txt'),
+ 'Should navigate to /c/42/10/glados.txt');
+ element._path = 'glados.txt';
+ assert.equal(element.changeViewState.selectedFileIndex, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/10/chell.go'),
+ 'Should navigate to /c/42/10/chell.go');
+ element._path = 'chell.go';
+ assert.equal(element.changeViewState.selectedFileIndex, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/'),
+ 'Should navigate to /c/42/');
+ assert.equal(element.changeViewState.selectedFileIndex, 0);
+
+ var showPrefsStub = sinon.stub(element.$.diff, 'showDiffPreferences');
+ MockInteractions.pressAndReleaseKeyOn(element, 188); // ','
+ assert(showPrefsStub.calledOnce);
+
+ var scrollStub = sinon.stub(element.$.diff, 'scrollToNextDiffChunk');
+ MockInteractions.pressAndReleaseKeyOn(element, 78); // 'n'
+ assert(scrollStub.calledOnce);
+ scrollStub.restore();
+
+ scrollStub = sinon.stub(element.$.diff, 'scrollToPreviousDiffChunk');
+ MockInteractions.pressAndReleaseKeyOn(element, 80); // 'p'
+ assert(scrollStub.calledOnce);
+ scrollStub.restore();
+
+ scrollStub = sinon.stub(element.$.diff, 'scrollToNextCommentThread');
+ MockInteractions.pressAndReleaseKeyOn(element, 78, ['shift']); // 'N'
+ assert(scrollStub.calledOnce);
+ scrollStub.restore();
+
+ scrollStub = sinon.stub(element.$.diff, 'scrollToPreviousCommentThread');
+ MockInteractions.pressAndReleaseKeyOn(element, 80, ['shift']); // 'P'
+ assert(scrollStub.calledOnce);
+ scrollStub.restore();
+
+ showPrefsStub.restore();
+ showStub.restore();
+ });
+
+ test('keyboard shortcuts with patch range', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: '5',
+ patchNum: '10',
+ };
+ element._change = {
+ revisions: {
+ a: { _number: 10, },
+ },
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+
+ var showStub = sinon.stub(page, 'show');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' +
+ 'only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(showStub.lastCall.calledWithExactly('/c/42/'),
+ 'Should navigate to /c/42/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u'
+ assert(showStub.lastCall.calledWithExactly('/c/42/'),
+ 'Should navigate to /c/42/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221); // ']'
+ assert(showStub.lastCall.calledWithExactly('/c/42/5..10/wheatley.md'),
+ 'Should navigate to /c/42/5..10/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/5..10/glados.txt'),
+ 'Should navigate to /c/42/5..10/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/5..10/chell.go'),
+ 'Should navigate to /c/42/5..10/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/'),
+ 'Should navigate to /c/42/');
+
+ showStub.restore();
+ });
+
+ test('keyboard shortcuts with old patch number', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ patchNum: '1',
+ };
+ element._change = {
+ revisions: {
+ a: { _number: 1, },
+ b: { _number: 2, },
+ },
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+
+ var showStub = sinon.stub(page, 'show');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' +
+ 'only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u'
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221); // ']'
+ assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'),
+ 'Should navigate to /c/42/1/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'),
+ 'Should navigate to /c/42/1/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'),
+ 'Should navigate to /c/42/1/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+
+ showStub.restore();
+ });
+
+ test('go up to change via kb without change loaded', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ patchNum: '1',
+ };
+
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+
+ var showStub = sinon.stub(page, 'show');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' +
+ 'only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a'
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u'
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221); // ']'
+ assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'),
+ 'Should navigate to /c/42/1/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'),
+ 'Should navigate to /c/42/1/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'),
+ 'Should navigate to /c/42/1/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+
+ showStub.restore();
+ });
+
+ test('jump to file dropdown', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ patchNum: '10',
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+ flushAsynchronousOperations();
+ var linkEls =
+ Polymer.dom(element.root).querySelectorAll('.dropdown-content > a');
+ assert.equal(linkEls.length, 3);
+ assert.isFalse(linkEls[0].hasAttribute('selected'));
+ assert.isTrue(linkEls[1].hasAttribute('selected'));
+ assert.isFalse(linkEls[2].hasAttribute('selected'));
+ assert.equal(linkEls[0].getAttribute('data-key-nav'), '[');
+ assert.equal(linkEls[1].getAttribute('data-key-nav'), '');
+ assert.equal(linkEls[2].getAttribute('data-key-nav'), ']');
+ assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/chell.go');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/glados.txt');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/10/wheatley.md');
+
+ assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
+ '/foo/bar/baz');
+ assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
+ 'Commit message');
+ });
+
+ test('jump to file dropdown with patch range', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: '5',
+ patchNum: '10',
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+ flushAsynchronousOperations();
+ var linkEls =
+ Polymer.dom(element.root).querySelectorAll('.dropdown-content > a');
+ assert.equal(linkEls.length, 3);
+ assert.isFalse(linkEls[0].hasAttribute('selected'));
+ assert.isTrue(linkEls[1].hasAttribute('selected'));
+ assert.isFalse(linkEls[2].hasAttribute('selected'));
+ assert.equal(linkEls[0].getAttribute('data-key-nav'), '[');
+ assert.equal(linkEls[1].getAttribute('data-key-nav'), '');
+ assert.equal(linkEls[2].getAttribute('data-key-nav'), ']');
+ assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/chell.go');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/glados.txt');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md');
+ });
+
+ test('file review status', function(done) {
+ element._loggedIn = true;
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: '1',
+ patchNum: '2',
+ };
+ element._fileList = ['/COMMIT_MSG'];
+ element._path = '/COMMIT_MSG';
+
+ server.respond();
+
+ element.async(function() {
+ var commitMsg = Polymer.dom(element.root).querySelector(
+ 'input[type="checkbox"]');
+
+ assert.isTrue(commitMsg.checked);
+
+ MockInteractions.tap(commitMsg);
+ server.respond();
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(commitMsg.checked);
+ assert.equal(req.status, 204);
+ assert.equal(req.url,
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
+
+ MockInteractions.tap(commitMsg);
+ server.respond();
+ }).then(function() {
+ element._xhrPromise.then(function(req) {
+ assert.isTrue(commitMsg.checked);
+ assert.equal(req.status, 201);
+ assert.equal(req.url,
+ '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
+
+ done();
+ });
+ });
+ }, 1);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
new file mode 100644
index 0000000..21ee076
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
new file mode 100644
index 0000000..485e2cc
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -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));
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
new file mode 100644
index 0000000..9a8cb81
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -0,0 +1,574 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-diff></gr-diff>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.changeNum = 42;
+ element.path = 'sieve.go';
+ element.prefs = {
+ context: 10,
+ tab_size: 8,
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ /\/changes\/42\/revisions\/(1|2)\/files\/sieve\.go\/diff(.*)/,
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ change_type: 'MODIFIED',
+ content: [
+ {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ '<title>My great page</title>',
+ '<style>',
+ ' *,',
+ ' *:before,',
+ ' *:after {',
+ ' box-sizing: border-box;',
+ ' }',
+ '</style>',
+ '<header>',
+ ]
+ },
+ {
+ a: [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ],
+ b: [
+ ' Hello, world!',
+ ],
+ },
+ {
+ ab: [
+ '</header>',
+ '<body>',
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]
+ },
+ ]
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/comments',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_5f2b8b82',
+ line: 1,
+ message: 'this isn’t quite right',
+ updated: '2015-12-10 02:50:21.627000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2015-12-10 00:08:42.255000000',
+ },
+ ],
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/comments',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f30f3048',
+ line: 12,
+ message: 'What on earth are you thinking, here?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f6b1b1c8',
+ in_reply_to: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'Yeah not sure how this worked either?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: 'a0407443_30dfe8fb',
+ in_reply_to: '001a2067_f30f3048',
+ line: 12,
+ message: '¯\\_(ツ)_/¯',
+ updated: '2015-12-12 18:50:21.627000000',
+ },
+ ],
+ }),
+ ]
+ );
+
+ server.respondWith(
+ 'PUT',
+ '/accounts/self/preferences.diff',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({context: 25}),
+ ]
+ );
+
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('comments with parent', function(done) {
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+
+ element.reload();
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 1);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('comments between two patches', function(done) {
+ element.patchRange = {
+ basePatchNum: 1,
+ patchNum: 2,
+ };
+
+ element.reload();
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 2);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('comment rendering', function(done) {
+ element.prefs.context = -1;
+ element._loggedIn = true;
+ element.patchRange = {
+ basePatchNum: 1,
+ patchNum: 2,
+ };
+
+ element.reload();
+ server.respond();
+
+ // Allow events to fire and the threads to render.
+ element.async(function() {
+ var leftThreadEls =
+ Polymer.dom(element.$.leftDiff.root).querySelectorAll(
+ 'gr-diff-comment-thread');
+ assert.equal(leftThreadEls.length, 1);
+ assert.equal(leftThreadEls[0].comments.length, 1);
+
+ var rightThreadEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ 'gr-diff-comment-thread');
+ assert.equal(rightThreadEls.length, 1);
+ assert.equal(rightThreadEls[0].comments.length, 2);
+
+ var index = leftThreadEls[0].getAttribute('data-index');
+ var leftFillerEls =
+ Polymer.dom(element.$.leftDiff.root).querySelectorAll(
+ '.commentThread.filler[data-index="' + index + '"]');
+ assert.equal(leftFillerEls.length, 1);
+ var rightFillerEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ assert.equal(rightFillerEls.length, 2);
+
+ for (var i = 0; i < rightFillerEls.length; i++) {
+ assert.isTrue(rightFillerEls[i].classList.contains('filler'));
+ }
+ var originalHeight = rightFillerEls[0].offsetHeight;
+ assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
+ assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
+ assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
+
+ // Create a comment on the opposite side of the first comment.
+ var rightLineEL = element.$.rightDiff.$$(
+ '.lineNum[data-index="' + (index - 1) + '"]');
+ assert.ok(rightLineEL);
+ MockInteractions.tap(rightLineEL);
+ element.async(function() {
+ var newThreadEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ assert.equal(newThreadEls.length, 2);
+ for (var i = 0; i < newThreadEls.length; i++) {
+ assert.isTrue(
+ newThreadEls[i].classList.contains('commentThread') ||
+ newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD');
+ }
+ var newHeight = newThreadEls[0].offsetHeight;
+ assert.equal(newThreadEls[1].offsetHeight, newHeight);
+ assert.equal(leftFillerEls[0].offsetHeight, newHeight);
+ assert.equal(leftThreadEls[0].offsetHeight, newHeight);
+
+ // The editing mode height of the right comment will be greater than
+ // the non-editing mode height of the left comment.
+ assert.isAbove(newHeight, originalHeight);
+
+ // Discard the right thread and ensure the left comment heights are
+ // back to their original values.
+ newThreadEls[1].addEventListener('discard', function() {
+ rightFillerEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ assert.equal(rightFillerEls.length, 2);
+
+ for (var i = 0; i < rightFillerEls.length; i++) {
+ assert.isTrue(rightFillerEls[i].classList.contains('filler'));
+ }
+ var originalHeight = rightFillerEls[0].offsetHeight;
+ assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
+ assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
+ assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
+ done();
+ });
+ var commentEl = newThreadEls[1].$$('gr-diff-comment');
+ commentEl.fire('discard', null, {bubbles: false});
+ }, 1);
+ }, 1);
+ });
+
+ test('intraline normalization', function() {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ var content = [
+ ' <section class="summary">',
+ ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+ ' </section>',
+ ];
+ var highlights = [
+ [31, 34], [42, 26]
+ ];
+ var results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 75,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 6,
+ }
+ ]);
+
+ content = [
+ ' this._path = value.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) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ }
+ ]);
+ });
+
+ test('context', function() {
+ element.prefs.context = 3;
+ element._diffResponse = {
+ content: [
+ {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ '<title>My great page</title>',
+ '<style>',
+ ' *,',
+ ' *:before,',
+ ' *:after {',
+ ' box-sizing: border-box;',
+ ' }',
+ '</style>',
+ '<header>',
+ ]
+ },
+ {
+ a: [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ],
+ b: [
+ ' Hello, world!',
+ ],
+ },
+ {
+ ab: [
+ '</header>',
+ '<body>',
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]
+ },
+ ]
+ };
+ element._processContent();
+
+ // First eight lines should be hidden on both sides.
+ for (var i = 0; i < 8; i++) {
+ assert.isTrue(element._diff.leftSide[i].hidden);
+ assert.isTrue(element._diff.rightSide[i].hidden);
+ }
+ // A context control should be at index 8 on both sides.
+ var leftContext = element._diff.leftSide[8];
+ var rightContext = element._diff.rightSide[8];
+ assert.deepEqual(leftContext, rightContext);
+ assert.equal(leftContext.numLines, 8);
+ assert.equal(leftContext.start, 0);
+ assert.equal(leftContext.end, 8);
+
+ // Line indices 9-16 should be shown.
+ for (var i = 9; i <= 16; i++) {
+ // notOk (falsy) because the `hidden` attribute may not be present.
+ assert.notOk(element._diff.leftSide[i].hidden);
+ assert.notOk(element._diff.rightSide[i].hidden);
+ }
+
+ // Lines at indices 17 and 18 should be hidden.
+ assert.isTrue(element._diff.leftSide[17].hidden);
+ assert.isTrue(element._diff.rightSide[17].hidden);
+ assert.isTrue(element._diff.leftSide[18].hidden);
+ assert.isTrue(element._diff.rightSide[18].hidden);
+
+ // Context control at index 19.
+ leftContext = element._diff.leftSide[19];
+ rightContext = element._diff.rightSide[19];
+ assert.deepEqual(leftContext, rightContext);
+ assert.equal(leftContext.numLines, 2);
+ assert.equal(leftContext.start, 17);
+ assert.equal(leftContext.end, 19);
+ });
+
+ test('save prefs', function(done) {
+ element._loggedIn = false;
+
+ element.prefs = {
+ tab_size: 4,
+ context: 50,
+ };
+ element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
+ assert.isTrue(element._diffPreferencesPromise == null);
+
+ element._loggedIn = true;
+ element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
+ server.respond();
+
+ element._diffPreferencesPromise.then(function(req) {
+ assert.equal(req.xhr.requestBody, JSON.stringify(element.prefs));
+ done();
+ });
+ });
+
+ test('visible line length', function() {
+ assert.equal(element._visibleLineLength('A'.repeat(5)), 5);
+ assert.equal(
+ element._visibleLineLength('A'.repeat(5) + '\t' + 'A'.repeat(5)), 18);
+ });
+
+ test('break up common diff chunks', function() {
+ element._groupedBaseComments = {
+ 1: {},
+ };
+ element._groupedComments = {
+ 10: {},
+ };
+ var ctx = {
+ left: {lineNum: 0},
+ right: {lineNum: 0},
+ };
+ var content = [
+ {
+ ab: [
+ '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.',
+ ]
+ }
+ ];
+ var result = element._breakUpCommonChunksWithComments(ctx, content);
+ assert.deepEqual(result, [
+ {
+ __noHighlight: true,
+ a: ['Copyright (C) 2015 The Android Open Source Project'],
+ b: ['Copyright (C) 2015 The Android Open Source Project'],
+ },
+ {
+ ab: [
+ '',
+ '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, ',
+ ]
+ },
+ {
+ __noHighlight: true,
+ a: ['software distributed under the License is distributed on an '],
+ b: ['software distributed under the License is distributed on an ']
+ },
+ {
+ ab: [
+ '"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.',
+ ]
+ }
+ ]);
+ });
+ });
+
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
new file mode 100644
index 0000000..b0ee0b73
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -0,0 +1,53 @@
+<!--
+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-patch-range-select">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .patchRange {
+ display: inline-block;
+ }
+ </style>
+ Patch set:
+ <span class="patchRange">
+ <select id="leftPatchSelect" on-change="_handlePatchChange">
+ <option value="PARENT"
+ selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option>
+ <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
+ <option value$="[[patchNum]]"
+ selected$="[[_computeLeftSelected(patchNum, patchRange)]]"
+ disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
+ </template>
+ </select>
+ </span>
+ →
+ <span class="patchRange">
+ <select id="rightPatchSelect" on-change="_handlePatchChange">
+ <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
+ <option value$="[[patchNum]]"
+ selected$="[[_computeRightSelected(patchNum, patchRange)]]"
+ disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
+ </template>
+ </select>
+ </span>
+ </template>
+ <script src="gr-patch-range-select.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
new file mode 100644
index 0000000..3439ecd
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -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);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
new file mode 100644
index 0000000..a7d909e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-patch-range-select auto></gr-patch-range-select>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-patch-range-select tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('enabled/disabled options', function() {
+ var patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
+ ['1', '2', '3'].forEach(function(patchNum) {
+ assert.isFalse(element._computeRightDisabled(patchNum, patchRange));
+ });
+ ['PARENT', '1', '2'].forEach(function(patchNum) {
+ assert.isFalse(element._computeLeftDisabled(patchNum, patchRange));
+ });
+ assert.isTrue(element._computeLeftDisabled('3', patchRange));
+
+ patchRange.basePatchNum = '2';
+ assert.isTrue(element._computeLeftDisabled('3', patchRange));
+ assert.isTrue(element._computeRightDisabled('1', patchRange));
+ assert.isTrue(element._computeRightDisabled('2', patchRange));
+ assert.isFalse(element._computeRightDisabled('3', patchRange));
+ });
+
+ test('navigation', function(done) {
+ var showStub = sinon.stub(page, 'show');
+ var leftSelectEl = element.$.leftPatchSelect;
+ var rightSelectEl = element.$.rightPatchSelect;
+ element.changeNum = '42';
+ element.path = 'path/to/file.txt';
+ element.availablePatches = ['1', '2', '3'];
+ flushAsynchronousOperations();
+
+ var numEvents = 0;
+ leftSelectEl.addEventListener('change', function(e) {
+ numEvents++;
+ if (numEvents == 1) {
+ assert(showStub.lastCall.calledWithExactly(
+ '/c/42/3/path/to/file.txt'),
+ 'Should navigate to /c/42/3/path/to/file.txt');
+ leftSelectEl.value = '1';
+ element.fire('change', {}, {node: leftSelectEl});
+ } else if (numEvents == 2) {
+ assert(showStub.lastCall.calledWithExactly(
+ '/c/42/1..3/path/to/file.txt'),
+ 'Should navigate to /c/42/1..3/path/to/file.txt');
+ showStub.restore();
+ done();
+ }
+ });
+ leftSelectEl.value = 'PARENT';
+ rightSelectEl.value = '3';
+ element.fire('change', {}, {node: leftSelectEl});
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-ajax.html b/polygerrit-ui/app/elements/gr-ajax.html
deleted file mode 100644
index 58b647e..0000000
--- a/polygerrit-ui/app/elements/gr-ajax.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 8890983..e7fa31c 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -17,15 +17,17 @@
<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 @@
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>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
new file mode 100644
index 0000000..23ee6c6
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -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();
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/gr-avatar.html b/polygerrit-ui/app/elements/gr-avatar.html
deleted file mode 100644
index c6b14c7..0000000
--- a/polygerrit-ui/app/elements/gr-avatar.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-change-actions.html b/polygerrit-ui/app/elements/gr-change-actions.html
deleted file mode 100644
index 2381ce4..0000000
--- a/polygerrit-ui/app/elements/gr-change-actions.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html
deleted file mode 100644
index 1e57e09..0000000
--- a/polygerrit-ui/app/elements/gr-change-list-item.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-change-list-view.html b/polygerrit-ui/app/elements/gr-change-list-view.html
deleted file mode 100644
index d6f7649..0000000
--- a/polygerrit-ui/app/elements/gr-change-list-view.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-change-list.html b/polygerrit-ui/app/elements/gr-change-list.html
deleted file mode 100644
index 1a0341a..0000000
--- a/polygerrit-ui/app/elements/gr-change-list.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-change-star.html b/polygerrit-ui/app/elements/gr-change-star.html
deleted file mode 100644
index ad377ef..0000000
--- a/polygerrit-ui/app/elements/gr-change-star.html
+++ /dev/null
@@ -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">
-<link rel="import" href="gr-request.html">
-
-<dom-module id="gr-change-star">
- <template>
- <style>
- :host {
- display: inline-block;
- overflow: hidden;
- }
- .starButton {
- background-color: transparent;
- border-color: transparent;
- cursor: pointer;
- font-size: 1.1em;
- width: 1.2em;
- height: 1.2em;
- outline: none;
- }
- .starButton svg {
- fill: #ccc;
- width: 1em;
- height: 1em;
- }
- .starButton-active svg {
- fill: #ffac33;
- }
- </style>
- <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap">
- <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 -->
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657 l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657 L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg>
- </button>
- </template>
- <script>
- (function() {
- 'use strict';
-
- Polymer({
- is: 'gr-change-star',
-
- properties: {
- change: {
- type: Object,
- notify: true,
- },
-
- _xhrPromise: Object, // Used for testing.
- },
-
- _computeStarClass: function(starred) {
- var classes = ['starButton'];
- if (starred) {
- classes.push('starButton-active');
- }
- return classes.join(' ');
- },
-
- _handleStarTap: function() {
- var method = this.change.starred ? 'DELETE' : 'PUT';
- this.set('change.starred', !this.change.starred);
- this._send(method, this._restEndpoint()).catch(function(err) {
- this.set('change.starred', !this.change.starred);
- alert('Change couldn’t be starred. Check the console and contact ' +
- 'the PolyGerrit team for assistance.');
- throw err;
- }.bind(this));
- },
-
- _send: function(method, url) {
- var xhr = document.createElement('gr-request');
- this._xhrPromise = xhr.send({
- method: method,
- url: url,
- });
- return this._xhrPromise;
- },
-
- _restEndpoint: function() {
- return '/accounts/self/starred.changes/' + this.change._number;
- },
-
- });
- })();
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
deleted file mode 100644
index 05eb709..0000000
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-comment-list.html b/polygerrit-ui/app/elements/gr-comment-list.html
deleted file mode 100644
index 6fb5dae..0000000
--- a/polygerrit-ui/app/elements/gr-comment-list.html
+++ /dev/null
@@ -1,118 +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-comment-list">
- <template>
- <style>
- :host {
- display: block;
- }
- .file {
- border-top: 1px solid #ddd;
- font-weight: bold;
- margin: 10px 0 3px;
- padding: 10px 0 5px;
- }
- .container {
- display: flex;
- margin: 5px 0;
- }
- .lineNum {
- margin-right: .35em;
- min-width: 7em;
- }
- .message {
- flex: 1;
- white-space: pre-wrap;
- word-wrap: break-word;
- }
- </style>
- <template is="dom-repeat" items="{{_files}}" as="file">
- <div class="file">
- <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
- </div>
- <template is="dom-repeat"
- items="[[_computeCommentsForFile(file)]]" as="comment">
- <div class="container">
- <a class="lineNum"
- href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
- <span hidden$="[[!comment.line]]">
- <span>[[_computePatchDisplayName(comment)]]</span>
- Line <span>[[comment.line]]</span>:
- </span>
- <span hidden$="[[comment.line]]">
- File comment:
- </span>
- </a>
- <div class="message">[[comment.message]]</div>
- </div>
- </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>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-confirm-dialog.html b/polygerrit-ui/app/elements/gr-confirm-dialog.html
deleted file mode 100644
index a82e31c..0000000
--- a/polygerrit-ui/app/elements/gr-confirm-dialog.html
+++ /dev/null
@@ -1,89 +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="gr-button.html">
-
-<dom-module id="gr-confirm-dialog">
- <template>
- <style>
- :host {
- display: block;
- }
- header {
- border-bottom: 1px solid #ddd;
- font-weight: bold;
- }
- header,
- main,
- footer {
- padding: .5em .65em;
- }
- footer {
- display: flex;
- justify-content: space-between;
- }
- </style>
- <header><content select=".header"></content></header>
- <main><content select=".main"></content></main>
- <footer>
- <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button>
- <gr-button on-tap="_handleCancelTap">Cancel</gr-button>
- </footer>
- </template>
- <script>
- (function() {
- 'use strict';
-
- Polymer({
- is: 'gr-confirm-dialog',
-
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
-
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- properties: {
- confirmLabel: {
- type: String,
- value: 'Confirm',
- }
- },
-
- hostAttributes: {
- role: 'dialog',
- },
-
- _handleConfirmTap: function(e) {
- e.preventDefault();
- this.fire('confirm', null, {bubbles: false});
- },
-
- _handleCancelTap: function(e) {
- e.preventDefault();
- this.fire('cancel', null, {bubbles: false});
- },
- });
- })();
- </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-dashboard-view.html b/polygerrit-ui/app/elements/gr-dashboard-view.html
deleted file mode 100644
index cdd4b1a..0000000
--- a/polygerrit-ui/app/elements/gr-dashboard-view.html
+++ /dev/null
@@ -1,129 +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">
-
-<dom-module id="gr-dashboard-view">
- <template>
- <style>
- :host {
- background-color: var(--view-background-color);
- display: block;
- margin: 0 var(--default-horizontal-margin);
- }
- .loading {
- margin-top: 1em;
- color: #666;
- background-color: #f1f2f3;
- }
- gr-change-list {
- margin-top: 1em;
- width: 100%;
- }
- @media only screen and (max-width: 50em) {
- :host {
- margin: 0;
- }
- .loading {
- padding: 0 var(--default-horizontal-margin);
- }
- }
- </style>
- <gr-ajax
- auto
- url="/changes/"
- params="[[_computeQueryParams()]]"
- last-response="{{_results}}"
- loading="{{_loading}}"></gr-ajax>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <div hidden$="[[_loading]]" hidden>
- <gr-change-list
- show-star
- show-reviewed-state
- account="[[account]]"
- selected-index="{{viewState.selectedChangeIndex}}"
- groups="{{_results}}"
- 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>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-date-formatter.html b/polygerrit-ui/app/elements/gr-date-formatter.html
deleted file mode 100644
index a13dbb8..0000000
--- a/polygerrit-ui/app/elements/gr-date-formatter.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/gr-diff-comment-thread.html
deleted file mode 100644
index 178b417..0000000
--- a/polygerrit-ui/app/elements/gr-diff-comment-thread.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment.html b/polygerrit-ui/app/elements/gr-diff-comment.html
deleted file mode 100644
index c544a8d..0000000
--- a/polygerrit-ui/app/elements/gr-diff-comment.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-diff-side.html b/polygerrit-ui/app/elements/gr-diff-side.html
deleted file mode 100644
index 09ed5f8..0000000
--- a/polygerrit-ui/app/elements/gr-diff-side.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
deleted file mode 100644
index 761dd71..0000000
--- a/polygerrit-ui/app/elements/gr-diff-view.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-diff.html b/polygerrit-ui/app/elements/gr-diff.html
deleted file mode 100644
index 39b1c9c..0000000
--- a/polygerrit-ui/app/elements/gr-diff.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-download-dialog.html b/polygerrit-ui/app/elements/gr-download-dialog.html
deleted file mode 100644
index 3212a0f..0000000
--- a/polygerrit-ui/app/elements/gr-download-dialog.html
+++ /dev/null
@@ -1,269 +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-button.html">
-
-<dom-module id="gr-download-dialog">
- <template>
- <style>
- :host {
- display: block;
- padding: 1em;
- }
- ul {
- list-style: none;
- margin-bottom: .5em;
- }
- li {
- display: inline-block;
- margin: 0;
- padding: 0;
- }
- li gr-button {
- margin-right: 1em;
- }
- label,
- input {
- display: block;
- }
- label {
- font-weight: bold;
- }
- input {
- font-family: var(--monospace-font-family);
- font-size: inherit;
- margin-bottom: .5em;
- width: 60em;
- }
- li[selected] gr-button {
- color: #000;
- font-weight: bold;
- text-decoration: none;
- }
- header {
- display: flex;
- justify-content: space-between;
- }
- main {
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #ddd;
- padding: .5em;
- }
- footer {
- display: flex;
- justify-content: space-between;
- padding-top: .75em;
- }
- .closeButtonContainer {
- display: flex;
- flex: 1;
- justify-content: flex-end;
- }
- .patchFiles {
- margin-right: 2em;
- }
- .patchFiles a,
- .archives a {
- display: inline-block;
- margin-right: 1em;
- }
- .patchFiles a:last-of-type,
- .archives a:last-of-type {
- margin-right: 0;
- }
- </style>
- <header>
- <ul hidden$="[[!_schemes.length]]" hidden>
- <template is="dom-repeat" items="[[_schemes]]" as="scheme">
- <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]">
- <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
- [[scheme]]
- </gr-button>
- </li>
- </template>
- </ul>
- <span class="closeButtonContainer">
- <gr-button link on-tap="_handleCloseTap">Close</gr-button>
- </span>
- </header>
- <main hidden$="[[!_schemes.length]]" hidden>
- <template is="dom-repeat"
- items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
- as="command">
- <div class="command">
- <label>[[command.title]]</label>
- <input is="iron-input"
- type="text"
- bind-value="[[command.command]]"
- on-tap="_handleInputTap"
- readonly>
- </div>
- </template>
- </main>
- <footer>
- <div class="patchFiles">
- <label>Patch file</label>
- <div>
- <a href$="[[_computeDownloadLink(change, patchNum)]]">
- [[_computeDownloadFilename(change, patchNum)]]
- </a>
- <a href$="[[_computeZipDownloadLink(change, patchNum)]]">
- [[_computeZipDownloadFilename(change, patchNum)]]
- </a>
- </div>
- </div>
- <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
- <label>Archive</label>
- <div class="archives">
- <template is="dom-repeat" items="[[config.archives]]" as="format">
- <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
- [[format]]
- </a>
- </template>
- </div>
- </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>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-file-list.html b/polygerrit-ui/app/elements/gr-file-list.html
deleted file mode 100644
index bcf3e05..0000000
--- a/polygerrit-ui/app/elements/gr-file-list.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-linked-text.html b/polygerrit-ui/app/elements/gr-linked-text.html
deleted file mode 100644
index 5c2d9a5..0000000
--- a/polygerrit-ui/app/elements/gr-linked-text.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html
deleted file mode 100644
index fef8968..0000000
--- a/polygerrit-ui/app/elements/gr-message.html
+++ /dev/null
@@ -1,223 +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-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">
-
-<dom-module id="gr-message">
- <template>
- <style>
- :host {
- border-top: 1px solid #ddd;
- display: block;
- position: relative;
- }
- :host(:not([expanded])) {
- cursor: pointer;
- }
- gr-avatar {
- position: absolute;
- left: var(--default-horizontal-margin);
- }
- .collapsed .contentContainer {
- color: #777;
- white-space: nowrap;
- overflow-x: hidden;
- text-overflow: ellipsis;
- }
- .showAvatar.expanded .contentContainer {
- margin-left: calc(var(--default-horizontal-margin) + 2.5em);
- padding: 10px 0;
- }
- .showAvatar.collapsed .contentContainer {
- margin-left: calc(var(--default-horizontal-margin) + 1.75em);
- padding: 10px 75px 10px 0;
- }
- .hideAvatar.collapsed .contentContainer,
- .hideAvatar.expanded .contentContainer {
- margin-left: 0;
- padding: 10px 75px 10px 0;
- }
- .collapsed gr-avatar {
- top: 8px;
- height: 1.75em;
- width: 1.75em;
- }
- .expanded gr-avatar {
- top: 12px;
- height: 2.5em;
- width: 2.5em;
- }
- .name {
- font-weight: bold;
- }
- .content {
- font-family: var(--monospace-font-family);
- }
- .collapsed .name,
- .collapsed .content,
- .collapsed .message {
- display: inline;
- }
- .collapsed gr-comment-list,
- .collapsed .replyContainer {
- display: none;
- }
- .collapsed .name {
- color: var(--default-text-color);
- }
- .expanded .name {
- cursor: pointer;
- }
- .date {
- color: #666;
- position: absolute;
- right: var(--default-horizontal-margin);
- top: 10px;
- }
- .replyContainer {
- padding: .5em 0 1em;
- }
- </style>
- <div class$="[[_computeClass(expanded, showAvatar)]]">
- <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar>
- <div class="contentContainer">
- <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div>
- <div class="content">
- <gr-linked-text class="message"
- pre="[[expanded]]"
- content="[[message.message]]"
- disabled="[[!expanded]]"
- config="[[projectConfig.commentlinks]]"></gr-linked-text>
- <gr-comment-list
- comments="[[comments]]"
- change-num="[[changeNum]]"
- patch-num="[[message._revision_number]]"></gr-comment-list>
- </div>
- <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
- <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
- </a>
- </div>
- <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
- <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
- </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>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-messages-list.html b/polygerrit-ui/app/elements/gr-messages-list.html
deleted file mode 100644
index 38bec1a..0000000
--- a/polygerrit-ui/app/elements/gr-messages-list.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-overlay.html b/polygerrit-ui/app/elements/gr-overlay.html
deleted file mode 100644
index 3119747..0000000
--- a/polygerrit-ui/app/elements/gr-overlay.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-patch-range-select.html b/polygerrit-ui/app/elements/gr-patch-range-select.html
deleted file mode 100644
index a2a5dc4..0000000
--- a/polygerrit-ui/app/elements/gr-patch-range-select.html
+++ /dev/null
@@ -1,95 +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-patch-range-select">
- <template>
- <style>
- :host {
- display: block;
- }
- .patchRange {
- display: inline-block;
- }
- </style>
- Patch set:
- <span class="patchRange">
- <select id="leftPatchSelect" on-change="_handlePatchChange">
- <option value="PARENT"
- selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option>
- <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
- <option value$="[[patchNum]]"
- selected$="[[_computeLeftSelected(patchNum, patchRange)]]"
- disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
- </template>
- </select>
- </span>
- →
- <span class="patchRange">
- <select id="rightPatchSelect" on-change="_handlePatchChange">
- <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
- <option value$="[[patchNum]]"
- selected$="[[_computeRightSelected(patchNum, patchRange)]]"
- disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
- </template>
- </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>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-related-changes-list.html b/polygerrit-ui/app/elements/gr-related-changes-list.html
deleted file mode 100644
index e3a7d2e..0000000
--- a/polygerrit-ui/app/elements/gr-related-changes-list.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-reply-dialog.html b/polygerrit-ui/app/elements/gr-reply-dialog.html
deleted file mode 100644
index ead001a..0000000
--- a/polygerrit-ui/app/elements/gr-reply-dialog.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-request.html b/polygerrit-ui/app/elements/gr-request.html
deleted file mode 100644
index d6d3a95..0000000
--- a/polygerrit-ui/app/elements/gr-request.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-reviewer-list.html b/polygerrit-ui/app/elements/gr-reviewer-list.html
deleted file mode 100644
index 2b7f0c5..0000000
--- a/polygerrit-ui/app/elements/gr-reviewer-list.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-search-bar.html b/polygerrit-ui/app/elements/gr-search-bar.html
deleted file mode 100644
index 293474c..0000000
--- a/polygerrit-ui/app/elements/gr-search-bar.html
+++ /dev/null
@@ -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>
diff --git a/polygerrit-ui/app/elements/gr-account-dropdown.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
similarity index 81%
rename from polygerrit-ui/app/elements/gr-account-dropdown.html
rename to polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
index 99850f6..7615d15 100644
--- a/polygerrit-ui/app/elements/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
@@ -14,9 +14,9 @@
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 @@
</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>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js
new file mode 100644
index 0000000..09de6c1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js
@@ -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();
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
new file mode 100644
index 0000000..3ae3b14
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<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>
+ <gr-account-dropdown></gr-account-dropdown>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-dropdown tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('tap on trigger opens menu', function() {
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.$.dropdown.opened);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
similarity index 60%
rename from polygerrit-ui/app/elements/gr-account-label.html
rename to polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index 7d35698..b7f9715 100644
--- a/polygerrit-ui/app/elements/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -14,8 +14,8 @@
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 @@
</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>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
new file mode 100644
index 0000000..98871cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -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);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
new file mode 100644
index 0000000..c39f288
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-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>
+
+<link rel="import" href="gr-account-label.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-account-label></gr-account-label>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-label tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeAccountTitle(
+ {
+ name: 'Andrew Bonventre',
+ email: 'andybons+gerrit@gmail.com'
+ }),
+ 'Andrew Bonventre <andybons+gerrit@gmail.com>');
+
+ assert.equal(element._computeAccountTitle(
+ {name: 'Andrew Bonventre'}),
+ 'Andrew Bonventre');
+
+ assert.equal(element._computeShowEmail(true,
+ {
+ name: 'Andrew Bonventre',
+ email: 'andybons+gerrit@gmail.com'
+ }), true);
+
+ assert.equal(element._computeShowEmail(true,
+ {name: 'Andrew Bonventre'}), false);
+
+ assert.equal(element._computeShowEmail(false,
+ {name: 'Andrew Bonventre'}), false);
+
+ assert.equal(element._computeShowEmail(
+ true, undefined), false);
+
+ assert.equal(element._computeShowEmail(
+ false, undefined), false);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
similarity index 65%
rename from polygerrit-ui/app/elements/gr-account-link.html
rename to polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index 8b0b726..d3585ef 100644
--- a/polygerrit-ui/app/elements/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -14,8 +14,8 @@
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 @@
</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>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
new file mode 100644
index 0000000..058b27d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -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';
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
new file mode 100644
index 0000000..e1ef862
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<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>
+
+<link rel="import" href="gr-account-link.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-account-link></gr-account-link>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-link tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeOwnerLink(
+ {
+ _account_id: 123,
+ email: 'andybons+gerrit@gmail.com'
+ }),
+ '/q/owner:andybons%2Bgerrit%40gmail.com+status:open');
+
+ assert.equal(element._computeOwnerLink({_account_id: 42}),
+ '/q/owner:42+status:open');
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
new file mode 100644
index 0000000..9a93426
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
new file mode 100644
index 0000000..7fec507
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
@@ -0,0 +1,81 @@
+// 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-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});
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
new file mode 100644
index 0000000..3491443
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -0,0 +1,31 @@
+<!--
+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 src="gr-avatar.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
new file mode 100644
index 0000000..8f289ca
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -0,0 +1,63 @@
+// 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-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;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
new file mode 100644
index 0000000..7e3c25c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-avatar</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>
+
+<link rel="import" href="gr-avatar.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-avatar></gr-avatar>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-avatar tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('methods', function() {
+ assert.equal(element._buildAvatarURL(
+ {
+ _account_id: 123
+ }),
+ '/accounts/123/avatar?s=16');
+ });
+
+ test('dom for existing account', function() {
+ assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially');
+ element.hidden = false;
+ element.imageSize = 64;
+ element.account = {
+ _account_id: 123
+ };
+ assert.isFalse(element.hasAttribute('hidden'), 'element hidden');
+ assert.isTrue(element.style.backgroundImage.indexOf('/accounts/123/avatar?s=64') > -1);
+ });
+
+ test('dom for non available account', function() {
+ assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially');
+ element.account = undefined;
+ assert.isTrue(element.hasAttribute('hidden'), 'element not hidden');
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
similarity index 70%
rename from polygerrit-ui/app/elements/gr-button.html
rename to polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 33b7871..1df98fd 100644
--- a/polygerrit-ui/app/elements/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -14,8 +14,8 @@
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="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
<dom-module id="gr-button">
<template strip-whitespace>
@@ -102,50 +102,5 @@
</style>
<content></content>
</template>
- <script>
- (function() {
- 'use strict';
-
- Polymer({
- is: 'gr-button',
-
- properties: {
- disabled: {
- type: Boolean,
- observer: '_disabledChanged',
- reflectToAttribute: true,
- },
- _enabledTabindex: {
- type: String,
- value: '0',
- },
- },
-
- behaviors: [
- Gerrit.KeyboardShortcutBehavior,
- ],
-
- hostAttributes: {
- role: 'button',
- tabindex: '0',
- },
-
- _disabledChanged: function(disabled) {
- if (disabled) {
- this._enabledTabindex = this.getAttribute('tabindex');
- }
- this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
- },
-
- _handleKey: function(e) {
- switch (e.keyCode) {
- case 32: // 'spacebar'
- case 13: // 'enter'
- e.preventDefault();
- this.click();
- }
- },
- });
- })();
- </script>
+ <script src="gr-button.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
new file mode 100644
index 0000000..772fccc
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -0,0 +1,57 @@
+// 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-button',
+
+ properties: {
+ disabled: {
+ type: Boolean,
+ observer: '_disabledChanged',
+ reflectToAttribute: true,
+ },
+ _enabledTabindex: {
+ type: String,
+ value: '0',
+ },
+ },
+
+ behaviors: [
+ Gerrit.KeyboardShortcutBehavior,
+ ],
+
+ hostAttributes: {
+ role: 'button',
+ tabindex: '0',
+ },
+
+ _disabledChanged: function(disabled) {
+ if (disabled) {
+ this._enabledTabindex = this.getAttribute('tabindex');
+ }
+ this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
+ },
+
+ _handleKey: function(e) {
+ switch (e.keyCode) {
+ case 32: // 'spacebar'
+ case 13: // 'enter'
+ e.preventDefault();
+ this.click();
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
new file mode 100644
index 0000000..62a9d2d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -0,0 +1,51 @@
+<!--
+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-request/gr-request.html">
+
+<dom-module id="gr-change-star">
+ <template>
+ <style>
+ :host {
+ display: inline-block;
+ overflow: hidden;
+ }
+ .starButton {
+ background-color: transparent;
+ border-color: transparent;
+ cursor: pointer;
+ font-size: 1.1em;
+ width: 1.2em;
+ height: 1.2em;
+ outline: none;
+ }
+ .starButton svg {
+ fill: #ccc;
+ width: 1em;
+ height: 1em;
+ }
+ .starButton-active svg {
+ fill: #ffac33;
+ }
+ </style>
+ <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap">
+ <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 -->
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657 l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657 L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg>
+ </button>
+ </template>
+ <script src="gr-change-star.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
new file mode 100644
index 0000000..26680b6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -0,0 +1,61 @@
+// 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-star',
+
+ properties: {
+ change: {
+ type: Object,
+ notify: true,
+ },
+
+ _xhrPromise: Object, // Used for testing.
+ },
+
+ _computeStarClass: function(starred) {
+ var classes = ['starButton'];
+ if (starred) {
+ classes.push('starButton-active');
+ }
+ return classes.join(' ');
+ },
+
+ _handleStarTap: function() {
+ var method = this.change.starred ? 'DELETE' : 'PUT';
+ this.set('change.starred', !this.change.starred);
+ this._send(method, this._restEndpoint()).catch(function(err) {
+ this.set('change.starred', !this.change.starred);
+ alert('Change couldn’t be starred. Check the console and contact ' +
+ 'the PolyGerrit team for assistance.');
+ throw err;
+ }.bind(this));
+ },
+
+ _send: function(method, url) {
+ var xhr = document.createElement('gr-request');
+ this._xhrPromise = xhr.send({
+ method: method,
+ url: url,
+ });
+ return this._xhrPromise;
+ },
+
+ _restEndpoint: function() {
+ return '/accounts/self/starred.changes/' + this.change._number;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
new file mode 100644
index 0000000..86ee947
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-star</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>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-star.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-star></gr-change-star>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-star tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.change = {
+ _number: 2,
+ starred: true,
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'PUT',
+ '/accounts/self/starred.changes/2',
+ [
+ 204,
+ {'Content-Type': 'application/json'},
+ ''
+ ]
+ );
+
+ server.respondWith(
+ 'DELETE',
+ '/accounts/self/starred.changes/2',
+ [
+ 204,
+ {'Content-Type': 'application/json'},
+ ''
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('star visibility states', function() {
+ element.set('change.starred', true);
+ assert.isTrue(element.$$('button').classList.contains('starButton'));
+ assert.isTrue(
+ element.$$('button').classList.contains('starButton-active'));
+
+ element.set('change.starred', false);
+ assert.isTrue(element.$$('button').classList.contains('starButton'));
+ assert.isFalse(
+ element.$$('button').classList.contains('starButton-active'));
+ });
+
+ test('starring', function(done) {
+ element.set('change.starred', false);
+ MockInteractions.tap(element.$$('button'));
+
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.equal(req.status, 204);
+ assert.equal(req.url, '/accounts/self/starred.changes/2');
+ assert.equal(element.change.starred, true);
+ done();
+ });
+ });
+
+ test('unstarring', function(done) {
+ element.set('change.starred', true);
+ MockInteractions.tap(element.$$('button'));
+
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.equal(req.status, 204);
+ assert.equal(req.url, '/accounts/self/starred.changes/2');
+ assert.equal(element.change.starred, false);
+ done();
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
new file mode 100644
index 0000000..d8fc1df
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -0,0 +1,48 @@
+<!--
+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="../gr-button/gr-button.html">
+
+<dom-module id="gr-confirm-dialog">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ header {
+ border-bottom: 1px solid #ddd;
+ font-weight: bold;
+ }
+ header,
+ main,
+ footer {
+ padding: .5em .65em;
+ }
+ footer {
+ display: flex;
+ justify-content: space-between;
+ }
+ </style>
+ <header><content select=".header"></content></header>
+ <main><content select=".main"></content></main>
+ <footer>
+ <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button>
+ <gr-button on-tap="_handleCancelTap">Cancel</gr-button>
+ </footer>
+ </template>
+ <script src="gr-confirm-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
new file mode 100644
index 0000000..0f20e0a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -0,0 +1,53 @@
+// 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-dialog',
+
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
+
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
+
+ properties: {
+ confirmLabel: {
+ type: String,
+ value: 'Confirm',
+ }
+ },
+
+ hostAttributes: {
+ role: 'dialog',
+ },
+
+ _handleConfirmTap: function(e) {
+ e.preventDefault();
+ this.fire('confirm', null, {bubbles: false});
+ },
+
+ _handleCancelTap: function(e) {
+ e.preventDefault();
+ this.fire('cancel', null, {bubbles: false});
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
new file mode 100644
index 0000000..812f32a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-confirm-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-confirm-dialog.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-confirm-dialog></gr-confirm-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-confirm-dialog tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('events', function(done) {
+ var numEvents = 0;
+ function handler() { if (++numEvents == 2) { done(); } }
+
+ element.addEventListener('confirm', handler);
+ element.addEventListener('cancel', handler);
+
+ MockInteractions.tap(element.$$('gr-button[primary]'));
+ MockInteractions.tap(element.$$('gr-button:not([primary])'));
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
similarity index 60%
copy from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
copy to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 6abf8c2..6d4b2ea 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -1,5 +1,5 @@
<!--
-Copyright (C) 2016 The Android Open Source Project
+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.
@@ -14,11 +14,16 @@
limitations under the License.
-->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<script src="../../bower_components/fetch/fetch.js"></script>
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<dom-module id="gr-rest-api-interface">
- <template></template>
- <script src="gr-rest-api-interface.js"></script>
+<dom-module id="gr-date-formatter">
+ <template>
+ <style>
+ :host {
+ display: inline;
+ }
+ </style>
+ <span>[[_computeDateStr(dateStr)]]</span>
+ </template>
+ <script src="gr-date-formatter.js"></script>
</dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
new file mode 100644
index 0000000..cb77cc1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -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 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();
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
new file mode 100644
index 0000000..9bba517
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-date-formatter</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>
+
+<link rel="import" href="gr-date-formatter.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-date-formatter tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('date is parsed correctly', function() {
+ assert.notOk((new Date('foo')).valueOf());
+ var d = element._parseDateStr(element.getAttribute('date-str'));
+ assert.isAbove(d.valueOf(), 0);
+ });
+
+ function normalizedDate(dateStr) {
+ var d = new Date(dateStr);
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
+ return d;
+ }
+
+ function testDates(nowStr, dateStr, expected) {
+ var now = normalizedDate(nowStr);
+ var t = normalizedDate(dateStr);
+ assert.equal(element._dateStr(t, now), expected);
+ }
+
+ test('dates strings are correct', function() {
+ // Within 24 hours on same day.
+ testDates('2015-07-29T20:34:00.000Z',
+ '2015-07-29T15:34:00.000Z',
+ '3:34 PM');
+ testDates('2016-01-27T17:41:00.000Z',
+ '2016-01-27T12:41:00.000Z',
+ '12:41 PM');
+
+ // Within 24 hours on different days.
+ testDates('2015-07-29T03:34:00.000Z',
+ '2015-07-28T20:25:00.000Z',
+ 'Jul 28');
+
+ // More than 24 hours. Less than six months.
+ testDates('2015-07-29T20:34:00.000Z',
+ '2015-06-15T03:25:00.000Z',
+ 'Jun 15');
+
+ // More than six months.
+ testDates('2015-09-15T20:34:00.000Z',
+ '2015-01-15T03:25:00.000Z',
+ 'Jan 15, 2015');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
similarity index 86%
rename from polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html
rename to polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 7e80baa..9988c28 100644
--- a/polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -14,8 +14,8 @@
limitations under the License.
-->
-<link rel="import" href="../bower_components/polymer/polymer.html">
-<link rel="import" href="gr-button.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-button/gr-button.html">
<dom-module id="gr-keyboard-shortcuts-dialog">
<template>
@@ -199,42 +199,5 @@
</main>
<footer></footer>
</template>
- <script>
- (function() {
- 'use strict';
-
- Polymer({
- is: 'gr-keyboard-shortcuts-dialog',
-
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
-
- properties: {
- view: String,
- },
-
- hostAttributes: {
- role: 'dialog',
- },
-
- _computeInView: function(currentView, view) {
- return view == currentView;
- },
-
- _computeInChangeListView: function(currentView) {
- return currentView == 'gr-change-list-view' ||
- currentView == 'gr-dashboard-view';
- },
-
- _handleCloseTap: function(e) {
- e.preventDefault();
- this.fire('close', null, {bubbles: false});
- },
-
- });
- })();
- </script>
+ <script src="gr-keyboard-shortcuts-dialog.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
new file mode 100644
index 0000000..7ed5012
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -0,0 +1,48 @@
+// 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-keyboard-shortcuts-dialog',
+
+ /**
+ * Fired when the user presses the close button.
+ *
+ * @event close
+ */
+
+ properties: {
+ view: String,
+ },
+
+ hostAttributes: {
+ role: 'dialog',
+ },
+
+ _computeInView: function(currentView, view) {
+ return view == currentView;
+ },
+
+ _computeInChangeListView: function(currentView) {
+ return currentView == 'gr-change-list-view' ||
+ currentView == 'gr-dashboard-view';
+ },
+
+ _handleCloseTap: function(e) {
+ e.preventDefault();
+ this.fire('close', null, {bubbles: false});
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
new file mode 100644
index 0000000..26dacd6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
@@ -0,0 +1,191 @@
+/*!
+ * JavaScript Linkify - v0.3 - 6/27/2009
+ * http://benalman.com/projects/javascript-linkify/
+ *
+ * Copyright (c) 2009 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ *
+ * Some regexps adapted from http://userscripts.org/scripts/review/7122
+ */
+
+// Script: JavaScript Linkify: Process links in text!
+//
+// *Version: 0.3, Last updated: 6/27/2009*
+//
+// Project Home - http://benalman.com/projects/javascript-linkify/
+// GitHub - http://github.com/cowboy/javascript-linkify/
+// Source - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.js
+// (Minified) - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.min.js (2.8kb)
+//
+// About: License
+//
+// Copyright (c) 2009 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+
+window.linkify = (function(){
+ var
+ SCHEME = "[a-z\\d.-]+://",
+ IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",
+ HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",
+ TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",
+ HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")",
+ PATH = "(?:[;/][^#?<>\\s]*)?",
+ QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",
+ URI1 = "\\b" + SCHEME + "[^<>\\s]+",
+ URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)",
+
+ MAILTO = "mailto:",
+ EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)",
+
+ URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ),
+ SCHEME_RE = new RegExp( "^" + SCHEME, "i" ),
+
+ quotes = {
+ "'": "`",
+ '>': '<',
+ ')': '(',
+ ']': '[',
+ '}': '{',
+ '»': '«',
+ '›': '‹'
+ },
+
+ default_options = {
+ callback: function( text, href ) {
+ return href ? '<a href="' + href + '" title="' + href + '">' + text + '</a>' : text;
+ },
+ punct_regexp: /(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/
+ };
+
+ return function( txt, options ) {
+ options = options || {};
+
+ // Temp variables.
+ var arr,
+ i,
+ link,
+ href,
+
+ // Output HTML.
+ html = '',
+
+ // Store text / link parts, in order, for re-combination.
+ parts = [],
+
+ // Used for keeping track of indices in the text.
+ idx_prev,
+ idx_last,
+ idx,
+ link_last,
+
+ // Used for trimming trailing punctuation and quotes from links.
+ matches_begin,
+ matches_end,
+ quote_begin,
+ quote_end;
+
+ // Initialize options.
+ for ( i in default_options ) {
+ if ( options[ i ] === undefined ) {
+ options[ i ] = default_options[ i ];
+ }
+ }
+
+ // Find links.
+ while ( arr = URI_RE.exec( txt ) ) {
+
+ link = arr[0];
+ idx_last = URI_RE.lastIndex;
+ idx = idx_last - link.length;
+
+ // Not a link if preceded by certain characters.
+ if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) {
+ continue;
+ }
+
+ // Trim trailing punctuation.
+ do {
+ // If no changes are made, we don't want to loop forever!
+ link_last = link;
+
+ quote_end = link.substr( -1 )
+ quote_begin = quotes[ quote_end ];
+
+ // Ending quote character?
+ if ( quote_begin ) {
+ matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) );
+ matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) );
+
+ // If quotes are unbalanced, remove trailing quote character.
+ if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) {
+ link = link.substr( 0, link.length - 1 );
+ idx_last--;
+ }
+ }
+
+ // Ending non-quote punctuation character?
+ if ( options.punct_regexp ) {
+ link = link.replace( options.punct_regexp, function(a){
+ idx_last -= a.length;
+ return '';
+ });
+ }
+ } while ( link.length && link !== link_last );
+
+ href = link;
+
+ // Add appropriate protocol to naked links.
+ if ( !SCHEME_RE.test( href ) ) {
+ href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO )
+ : !href.indexOf( 'irc.' ) ? 'irc://'
+ : !href.indexOf( 'ftp.' ) ? 'ftp://'
+ : 'http://' )
+ + href;
+ }
+
+ // Push preceding non-link text onto the array.
+ if ( idx_prev != idx ) {
+ parts.push([ txt.slice( idx_prev, idx ) ]);
+ idx_prev = idx_last;
+ }
+
+ // Push massaged link onto the array
+ parts.push([ link, href ]);
+ };
+
+ // Push remaining non-link text onto the array.
+ parts.push([ txt.substr( idx_prev ) ]);
+
+ // Process the array items.
+ for ( i = 0; i < parts.length; i++ ) {
+ html += options.callback.apply( window, parts[i] );
+ }
+
+ // In case of catastrophic failure, return the original text;
+ return html || txt;
+ };
+
+})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
new file mode 100644
index 0000000..68a98e8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -0,0 +1,39 @@
+<!--
+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="ba-linkify.js"></script>
+<script src="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 src="gr-linked-text.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
new file mode 100644
index 0000000..cb852fd
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -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-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);
+ }
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
new file mode 100644
index 0000000..08ee24b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-linked-text</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>
+
+<link rel="import" href="gr-linked-text.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-linked-text>
+ <div id="output"></div>
+ </gr-linked-text>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-linked-text tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ element.config = {
+ ph: {
+ match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
+ link: 'https://code.google.com/p/gerrit/issues/detail?id=$2'
+ },
+ changeid: {
+ match: '(I[0-9a-f]{8,40})',
+ link: '#/q/$1'
+ },
+ googlesearch: {
+ match: 'google:(.+)',
+ link: 'https://bing.com/search?q=$1', // html should supercede link.
+ html: '<a href="https://google.com/search?q=$1">$1</a>',
+ },
+ hashedhtml: {
+ match: 'hash:(.+)',
+ html: '<a href="#/awesomesauce">$1</a>',
+ },
+ disabledconfig: {
+ match: 'foo:(.+)',
+ link: 'https://google.com/search?q=$1',
+ enabled: false,
+ },
+ };
+ });
+
+ test('URL pattern was parsed and linked.', function() {
+ // Reguar inline link.
+ var url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+ element.content = url;
+ var linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, url);
+ });
+
+ test('Bug pattern was parsed and linked', function() {
+ // "Issue/Bug" pattern.
+ element.content = 'Issue 3650';
+
+ var linkEl = element.$.output.childNodes[0];
+ var url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'Issue 3650');
+
+ element.content = 'Bug 3650';
+ linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'Bug 3650');
+ });
+
+ test('Change-Id pattern was parsed and linked', function() {
+ // "Change-Id:" pattern.
+ var changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ var prefix = 'Change-Id: ';
+ element.content = prefix + changeID;
+
+ var textNode = element.$.output.childNodes[0];
+ var linkEl = element.$.output.childNodes[1];
+ assert.equal(textNode.textContent, prefix);
+ var url = '/q/' + changeID;
+ assert.equal(linkEl.target, '_blank');
+ // Since url is a path, the host is added automatically.
+ assert.isTrue(linkEl.href.endsWith(url));
+ assert.equal(linkEl.textContent, changeID);
+ });
+
+ test('Multiple matches', function() {
+ element.content = 'Issue 3650\nIssue 3450';
+ var linkEl1 = element.$.output.childNodes[0];
+ var linkEl2 = element.$.output.childNodes[2];
+
+ assert.equal(linkEl1.target, '_blank');
+ assert.equal(linkEl1.href,
+ 'https://code.google.com/p/gerrit/issues/detail?id=3650');
+ assert.equal(linkEl1.textContent, 'Issue 3650');
+
+ assert.equal(linkEl2.target, '_blank');
+ assert.equal(linkEl2.href,
+ 'https://code.google.com/p/gerrit/issues/detail?id=3450');
+ assert.equal(linkEl2.textContent, 'Issue 3450');
+ });
+
+ test('html field in link config', function() {
+ element.content = 'google:do a barrel roll';
+ var linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.href,
+ 'https://google.com/search?q=do%20a%20barrel%20roll');
+ assert.equal(linkEl.textContent, 'do a barrel roll');
+ });
+
+ test('removing hash from links', function() {
+ element.content = 'hash:foo';
+ var linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('disabled config', function() {
+ element.content = 'foo:baz';
+ assert.equal(element.$.output.innerHTML, 'foo:baz');
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
new file mode 100644
index 0000000..b4b1678
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -0,0 +1,87 @@
+// 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.
+
+'use strict';
+
+function GrLinkTextParser(linkConfig, callback) {
+ this.linkConfig = linkConfig;
+ this.callback = callback;
+ Object.preventExtensions(this);
+}
+
+GrLinkTextParser.prototype.addText = function(text, href) {
+ if (!text) {
+ return;
+ }
+ this.callback(text, href);
+};
+
+GrLinkTextParser.prototype.addHTML = function(html) {
+ this.callback(null, null, html);
+};
+
+GrLinkTextParser.prototype.parse = function(text) {
+ linkify(text, {
+ callback: this.parseChunk.bind(this)
+ });
+};
+
+GrLinkTextParser.prototype.parseChunk = function(text, href) {
+ if (href) {
+ this.addText(text, href);
+ } else {
+ this.parseLinks(text, this.linkConfig);
+ }
+};
+
+GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
+ for (var p in patterns) {
+ if (patterns[p].enabled != null && patterns[p].enabled == false) {
+ continue;
+ }
+ // PolyGerrit doesn't use hash-based navigation like GWT.
+ // Account for this.
+ // TODO(andybons): Support Gerrit being served from a base other than /,
+ // e.g. https://git.eclipse.org/r/
+ if (patterns[p].html) {
+ patterns[p].html =
+ patterns[p].html.replace(/<a href=\"#\//g, '<a href="/');
+ } else if (patterns[p].link) {
+ if (patterns[p].link[0] == '#') {
+ patterns[p].link = patterns[p].link.substr(1);
+ }
+ }
+
+ var pattern = new RegExp(patterns[p].match, 'g');
+
+ var match;
+ while ((match = pattern.exec(text)) != null) {
+ var before = text.substr(0, match.index);
+ this.addText(before);
+ text = text.substr(match.index + match[0].length);
+ var result = match[0].replace(pattern,
+ patterns[p].html || patterns[p].link);
+
+ if (patterns[p].html) {
+ this.addHTML(result);
+ } else if (patterns[p].link) {
+ this.addText(match[0], result);
+ } else {
+ throw Error('linkconfig entry ' + p +
+ ' doesn’t contain a link or html attribute.');
+ }
+ }
+ }
+ this.addText(text);
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
new file mode 100644
index 0000000..817d8c5
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -0,0 +1,32 @@
+<!--
+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 src="gr-overlay.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
new file mode 100644
index 0000000..5fa33ea
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -0,0 +1,44 @@
+// 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-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);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
similarity index 60%
copy from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
copy to polygerrit-ui/app/elements/shared/gr-request/gr-request.html
index 6abf8c2..df9eddc 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
@@ -1,5 +1,5 @@
<!--
-Copyright (C) 2016 The Android Open Source Project
+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.
@@ -14,11 +14,12 @@
limitations under the License.
-->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<script src="../../bower_components/fetch/fetch.js"></script>
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-ajax/iron-request.html">
-<dom-module id="gr-rest-api-interface">
- <template></template>
- <script src="gr-rest-api-interface.js"></script>
+<dom-module id="gr-request">
+ <template>
+ <iron-request id="xhr"></iron-request>
+ </template>
+ <script src="gr-request.js"></script>
</dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-request/gr-request.js b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
new file mode 100644
index 0000000..be24344
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
@@ -0,0 +1,36 @@
+// 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-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);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
similarity index 83%
rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 6abf8c2..10c8a29 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,8 +14,8 @@
limitations under the License.
-->
-<link rel="import" href="../../bower_components/polymer/polymer.html">
-<script src="../../bower_components/fetch/fetch.js"></script>
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<script src="../../../bower_components/fetch/fetch.js"></script>
<dom-module id="gr-rest-api-interface">
<template></template>
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
similarity index 100%
rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.js
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
similarity index 88%
rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 5ea81b9..71d747e 100644
--- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,10 +18,10 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rest-api-interface</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="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-rest-api-interface.html">
<test-fixture id="basic">
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
new file mode 100644
index 0000000..900245b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
@@ -0,0 +1,52 @@
+<!--
+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/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 src="gr-search-bar.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js
new file mode 100644
index 0000000..bef461d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js
@@ -0,0 +1,74 @@
+// 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-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;
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
new file mode 100644
index 0000000..84752e4
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-search-bar</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>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-search-bar.html">
+<script src="../../../scripts/util.js"></script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-search-bar></gr-search-bar>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-search-bar tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('value is propagated to _inputVal', function() {
+ element.value = 'foo';
+ assert.equal(element._inputVal, 'foo');
+ });
+
+ function getActiveElement() {
+ return document.activeElement.shadowRoot ?
+ document.activeElement.shadowRoot.activeElement :
+ document.activeElement;
+ }
+
+ test('tap on search button triggers nav', function(done) {
+ sinon.stub(page, 'show', function() {
+ page.show.restore();
+ assert.notEqual(getActiveElement(), element.$.searchInput);
+ assert.notEqual(getActiveElement(), element.$.searchButton);
+ done();
+ });
+ MockInteractions.tap(element.$.searchButton);
+ });
+
+ test('enter in search input triggers nav', function(done) {
+ sinon.stub(page, 'show', function() {
+ page.show.restore();
+ assert.notEqual(getActiveElement(), element.$.searchInput);
+ assert.notEqual(getActiveElement(), element.$.searchButton);
+ done();
+ });
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput, 13);
+ });
+
+ });
+</script>