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/test/gr-change-list-item-test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
similarity index 93%
rename from polygerrit-ui/app/test/gr-change-list-item-test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 5f06945..0a4aec4 100644
--- a/polygerrit-ui/app/test/gr-change-list-item-test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-item</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../elements/gr-change-list-item.html">
+<link rel="import" href="gr-change-list-item.html">
<test-fixture id="basic">
<template>
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/test/gr-change-list-test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
similarity index 93%
rename from polygerrit-ui/app/test/gr-change-list-test.html
rename to polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index edd2ab8..0575f85 100644
--- a/polygerrit-ui/app/test/gr-change-list-test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -18,14 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-list.html">
<test-fixture id="basic">
<template>
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/test/gr-change-actions-test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
similarity index 91%
rename from polygerrit-ui/app/test/gr-change-actions-test.html
rename to polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index e510e34..a89a7a5 100644
--- a/polygerrit-ui/app/test/gr-change-actions-test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-actions.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-actions.html">
<test-fixture id="basic">
<template>
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/test/gr-change-metadata-test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
similarity index 82%
rename from polygerrit-ui/app/test/gr-change-metadata-test.html
rename to polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index e4ce22f..6c97b5a 100644
--- a/polygerrit-ui/app/test/gr-change-metadata-test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -18,13 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-metadata.html">
-<script src="../scripts/util.js"></script>
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-metadata.html">
+<script src="../../../scripts/util.js"></script>
<test-fixture id="basic">
<template>
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/test/gr-change-view-test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
similarity index 90%
rename from polygerrit-ui/app/test/gr-change-view-test.html
rename to polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index f8ab059..ed9d28d 100644
--- a/polygerrit-ui/app/test/gr-change-view-test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -18,14 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-view</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-view.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-change-view.html">
<test-fixture id="basic">
<template>
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/test/gr-confirm-rebase-dialog-test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
similarity index 81%
rename from polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index 7db82e9..c02e11e 100644
--- a/polygerrit-ui/app/test/gr-confirm-rebase-dialog-test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -18,11 +18,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-rebase-dialog</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-confirm-rebase-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-confirm-rebase-dialog.html">
<test-fixture id="basic">
<template>
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/test/gr-download-dialog-test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
similarity index 92%
rename from polygerrit-ui/app/test/gr-download-dialog-test.html
rename to polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 20ddf0a..2480c4a 100644
--- a/polygerrit-ui/app/test/gr-download-dialog-test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -18,11 +18,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-download-dialog</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-download-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-download-dialog.html">
<test-fixture id="basic">
<template>
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/test/gr-file-list-test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-file-list-test.html
rename to polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index aabca50..06a01c6 100644
--- a/polygerrit-ui/app/test/gr-file-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -18,14 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-file-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-file-list.html">
<test-fixture id="basic">
<template>
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/test/gr-message-test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
similarity index 78%
rename from polygerrit-ui/app/test/gr-message-test.html
rename to polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 503da1b..0f09b70 100644
--- a/polygerrit-ui/app/test/gr-message-test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -18,13 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-message</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-message.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-message.html">
<test-fixture id="basic">
<template>
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/test/gr-messages-list-test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
similarity index 89%
rename from polygerrit-ui/app/test/gr-messages-list-test.html
rename to polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index a8ead1d..5a562ba 100644
--- a/polygerrit-ui/app/test/gr-messages-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -18,13 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-messages-list</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-messages-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-messages-list.html">
<test-fixture id="basic">
<template>
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/test/gr-related-changes-list-test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-related-changes-list-test.html
rename to polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index d6fc3c0..7e0c236 100644
--- a/polygerrit-ui/app/test/gr-related-changes-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -18,11 +18,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-related-changes-list</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-related-changes-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-related-changes-list.html">
<test-fixture id="basic">
<template>
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/test/gr-reply-dialog-test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
similarity index 90%
rename from polygerrit-ui/app/test/gr-reply-dialog-test.html
rename to polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 60e2ab0..a6f4671 100644
--- a/polygerrit-ui/app/test/gr-reply-dialog-test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -18,13 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-dialog</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-reply-dialog.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-reply-dialog.html">
<test-fixture id="basic">
<template>
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/test/gr-reviewer-list-test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
similarity index 95%
rename from polygerrit-ui/app/test/gr-reviewer-list-test.html
rename to polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 860b138..898d328 100644
--- a/polygerrit-ui/app/test/gr-reviewer-list-test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-list</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-reviewer-list.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-reviewer-list.html">
<test-fixture id="basic">
<template>
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/test/gr-diff-comment-thread-test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-diff-comment-thread-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 1218d19..52ad066 100644
--- a/polygerrit-ui/app/test/gr-diff-comment-thread-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-comment-thread</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-comment-thread.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-comment-thread.html">
<test-fixture id="basic">
<template>
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/test/gr-diff-comment-test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
similarity index 94%
rename from polygerrit-ui/app/test/gr-diff-comment-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index e63db22..799dbf2 100644
--- a/polygerrit-ui/app/test/gr-diff-comment-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -18,13 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-comment</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-comment.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-comment.html">
<test-fixture id="basic">
<template>
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/test/gr-diff-preferences-test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
similarity index 86%
rename from polygerrit-ui/app/test/gr-diff-preferences-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 9256f2f..2d86a05 100644
--- a/polygerrit-ui/app/test/gr-diff-preferences-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -18,11 +18,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-preferences</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-preferences.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-preferences.html">
<test-fixture id="basic">
<template>
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/test/gr-diff-side-test.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
similarity index 95%
rename from polygerrit-ui/app/test/gr-diff-side-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
index 95f4b78..85a1011 100644
--- a/polygerrit-ui/app/test/gr-diff-side-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-side</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-side.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-side.html">
<test-fixture id="basic">
<template>
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/test/gr-diff-view-test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
similarity index 96%
rename from polygerrit-ui/app/test/gr-diff-view-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index da3626c..bfe4906 100644
--- a/polygerrit-ui/app/test/gr-diff-view-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -18,14 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-view</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff-view.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff-view.html">
<test-fixture id="basic">
<template>
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/test/gr-diff-test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
similarity index 97%
rename from polygerrit-ui/app/test/gr-diff-test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index cd0f158..9a8cb81 100644
--- a/polygerrit-ui/app/test/gr-diff-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -18,13 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-diff.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff.html">
<test-fixture id="basic">
<template>
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/test/gr-patch-range-select-test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
similarity index 88%
rename from polygerrit-ui/app/test/gr-patch-range-select-test.html
rename to polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 1e80511..a7d909e 100644
--- a/polygerrit-ui/app/test/gr-patch-range-select-test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-patch-range-select</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-patch-range-select.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-patch-range-select.html">
<test-fixture id="basic">
<template>
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/test/gr-account-dropdown-test.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
similarity index 78%
rename from polygerrit-ui/app/test/gr-account-dropdown-test.html
rename to polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
index 795d569..3ae3b14 100644
--- a/polygerrit-ui/app/test/gr-account-dropdown-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
@@ -18,11 +18,11 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-dropdown</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-account-dropdown.html">
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-account-dropdown.html">
<test-fixture id="basic">
<template>
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/test/gr-account-label-test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
similarity index 85%
rename from polygerrit-ui/app/test/gr-account-label-test.html
rename to polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index dbfdeec..c39f288 100644
--- a/polygerrit-ui/app/test/gr-account-label-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-label</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../elements/gr-account-label.html">
+<link rel="import" href="gr-account-label.html">
<test-fixture id="basic">
<template>
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/test/gr-account-link-test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
similarity index 80%
rename from polygerrit-ui/app/test/gr-account-link-test.html
rename to polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index dfc27f8..e1ef862 100644
--- a/polygerrit-ui/app/test/gr-account-link-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -18,12 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-link</title>
-<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
-<script src="../bower_components/web-component-tester/browser.js"></script>
-<script src="../scripts/fake-app.js"></script>
-<script src="../scripts/util.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../elements/gr-account-link.html">
+<link rel="import" href="gr-account-link.html">
<test-fixture id="basic">
<template>
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/test/gr-avatar-test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
similarity index 86%
rename from polygerrit-ui/app/test/gr-avatar-test.html
rename to polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index ed80c3b..7e3c25c 100644
--- a/polygerrit-ui/app/test/gr-avatar-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -18,11 +18,11 @@
<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>
+<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="../elements/gr-avatar.html">
+<link rel="import" href="gr-avatar.html">
<test-fixture id="basic">
<template>
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/test/gr-change-star-test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
similarity index 85%
rename from polygerrit-ui/app/test/gr-change-star-test.html
rename to polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index 3f7632b..86ee947 100644
--- a/polygerrit-ui/app/test/gr-change-star-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -18,14 +18,14 @@
<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>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-change-star.html">
+<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>
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/test/gr-confirm-dialog-test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
similarity index 80%
rename from polygerrit-ui/app/test/gr-confirm-dialog-test.html
rename to polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
index d19118e..812f32a 100644
--- a/polygerrit-ui/app/test/gr-confirm-dialog-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -18,11 +18,11 @@
<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>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-confirm-dialog.html">
+<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>
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/test/gr-date-formatter-test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
similarity index 89%
rename from polygerrit-ui/app/test/gr-date-formatter-test.html
rename to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 10e8ce8..9bba517 100644
--- a/polygerrit-ui/app/test/gr-date-formatter-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -18,11 +18,11 @@
<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>
+<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="../elements/gr-date-formatter.html">
+<link rel="import" href="gr-date-formatter.html">
<test-fixture id="basic">
<template>
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/scripts/ba-linkify.js b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
similarity index 100%
rename from polygerrit-ui/app/scripts/ba-linkify.js
rename to polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
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/test/gr-linked-text-test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
similarity index 93%
rename from polygerrit-ui/app/test/gr-linked-text-test.html
rename to polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 0761948..08ee24b 100644
--- a/polygerrit-ui/app/test/gr-linked-text-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -18,11 +18,11 @@
<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>
+<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="../elements/gr-linked-text.html">
+<link rel="import" href="gr-linked-text.html">
<test-fixture id="basic">
<template>
diff --git a/polygerrit-ui/app/scripts/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
similarity index 100%
rename from polygerrit-ui/app/scripts/link-text-parser.js
rename to polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
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/test/gr-search-bar-test.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
similarity index 82%
rename from polygerrit-ui/app/test/gr-search-bar-test.html
rename to polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
index c8a5ea7..84752e4 100644
--- a/polygerrit-ui/app/test/gr-search-bar-test.html
+++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
@@ -18,13 +18,13 @@
<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>
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/page/page.js"></script>
-<link rel="import" href="../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="../elements/gr-search-bar.html">
-<script src="../scripts/util.js"></script>
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-search-bar.html">
+<script src="../../../scripts/util.js"></script>
<test-fixture id="basic">
<template>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 66230cb..fcfaaa3 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -24,36 +24,36 @@
var testFiles = [];
[
- '../elements/gr-rest-api-interface/gr-rest-api-interface_test.html',
- 'gr-account-dropdown-test.html',
- 'gr-account-label-test.html',
- 'gr-account-link-test.html',
- 'gr-avatar-test.html',
- 'gr-change-actions-test.html',
- 'gr-change-list-item-test.html',
- 'gr-change-list-test.html',
- 'gr-change-metadata-test.html',
- 'gr-change-star-test.html',
- 'gr-change-view-test.html',
- 'gr-confirm-dialog-test.html',
- 'gr-confirm-rebase-dialog-test.html',
- 'gr-date-formatter-test.html',
- 'gr-diff-comment-test.html',
- 'gr-diff-comment-thread-test.html',
- 'gr-diff-preferences-test.html',
- 'gr-diff-side-test.html',
- 'gr-diff-test.html',
- 'gr-diff-view-test.html',
- 'gr-download-dialog-test.html',
- 'gr-file-list-test.html',
- 'gr-linked-text-test.html',
- 'gr-message-test.html',
- 'gr-messages-list-test.html',
- 'gr-patch-range-select-test.html',
- 'gr-related-changes-list-test.html',
- 'gr-reply-dialog-test.html',
- 'gr-reviewer-list-test.html',
- 'gr-search-bar-test.html',
+ '../elements/change/gr-change-actions/gr-change-actions_test.html',
+ '../elements/change/gr-change-metadata/gr-change-metadata_test.html',
+ '../elements/change/gr-change-view/gr-change-view_test.html',
+ '../elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
+ '../elements/change/gr-download-dialog/gr-download-dialog_test.html',
+ '../elements/change/gr-file-list/gr-file-list_test.html',
+ '../elements/change/gr-message/gr-message_test.html',
+ '../elements/change/gr-messages-list/gr-messages-list_test.html',
+ '../elements/change/gr-related-changes-list/gr-related-changes-list_test.html',
+ '../elements/change/gr-reply-dialog/gr-reply-dialog_test.html',
+ '../elements/change/gr-reviewer-list/gr-reviewer-list_test.html',
+ '../elements/change-list/gr-change-list/gr-change-list_test.html',
+ '../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
+ '../elements/diff/gr-diff/gr-diff_test.html',
+ '../elements/diff/gr-diff-comment/gr-diff-comment_test.html',
+ '../elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
+ '../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
+ '../elements/diff/gr-diff-side/gr-diff-side_test.html',
+ '../elements/diff/gr-diff-view/gr-diff-view_test.html',
+ '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
+ '../elements/shared/gr-account-dropdown/gr-account-dropdown_test.html',
+ '../elements/shared/gr-account-label/gr-account-label_test.html',
+ '../elements/shared/gr-account-link/gr-account-link_test.html',
+ '../elements/shared/gr-avatar/gr-avatar_test.html',
+ '../elements/shared/gr-change-star/gr-change-star_test.html',
+ '../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
+ '../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
+ '../elements/shared/gr-linked-text/gr-linked-text_test.html',
+ '../elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
+ '../elements/shared/gr-search-bar/gr-search-bar_test.html',
].forEach(function(file) {
testFiles.push(file);
testFiles.push(file + '?dom=shadow');